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.
|
||||
|
||||
## 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)
|
||||
- Migrate from setup.cfg to pyproject.toml (#1243)
|
||||
- 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)
|
||||
- `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:
|
||||
|
||||
$ curl -X POST https://ihatemoney.org/api/projects \
|
||||
|
@ -97,7 +90,6 @@ Getting information about the project:
|
|||
"id": "demo",
|
||||
"name": "demonstration",
|
||||
"contact_email": "demo@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"members": [{"id": 11515, "name": "f", "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},
|
||||
|
@ -181,14 +173,8 @@ Or get a specific bill by ID:
|
|||
"creation_date": "2021-01-13",
|
||||
"what": "Raclette du nouvel an",
|
||||
"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
|
||||
the following required parameters:
|
||||
|
||||
|
@ -203,9 +189,6 @@ And optional parameters:
|
|||
|
||||
- `date`: the date of the bill (`yyyy-mm-dd` format). Defaults to
|
||||
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.
|
||||
|
||||
Returns the id of the created bill :
|
||||
|
@ -250,23 +233,3 @@ You can get some project stats with a `GET` on
|
|||
"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 wtforms.fields import BooleanField
|
||||
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.emails import send_creation_email
|
||||
from ihatemoney.forms import EditProjectForm, MemberForm, ProjectForm, get_billform_for
|
||||
from ihatemoney.models import Bill, Person, Project, db
|
||||
|
@ -50,13 +49,6 @@ def need_auth(f):
|
|||
return wrapper
|
||||
|
||||
|
||||
class CurrenciesHandler(Resource):
|
||||
currency_helper = CurrencyConverter()
|
||||
|
||||
def get(self):
|
||||
return self.currency_helper.get_currencies()
|
||||
|
||||
|
||||
class ProjectsHandler(Resource):
|
||||
def post(self):
|
||||
form = ProjectForm(meta={"csrf": False})
|
||||
|
|
|
@ -5,7 +5,6 @@ from flask_restful import Api
|
|||
from ihatemoney.api.common import (
|
||||
BillHandler,
|
||||
BillsHandler,
|
||||
CurrenciesHandler,
|
||||
MemberHandler,
|
||||
MembersHandler,
|
||||
ProjectHandler,
|
||||
|
@ -18,7 +17,6 @@ api = Blueprint("api", __name__, url_prefix="/api")
|
|||
CORS(api)
|
||||
restful_api = Api(api)
|
||||
|
||||
restful_api.add_resource(CurrenciesHandler, "/currencies")
|
||||
restful_api.add_resource(ProjectsHandler, "/projects")
|
||||
restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
|
||||
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,
|
||||
DateField,
|
||||
DecimalField,
|
||||
Label,
|
||||
PasswordField,
|
||||
SelectField,
|
||||
SelectMultipleField,
|
||||
|
@ -38,13 +37,11 @@ from wtforms.validators import (
|
|||
ValidationError,
|
||||
)
|
||||
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
|
||||
from ihatemoney.utils import (
|
||||
em_surround,
|
||||
eval_arithmetic_expression,
|
||||
generate_password_hash,
|
||||
render_localized_currency,
|
||||
slugify,
|
||||
)
|
||||
|
||||
|
@ -64,20 +61,6 @@ def get_billform_for(project, set_default=True, **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]
|
||||
|
||||
|
@ -137,30 +120,6 @@ class EditProjectForm(FlaskForm):
|
|||
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
|
||||
project_history = BooleanField(_("Enable project history"))
|
||||
ip_recording = BooleanField(_("Use IP tracking for project history"))
|
||||
currency_helper = CurrencyConverter()
|
||||
default_currency = SelectField(
|
||||
_("Default Currency"),
|
||||
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):
|
||||
project = Project.query.get(self.id.data)
|
||||
|
@ -180,28 +139,6 @@ class EditProjectForm(FlaskForm):
|
|||
else:
|
||||
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):
|
||||
"""Update the project with the information from the form"""
|
||||
project.name = self.name.data
|
||||
|
@ -217,10 +154,20 @@ class EditProjectForm(FlaskForm):
|
|||
|
||||
project.contact_email = self.contact_email.data
|
||||
project.logging_preference = self.logging_preference
|
||||
project.switch_currency(self.default_currency.data)
|
||||
|
||||
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):
|
||||
file = FileField(
|
||||
|
@ -258,7 +205,6 @@ class ProjectForm(EditProjectForm):
|
|||
password=generate_password_hash(self.password.data),
|
||||
contact_email=self.contact_email.data,
|
||||
logging_preference=self.logging_preference,
|
||||
default_currency=self.default_currency.data,
|
||||
)
|
||||
return project
|
||||
|
||||
|
@ -352,8 +298,6 @@ class BillForm(FlaskForm):
|
|||
what = StringField(_("What?"), validators=[DataRequired()])
|
||||
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
|
||||
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
|
||||
currency_helper = CurrencyConverter()
|
||||
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
|
||||
external_link = URLField(
|
||||
_("External link"),
|
||||
default="",
|
||||
|
@ -377,10 +321,8 @@ class BillForm(FlaskForm):
|
|||
amount=float(self.amount.data),
|
||||
date=self.date.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),
|
||||
payer_id=self.payer.data,
|
||||
project_default_currency=project.default_currency,
|
||||
what=self.what.data,
|
||||
bill_type=self.bill_type.data,
|
||||
)
|
||||
|
@ -393,10 +335,6 @@ class BillForm(FlaskForm):
|
|||
bill.external_link = self.external_link.data
|
||||
bill.date = self.date.data
|
||||
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
|
||||
|
||||
def fill(self, bill, project):
|
||||
|
@ -405,18 +343,9 @@ class BillForm(FlaskForm):
|
|||
self.what.data = bill.what
|
||||
self.bill_type.data = bill.bill_type
|
||||
self.external_link.data = bill.external_link
|
||||
self.original_currency.data = bill.original_currency
|
||||
self.date.data = bill.date
|
||||
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
||||
|
||||
self.original_currency.label = Label("original_currency", _("Currency"))
|
||||
self.original_currency.description = _(
|
||||
"Project default: %(currency)s",
|
||||
currency=render_localized_currency(
|
||||
project.default_currency, detailed=False
|
||||
),
|
||||
)
|
||||
|
||||
def set_default(self):
|
||||
self.payed_for.data = self.payed_for.default
|
||||
|
||||
|
@ -425,17 +354,6 @@ class BillForm(FlaskForm):
|
|||
# See https://github.com/python-babel/babel/issues/821
|
||||
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):
|
||||
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
|
||||
|
|
|
@ -102,7 +102,6 @@ def get_history(project, human_readable_names=True):
|
|||
"amount": detailed_version.amount,
|
||||
"owers": [describe_version(o) for o in detailed_version.owers],
|
||||
"external_link": detailed_version.external_link,
|
||||
"original_currency": detailed_version.original_currency,
|
||||
}
|
||||
common_properties["bill_details"] = details
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ down_revision = "cb038f79982e"
|
|||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
@ -23,7 +22,7 @@ def upgrade():
|
|||
sa.Column(
|
||||
"original_currency",
|
||||
sa.String(length=3),
|
||||
server_default=CurrencyConverter.no_currency,
|
||||
server_default="",
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
@ -42,7 +41,7 @@ def upgrade():
|
|||
sa.Column(
|
||||
"default_currency",
|
||||
sa.String(length=3),
|
||||
server_default=CurrencyConverter.no_currency,
|
||||
server_default="",
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -20,7 +20,6 @@ from sqlalchemy.sql import func
|
|||
from sqlalchemy_continuum import make_versioned, version_class
|
||||
from sqlalchemy_continuum.plugins import FlaskPlugin
|
||||
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory
|
||||
from ihatemoney.utils import generate_password_hash, get_members, same_bill
|
||||
from ihatemoney.versioning import (
|
||||
|
@ -86,7 +85,6 @@ class Project(db.Model):
|
|||
members = db.relationship("Person", backref="project")
|
||||
|
||||
query_class = ProjectQuery
|
||||
default_currency = db.Column(db.String(3))
|
||||
|
||||
@property
|
||||
def _to_serialize(self):
|
||||
|
@ -96,7 +94,6 @@ class Project(db.Model):
|
|||
"contact_email": self.contact_email,
|
||||
"logging_preference": self.logging_preference.value,
|
||||
"members": [],
|
||||
"default_currency": self.default_currency,
|
||||
}
|
||||
|
||||
balance = self.balance
|
||||
|
@ -130,16 +127,14 @@ class Project(db.Model):
|
|||
total_weight = sum(ower.weight for ower in bill.owers)
|
||||
|
||||
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:
|
||||
should_pay[ower.id] += (
|
||||
ower.weight * bill.converted_amount / total_weight
|
||||
)
|
||||
should_pay[ower.id] += ower.weight * bill.amount / total_weight
|
||||
|
||||
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:
|
||||
should_receive[ower.id] -= bill.converted_amount
|
||||
should_receive[ower.id] -= bill.amount
|
||||
|
||||
for person in self.members:
|
||||
balance = should_receive[person.id] - should_pay[person.id]
|
||||
|
@ -183,7 +178,7 @@ class Project(db.Model):
|
|||
monthly = defaultdict(lambda: defaultdict(float))
|
||||
for bill in self.get_bills_unordered().all():
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -204,7 +199,6 @@ class Project(db.Model):
|
|||
"ower": transaction["ower"].name,
|
||||
"receiver": transaction["receiver"].name,
|
||||
"amount": round(transaction["amount"], 2),
|
||||
"currency": transaction["currency"],
|
||||
}
|
||||
)
|
||||
return pretty_transactions
|
||||
|
@ -217,7 +211,6 @@ class Project(db.Model):
|
|||
"ower": members[ower_id],
|
||||
"receiver": members[receiver_id],
|
||||
"amount": amount,
|
||||
"currency": self.default_currency,
|
||||
}
|
||||
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 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):
|
||||
"""Base query for bill list"""
|
||||
# The subqueryload option allows to pre-load data from the
|
||||
|
@ -344,7 +327,6 @@ class Project(db.Model):
|
|||
"what": bill.what,
|
||||
"bill_type": bill.bill_type.value,
|
||||
"amount": round(bill.amount, 2),
|
||||
"currency": bill.original_currency,
|
||||
"date": str(bill.date),
|
||||
"payer_name": Person.query.get(bill.payer_id).name,
|
||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||
|
@ -353,41 +335,6 @@ class Project(db.Model):
|
|||
)
|
||||
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):
|
||||
"""Import bills from a list of dictionaries"""
|
||||
# Add members not already in the project
|
||||
|
@ -416,10 +363,8 @@ class Project(db.Model):
|
|||
date=parse(b["date"]),
|
||||
bill_type=b["bill_type"],
|
||||
external_link="",
|
||||
original_currency=b["currency"],
|
||||
owers=Person.query.get_by_names(b["owers"], self),
|
||||
payer_id=id_dict[b["payer_name"]],
|
||||
project_default_currency=self.default_currency,
|
||||
what=b["what"],
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -527,7 +472,6 @@ class Project(db.Model):
|
|||
name="demonstration",
|
||||
password=generate_password_hash("demo"),
|
||||
contact_email="demo@notmyidea.org",
|
||||
default_currency="XXX",
|
||||
)
|
||||
db.session.add(project)
|
||||
db.session.commit()
|
||||
|
@ -554,10 +498,8 @@ class Project(db.Model):
|
|||
Bill(
|
||||
amount=amount,
|
||||
bill_type=bill_type,
|
||||
original_currency=project.default_currency,
|
||||
owers=[members[name] for name in owers],
|
||||
payer_id=members[payer].id,
|
||||
project_default_currency=project.default_currency,
|
||||
what=what,
|
||||
)
|
||||
)
|
||||
|
@ -689,22 +631,15 @@ class Bill(db.Model):
|
|||
bill_type = db.Column(db.Enum(BillType))
|
||||
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"))
|
||||
|
||||
currency_helper = CurrencyConverter()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
amount: float,
|
||||
date: datetime.datetime = None,
|
||||
external_link: str = "",
|
||||
original_currency: str = "",
|
||||
owers: list = [],
|
||||
payer_id: int = None,
|
||||
project_default_currency: str = "",
|
||||
what: str = "",
|
||||
bill_type: str = "Expense",
|
||||
):
|
||||
|
@ -712,14 +647,10 @@ class Bill(db.Model):
|
|||
self.amount = amount
|
||||
self.date = date
|
||||
self.external_link = external_link
|
||||
self.original_currency = original_currency
|
||||
self.owers = owers
|
||||
self.payer_id = payer_id
|
||||
self.what = what
|
||||
self.bill_type = BillType(bill_type)
|
||||
self.converted_amount = self.currency_helper.exchange_currency(
|
||||
self.amount, self.original_currency, project_default_currency
|
||||
)
|
||||
|
||||
@property
|
||||
def _to_serialize(self):
|
||||
|
@ -733,8 +664,6 @@ class Bill(db.Model):
|
|||
"what": self.what,
|
||||
"bill_type": self.bill_type.value,
|
||||
"external_link": self.external_link,
|
||||
"original_currency": self.original_currency,
|
||||
"converted_amount": self.converted_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
|
||||
it differently (see balance_full function)
|
||||
"""
|
||||
return self.pay_each_default(self.converted_amount)
|
||||
return self.pay_each_default(self.amount)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
|
|
|
@ -5,18 +5,16 @@ import warnings
|
|||
|
||||
from babel.dates import LOCALTZ
|
||||
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_migrate import Migrate, stamp, upgrade
|
||||
from flask_talisman import Talisman
|
||||
from jinja2 import pass_context
|
||||
from markupsafe import Markup
|
||||
import pytz
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from ihatemoney import default_settings
|
||||
from ihatemoney.api.v1 import api as apiv1
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.models import db
|
||||
from ihatemoney.utils import (
|
||||
IhmJSONEncoder,
|
||||
|
@ -176,9 +174,6 @@ def create_app(
|
|||
# Configure the a, root="main"pplication
|
||||
setup_database(app)
|
||||
|
||||
# Setup Currency Cache
|
||||
CurrencyConverter()
|
||||
|
||||
mail = Mail()
|
||||
mail.init_app(app)
|
||||
app.mail = mail
|
||||
|
@ -220,25 +215,6 @@ def create_app(
|
|||
else:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -99,7 +99,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{ input(form.default_currency) }}
|
||||
{{ input(form.current_password) }}
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary">{{ _("Save changes") }}</button>
|
||||
|
@ -198,9 +197,6 @@
|
|||
|
||||
<details class="mb-3">
|
||||
<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) }}
|
||||
</details>
|
||||
</fieldset>
|
||||
|
|
|
@ -97,7 +97,7 @@
|
|||
<summary>{% if before %} {{ _("Details of the bill (before the change)") }} {% else %} {{ _("Details of the bill") }} {% endif %}</summary>
|
||||
{{ _("Date:") }} {{ details.date|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 }}.
|
||||
{% if details.external_link %}
|
||||
{{ _("External link:") }}
|
||||
|
@ -229,10 +229,6 @@
|
|||
{{ bill_property_change(event, _("Amount")) }}
|
||||
{% elif event.prop_changed == "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 %}
|
||||
{% trans %}Bill {{ name }} modified{% endtrans %}
|
||||
{% endif %}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{% extends "sidebar_table_layout.html" %}
|
||||
|
||||
{%- macro weighted_bill_amount(bill, weights, currency=bill.original_currency, amount=bill.amount) %}
|
||||
{{ amount|currency(currency) }}
|
||||
{%- macro weighted_bill_amount(bill, weights, amount=bill.amount) %}
|
||||
{{ amount }}
|
||||
{%- if weights != 1.0 %}
|
||||
({{ _("%(amount)s each", amount=(amount / weights)|currency(currency)) }})
|
||||
({{ _("%(amount)s each", amount=(amount / weights)) }})
|
||||
{%- endif -%}
|
||||
{% endmacro -%}
|
||||
|
||||
|
@ -142,10 +142,7 @@
|
|||
{{ bill.owers|join(', ', 'name') }}
|
||||
{%- endif %}</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) }}
|
||||
</span>
|
||||
</td>
|
||||
<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>
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
<link>{{ url_for(".list_bills", _external=True) }}</link>
|
||||
{% for (weights, bill) in bills.items -%}
|
||||
<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>
|
||||
<dc:creator>{{ bill.payer }}</dc:creator>
|
||||
{% 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>
|
||||
</item>
|
||||
{% endfor -%}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<tr receiver={{bill.receiver.id}}>
|
||||
<td>{{ bill.ower }}</td>
|
||||
<td>{{ bill.receiver }}</td>
|
||||
<td>{{ bill.amount|currency }}</td>
|
||||
<td>{{ bill.amount }}</td>
|
||||
<td>
|
||||
<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">
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
{%- endif %}
|
||||
{%- 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>
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
{% for stat in members_stats|sort(attribute='member.name') %}
|
||||
<tr>
|
||||
<td class="d-md-none">{{ stat.member.name }}</td>
|
||||
<td>{{ stat.paid|currency }}</td>
|
||||
<td>{{ stat.spent|currency }}</td>
|
||||
<td>{{ stat.paid | round(2) }}</td>
|
||||
<td>{{ stat.spent | round(2) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -28,7 +28,7 @@
|
|||
{% for month in months %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -2,8 +2,6 @@ import base64
|
|||
import datetime
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from ihatemoney.tests.common.help_functions import em_surround
|
||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||
|
||||
|
@ -11,9 +9,7 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
|||
class TestAPI(IhatemoneyTestCase):
|
||||
"""Tests the API"""
|
||||
|
||||
def api_create(
|
||||
self, name, id=None, password=None, contact=None, default_currency=None
|
||||
):
|
||||
def api_create(self, name, id=None, password=None, contact=None):
|
||||
id = id or name
|
||||
password = password or name
|
||||
contact = contact or f"{name}@notmyidea.org"
|
||||
|
@ -24,8 +20,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"password": password,
|
||||
"contact_email": contact,
|
||||
}
|
||||
if default_currency:
|
||||
data["default_currency"] = default_currency
|
||||
|
||||
return self.client.post(
|
||||
"/api/projects",
|
||||
|
@ -90,7 +84,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "raclette",
|
||||
"contact_email": "not-an-email",
|
||||
"default_currency": "XXX",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -124,7 +117,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"members": [],
|
||||
"name": "raclette",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"id": "raclette",
|
||||
"logging_preference": 1,
|
||||
}
|
||||
|
@ -136,7 +128,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"/api/projects/raclette",
|
||||
data={
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"password": "raclette",
|
||||
"name": "The raclette party",
|
||||
"project_history": "y",
|
||||
|
@ -150,7 +141,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"/api/projects/raclette",
|
||||
data={
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"current_password": "fromage aux patates",
|
||||
"password": "raclette",
|
||||
"name": "The raclette party",
|
||||
|
@ -165,7 +155,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"/api/projects/raclette",
|
||||
data={
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"current_password": "raclette",
|
||||
"password": "raclette",
|
||||
"name": "The raclette party",
|
||||
|
@ -183,7 +172,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
expected = {
|
||||
"name": "The raclette party",
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"members": [],
|
||||
"id": "raclette",
|
||||
"logging_preference": 1,
|
||||
|
@ -196,7 +184,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"/api/projects/raclette",
|
||||
data={
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"current_password": "raclette",
|
||||
"password": "tartiflette",
|
||||
"name": "The raclette party",
|
||||
|
@ -250,7 +237,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"/api/projects/raclette",
|
||||
data={
|
||||
"contact_email": "yeah@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"password": "tartiflette",
|
||||
"name": "The raclette party",
|
||||
},
|
||||
|
@ -435,8 +421,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"amount": 25.0,
|
||||
"date": "2011-08-10",
|
||||
"id": 1,
|
||||
"converted_amount": 25.0,
|
||||
"original_currency": "XXX",
|
||||
"external_link": "https://raclette.fr",
|
||||
}
|
||||
|
||||
|
@ -507,8 +491,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"amount": 25.0,
|
||||
"date": "2011-09-10",
|
||||
"external_link": "https://raclette.fr",
|
||||
"converted_amount": 25.0,
|
||||
"original_currency": "XXX",
|
||||
"id": 1,
|
||||
}
|
||||
|
||||
|
@ -588,8 +570,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"date": "2011-08-10",
|
||||
"id": id,
|
||||
"external_link": "",
|
||||
"original_currency": "XXX",
|
||||
"converted_amount": expected_amount,
|
||||
}
|
||||
|
||||
got = json.loads(req.data.decode("utf-8"))
|
||||
|
@ -624,169 +604,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
)
|
||||
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):
|
||||
# create a project
|
||||
self.api_create("raclette")
|
||||
|
@ -896,8 +713,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"date": "2011-08-10",
|
||||
"id": 1,
|
||||
"external_link": "",
|
||||
"converted_amount": 25.0,
|
||||
"original_currency": "XXX",
|
||||
}
|
||||
got = json.loads(req.data.decode("utf-8"))
|
||||
assert (
|
||||
|
@ -940,7 +755,6 @@ class TestAPI(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"name": "raclette",
|
||||
"logging_preference": 1,
|
||||
"default_currency": "XXX",
|
||||
}
|
||||
|
||||
self.assertStatus(200, req)
|
||||
|
|
|
@ -4,11 +4,9 @@ import re
|
|||
from urllib.parse import unquote, urlparse, urlunparse
|
||||
|
||||
from flask import session, url_for
|
||||
import pytest
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from ihatemoney import models
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.tests.common.help_functions import extract_link
|
||||
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||
from ihatemoney.utils import generate_password_hash
|
||||
|
@ -182,7 +180,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"contact_email": "zorglub@notmyidea.org",
|
||||
"current_password": "raclette",
|
||||
"password": "didoudida",
|
||||
"default_currency": "XXX",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
@ -259,7 +256,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
@ -287,7 +283,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"id": "raclette", # already used !
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -305,7 +300,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -326,7 +320,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -345,7 +338,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"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[1].id] = 0.0
|
||||
result[self.get_project("raclette").members[2].id] = -8.12
|
||||
|
||||
# Since we're using floating point to store currency, we can have some
|
||||
# rounding issues that prevent test from working.
|
||||
# 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",
|
||||
"password": "didoudida",
|
||||
"logging_preference": LoggingMode.ENABLED.value,
|
||||
"default_currency": "USD",
|
||||
}
|
||||
|
||||
# It should fail if we don't provide the current password
|
||||
|
@ -988,7 +980,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
project = self.get_project("raclette")
|
||||
assert project.name != new_data["name"]
|
||||
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"])
|
||||
|
||||
# It should fail if we provide the wrong current password
|
||||
|
@ -998,7 +989,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
project = self.get_project("raclette")
|
||||
assert project.name != new_data["name"]
|
||||
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"])
|
||||
|
||||
# It should work if we give the current private code
|
||||
|
@ -1008,7 +998,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
project = self.get_project("raclette")
|
||||
assert project.name == new_data["name"]
|
||||
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"])
|
||||
|
||||
# Editing a project with a wrong email address should fail
|
||||
|
@ -1055,7 +1044,7 @@ class TestBudget(IhatemoneyTestCase):
|
|||
|
||||
def test_statistics(self):
|
||||
# Output is checked with the USD sign
|
||||
self.post_project("raclette", default_currency="USD")
|
||||
self.post_project("raclette")
|
||||
|
||||
# add participants
|
||||
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
|
||||
|
@ -1117,18 +1106,18 @@ class TestBudget(IhatemoneyTestCase):
|
|||
response = self.client.get("/raclette/statistics")
|
||||
regex = r"<td class=\"d-md-none\">{}</td>\s*<td>{}</td>\s*<td>{}</td>"
|
||||
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"),
|
||||
)
|
||||
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"),
|
||||
)
|
||||
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(
|
||||
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
|
||||
|
@ -1556,225 +1545,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
member = models.Person.query.filter(models.Person.id == 1).one_or_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):
|
||||
self.post_project("raclette")
|
||||
|
||||
|
@ -1791,7 +1561,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payed_for": [1],
|
||||
"bill_type": "Expense",
|
||||
"amount": "0",
|
||||
"original_currency": "XXX",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -1836,7 +1605,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payed_for": [1],
|
||||
"bill_type": "Expense",
|
||||
"amount": "9347242149381274732472348728748723473278472843.12",
|
||||
"original_currency": "EUR",
|
||||
},
|
||||
)
|
||||
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.
|
||||
"""
|
||||
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": "peter"})
|
||||
self.client.post("/raclette/members/add", data={"name": "steven"})
|
||||
|
@ -1883,7 +1651,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 1,
|
||||
"payed_for": [1, 2, 3],
|
||||
"amount": "12",
|
||||
"original_currency": "EUR",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
)
|
||||
|
@ -1895,7 +1662,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 2,
|
||||
"payed_for": [1, 2],
|
||||
"amount": "15",
|
||||
"original_currency": "EUR",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
)
|
||||
|
@ -1907,7 +1673,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 2,
|
||||
"payed_for": [1, 2],
|
||||
"amount": "10",
|
||||
"original_currency": "EUR",
|
||||
"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" />
|
||||
<link>http://localhost/raclette/</link>
|
||||
<item>
|
||||
<title>fromage à raclette - €12.00</title>
|
||||
<title>fromage à raclette - 12.0</title>
|
||||
<guid isPermaLink="false">1</guid>
|
||||
<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
|
||||
)
|
||||
|
||||
assert """<title>charcuterie - €15.00</title>""" in content
|
||||
assert """<title>vin blanc - €10.00</title>""" in content
|
||||
assert "<title>charcuterie - 15.0</title>" in content
|
||||
assert "<title>vin blanc - 10.0</title>" in content
|
||||
|
||||
def test_rss_feed_history_disabled(self):
|
||||
"""
|
||||
Tests that RSS feeds is correctly rendered even if the project
|
||||
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": "peter"})
|
||||
self.client.post("/raclette/members/add", data={"name": "steven"})
|
||||
|
@ -1954,7 +1719,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 1,
|
||||
"payed_for": [1, 2, 3],
|
||||
"amount": "12",
|
||||
"original_currency": "EUR",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
)
|
||||
|
@ -1966,7 +1730,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 2,
|
||||
"payed_for": [1, 2],
|
||||
"amount": "15",
|
||||
"original_currency": "EUR",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
)
|
||||
|
@ -1978,7 +1741,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 2,
|
||||
"payed_for": [1, 2],
|
||||
"amount": "10",
|
||||
"original_currency": "EUR",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
)
|
||||
|
@ -1988,8 +1750,8 @@ class TestBudget(IhatemoneyTestCase):
|
|||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||
|
||||
content = resp.data.decode()
|
||||
assert """<title>charcuterie - €15.00</title>""" in content
|
||||
assert """<title>vin blanc - €10.00</title>""" in content
|
||||
assert """<title>charcuterie - 15.0</title>""" in content
|
||||
assert """<title>vin blanc - 10.0</title>""" in content
|
||||
|
||||
def test_rss_if_modified_since_header(self):
|
||||
# Project creation
|
||||
|
@ -2036,7 +1798,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payer": 1,
|
||||
"payed_for": [1],
|
||||
"amount": "12",
|
||||
"original_currency": "XXX",
|
||||
"bill_type": "Expense",
|
||||
},
|
||||
follow_redirects=True,
|
||||
|
@ -2094,7 +1855,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"payed_for": [1],
|
||||
"amount": "12",
|
||||
"bill_type": "Expense",
|
||||
"original_currency": "XXX",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
@ -2175,7 +1935,6 @@ class TestBudget(IhatemoneyTestCase):
|
|||
"contact_email": "zorglub@notmyidea.org",
|
||||
"current_password": "raclette",
|
||||
"password": "didoudida",
|
||||
"default_currency": "XXX",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ from ihatemoney import models
|
|||
from ihatemoney.utils import generate_password_hash
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("client", "converter")
|
||||
@pytest.mark.usefixtures("client")
|
||||
class BaseTestCase:
|
||||
SECRET_KEY = "TEST SESSION"
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||
|
@ -29,7 +29,6 @@ class BaseTestCase:
|
|||
self,
|
||||
id,
|
||||
follow_redirects=True,
|
||||
default_currency="XXX",
|
||||
name=None,
|
||||
password=None,
|
||||
project_history=True,
|
||||
|
@ -45,7 +44,6 @@ class BaseTestCase:
|
|||
"id": id,
|
||||
"password": password,
|
||||
"contact_email": f"{id}@notmyidea.org",
|
||||
"default_currency": default_currency,
|
||||
"project_history": project_history,
|
||||
},
|
||||
follow_redirects=follow_redirects,
|
||||
|
@ -59,7 +57,7 @@ class BaseTestCase:
|
|||
)
|
||||
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)
|
||||
password = password or id
|
||||
project = models.Project(
|
||||
|
@ -67,7 +65,6 @@ class BaseTestCase:
|
|||
name=name,
|
||||
password=generate_password_hash(password),
|
||||
contact_email=f"{id}@notmyidea.org",
|
||||
default_currency=default_currency,
|
||||
)
|
||||
models.db.session.add(project)
|
||||
models.db.session.commit()
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
from flask import Flask
|
||||
from jinja2 import FileSystemBytecodeCache
|
||||
import pytest
|
||||
|
||||
from ihatemoney.babel_utils import compile_catalogs
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.run import create_app, db
|
||||
|
||||
|
||||
|
@ -44,21 +41,3 @@ def client(app: Flask, request: pytest.FixtureRequest):
|
|||
request.cls.client = 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",
|
||||
"password": "demo",
|
||||
"contact_email": "demo@notmyidea.org",
|
||||
"default_currency": "XXX",
|
||||
"project_history": True,
|
||||
},
|
||||
)
|
||||
|
@ -43,7 +42,6 @@ class TestHistory(IhatemoneyTestCase):
|
|||
"contact_email": "demo@notmyidea.org",
|
||||
"current_password": current_password,
|
||||
"password": "demo",
|
||||
"default_currency": "XXX",
|
||||
}
|
||||
|
||||
if logging_preference != LoggingMode.DISABLED:
|
||||
|
@ -93,7 +91,6 @@ class TestHistory(IhatemoneyTestCase):
|
|||
"current_password": "demo",
|
||||
"password": "123456",
|
||||
"project_history": "y",
|
||||
"default_currency": "USD", # Currency changed from default
|
||||
}
|
||||
|
||||
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(
|
||||
"utf-8"
|
||||
).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")
|
||||
|
||||
def test_project_privacy_edit(self):
|
||||
|
@ -184,7 +181,6 @@ class TestHistory(IhatemoneyTestCase):
|
|||
"contact_email": "demo2@notmyidea.org",
|
||||
"current_password": "demo",
|
||||
"password": "123456",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
|
||||
# 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 "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
|
||||
|
||||
# Generate more operations to confirm additional IP info isn't recorded
|
||||
|
@ -315,8 +311,8 @@ class TestHistory(IhatemoneyTestCase):
|
|||
|
||||
resp = self.client.get("/demo/history")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
|
||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
|
||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 10
|
||||
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
|
||||
resp = self.client.get("/demo/strip_ip_addresses")
|
||||
|
@ -326,8 +322,8 @@ class TestHistory(IhatemoneyTestCase):
|
|||
|
||||
resp = self.client.get("/demo/history")
|
||||
assert resp.status_code == 200
|
||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
|
||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
|
||||
assert resp.data.decode("utf-8").count("127.0.0.1") == 10
|
||||
assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
|
||||
|
||||
# Clear IP Data
|
||||
resp = self.client.post(
|
||||
|
@ -350,7 +346,7 @@ class TestHistory(IhatemoneyTestCase):
|
|||
"utf-8"
|
||||
)
|
||||
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):
|
||||
# adds a member to this project
|
||||
|
@ -638,7 +634,6 @@ class TestHistory(IhatemoneyTestCase):
|
|||
"payed_for": [1],
|
||||
"bill_type": "Expense",
|
||||
"amount": "10",
|
||||
"original_currency": "EUR",
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -81,50 +81,9 @@ class CommonTestCase(object):
|
|||
for d in range(len(self.data)):
|
||||
self.data[d]["currency"] = currencies[d]
|
||||
|
||||
def test_import_currencies_in_empty_project_with_currency(self):
|
||||
# Import JSON with currencies in an empty project with a default currency
|
||||
|
||||
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.
|
||||
def test_import_single_currency_in_empty_project(self):
|
||||
# Import JSON with a single currency in an empty project
|
||||
# It should work by stripping the currency from bills.
|
||||
|
||||
self.post_project("raclette")
|
||||
self.login("raclette")
|
||||
|
@ -153,8 +112,6 @@ class CommonTestCase(object):
|
|||
if b["what"] == d["what"]:
|
||||
assert b["payer_name"] == d["payer_name"]
|
||||
assert b["amount"] == d["amount"]
|
||||
# Currency should have been stripped
|
||||
assert b["currency"] == "XXX"
|
||||
assert b["payer_weight"] == d["payer_weight"]
|
||||
assert b["date"] == d["date"]
|
||||
assert b["bill_type"] == d["bill_type"]
|
||||
|
@ -182,51 +139,7 @@ class CommonTestCase(object):
|
|||
# Check that there are no bills
|
||||
assert len(bills) == 0
|
||||
|
||||
def test_import_no_currency_in_empty_project_with_currency(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.
|
||||
|
||||
def test_import_no_currency_in_empty_project(self):
|
||||
self.post_project("raclette")
|
||||
self.login("raclette")
|
||||
|
||||
|
@ -253,7 +166,6 @@ class CommonTestCase(object):
|
|||
if b["what"] == d["what"]:
|
||||
assert b["payer_name"] == d["payer_name"]
|
||||
assert b["amount"] == d["amount"]
|
||||
assert b["currency"] == "XXX"
|
||||
assert b["payer_weight"] == d["payer_weight"]
|
||||
assert b["date"] == d["date"]
|
||||
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))
|
||||
|
||||
bills = project.get_pretty_bills()
|
||||
|
@ -311,7 +221,6 @@ class CommonTestCase(object):
|
|||
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"]
|
||||
|
@ -406,7 +315,6 @@ class TestExport(IhatemoneyTestCase):
|
|||
"bill_type": "Reimbursement",
|
||||
"what": "refund",
|
||||
"amount": 13.33,
|
||||
"currency": "XXX",
|
||||
"payer_name": "tata",
|
||||
"payer_weight": 1.0,
|
||||
"owers": ["jeanne"],
|
||||
|
@ -416,7 +324,6 @@ class TestExport(IhatemoneyTestCase):
|
|||
"bill_type": "Expense",
|
||||
"what": "red wine",
|
||||
"amount": 200.0,
|
||||
"currency": "XXX",
|
||||
"payer_name": "jeanne",
|
||||
"payer_weight": 1.0,
|
||||
"owers": ["zorglub", "tata"],
|
||||
|
@ -426,7 +333,6 @@ class TestExport(IhatemoneyTestCase):
|
|||
"bill_type": "Expense",
|
||||
"what": "\xe0 raclette",
|
||||
"amount": 10.0,
|
||||
"currency": "XXX",
|
||||
"payer_name": "zorglub",
|
||||
"payer_weight": 2.0,
|
||||
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
|
||||
|
@ -437,10 +343,10 @@ class TestExport(IhatemoneyTestCase):
|
|||
# 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,XXX,13.33,tata,1.0,jeanne",
|
||||
'2016-12-31,red wine,Expense,XXX,200.0,jeanne,1.0,"zorglub, tata"',
|
||||
'2016-12-31,à raclette,Expense,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
|
||||
"date,what,bill_type,amount,payer_name,payer_weight,owers",
|
||||
"2017-01-01,refund,Reimbursement,13.33,tata,1.0,jeanne",
|
||||
'2016-12-31,red wine,Expense,200.0,jeanne,1.0,"zorglub, tata"',
|
||||
'2016-12-31,à raclette,Expense,10.0,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
|
||||
]
|
||||
received_lines = resp.data.decode("utf-8").split("\n")
|
||||
|
||||
|
@ -452,14 +358,12 @@ class TestExport(IhatemoneyTestCase):
|
|||
expected = [
|
||||
{
|
||||
"amount": 2.00,
|
||||
"currency": "XXX",
|
||||
"receiver": "jeanne",
|
||||
"ower": "p\xe9p\xe9",
|
||||
},
|
||||
{"amount": 55.34, "currency": "XXX", "receiver": "jeanne", "ower": "tata"},
|
||||
{"amount": 55.34, "receiver": "jeanne", "ower": "tata"},
|
||||
{
|
||||
"amount": 127.33,
|
||||
"currency": "XXX",
|
||||
"receiver": "jeanne",
|
||||
"ower": "zorglub",
|
||||
},
|
||||
|
@ -471,10 +375,10 @@ class TestExport(IhatemoneyTestCase):
|
|||
resp = self.client.get("/raclette/export/transactions.csv")
|
||||
|
||||
expected = [
|
||||
"amount,currency,receiver,ower",
|
||||
"2.0,XXX,jeanne,pépé",
|
||||
"55.34,XXX,jeanne,tata",
|
||||
"127.33,XXX,jeanne,zorglub",
|
||||
"amount,receiver,ower",
|
||||
"2.0,jeanne,pépé",
|
||||
"55.34,jeanne,tata",
|
||||
"127.33,jeanne,zorglub",
|
||||
]
|
||||
received_lines = resp.data.decode("utf-8").split("\n")
|
||||
|
||||
|
@ -485,179 +389,8 @@ class TestExport(IhatemoneyTestCase):
|
|||
resp = self.client.get("/raclette/export/transactions.wrong")
|
||||
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):
|
||||
self.post_project("raclette", default_currency="EUR")
|
||||
self.post_project("raclette")
|
||||
|
||||
# add participants
|
||||
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
||||
|
@ -672,15 +405,14 @@ class TestExport(IhatemoneyTestCase):
|
|||
"payer": 1,
|
||||
"payed_for": [1],
|
||||
"amount": "10.0",
|
||||
"original_currency": "EUR",
|
||||
},
|
||||
)
|
||||
|
||||
# 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",
|
||||
"2016-12-31,'=COS(36),Expense,10.0,EUR,zorglub,1.0,zorglub",
|
||||
"date,what,bill_type,amount,payer_name,payer_weight,owers",
|
||||
"2016-12-31,'=COS(36),Expense,10.0,zorglub,1.0,zorglub",
|
||||
]
|
||||
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 ihatemoney import models
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.manage import (
|
||||
delete_project,
|
||||
generate_config,
|
||||
|
@ -376,7 +375,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
"captcha": "éùüß",
|
||||
},
|
||||
)
|
||||
|
@ -391,7 +389,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
},
|
||||
)
|
||||
assert len(models.Project.query.all()) == 0
|
||||
|
@ -403,7 +400,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
"captcha": "nope",
|
||||
},
|
||||
)
|
||||
|
@ -416,7 +412,6 @@ class TestCaptcha(IhatemoneyTestCase):
|
|||
"id": "raclette",
|
||||
"password": "party",
|
||||
"contact_email": "raclette@notmyidea.org",
|
||||
"default_currency": "USD",
|
||||
"captcha": "euro",
|
||||
},
|
||||
)
|
||||
|
@ -435,37 +430,3 @@ class TestCaptcha(IhatemoneyTestCase):
|
|||
)
|
||||
assert resp.status_code == 201
|
||||
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
|
||||
|
||||
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_babel import get_locale, lazy_gettext as _
|
||||
from flask_limiter import Limiter
|
||||
|
@ -309,7 +308,6 @@ def same_bill(bill1, bill2):
|
|||
"payer_name",
|
||||
"payer_weight",
|
||||
"amount",
|
||||
"currency",
|
||||
"date",
|
||||
"owers",
|
||||
]
|
||||
|
@ -410,21 +408,6 @@ def localize_list(items, surround_with_em=True):
|
|||
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):
|
||||
"""Like render_template(), but selects the right template according to 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.security import check_password_hash
|
||||
|
||||
from ihatemoney.currency_convertor import CurrencyConverter
|
||||
from ihatemoney.emails import send_creation_email
|
||||
from ihatemoney.forms import (
|
||||
AdminAuthenticationForm,
|
||||
|
@ -448,7 +447,6 @@ def edit_project():
|
|||
edit_form.ip_recording.data = True
|
||||
|
||||
edit_form.contact_email.data = g.project.contact_email
|
||||
edit_form.default_currency.data = g.project.default_currency
|
||||
|
||||
return render_template(
|
||||
"edit_project.html",
|
||||
|
@ -479,7 +477,6 @@ def import_project():
|
|||
attr = [
|
||||
"amount",
|
||||
"bill_type",
|
||||
"currency",
|
||||
"date",
|
||||
"owers",
|
||||
"payer_name",
|
||||
|
@ -488,28 +485,22 @@ def import_project():
|
|||
]
|
||||
currencies = set()
|
||||
for b in bills:
|
||||
if b.get("currency", "") in ["", "XXX"]:
|
||||
b["currency"] = g.project.default_currency
|
||||
if "currency" in b.keys():
|
||||
currencies.add(b["currency"])
|
||||
del b["currency"]
|
||||
for a in attr:
|
||||
if a not in b:
|
||||
raise ValueError(
|
||||
_("Missing attribute: %(attribute)s", attribute=a)
|
||||
)
|
||||
currencies.add(b["currency"])
|
||||
|
||||
# Additional checks if project has no default currency
|
||||
if g.project.default_currency == CurrencyConverter.no_currency:
|
||||
# If bills have currencies, they must be consistent
|
||||
if len(currencies - {CurrencyConverter.no_currency}) >= 2:
|
||||
if len(currencies) > 1:
|
||||
raise ValueError(
|
||||
_(
|
||||
"Cannot add bills in multiple currencies to a project without default "
|
||||
"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)
|
||||
|
||||
|
@ -863,7 +854,6 @@ def settle(amount, ower_id, payer_id):
|
|||
date=datetime.datetime.today(),
|
||||
owers=[Person.query.get(payer_id)],
|
||||
payer_id=ower_id,
|
||||
project_default_currency=g.project.default_currency,
|
||||
bill_type=BillType.REIMBURSEMENT,
|
||||
what=_("Settlement"),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue