Add currency to import and export formats

This commit is contained in:
Baptiste Jonglez 2021-07-17 17:31:49 +02:00 committed by zorun
parent 6448d0d7df
commit 4bf9308908
5 changed files with 307 additions and 22 deletions

View file

@ -321,7 +321,7 @@ class BillForm(FlaskForm):
bill.external_link = "" bill.external_link = ""
bill.date = self.date bill.date = self.date
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for] bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
bill.original_currency = CurrencyConverter.no_currency bill.original_currency = self.original_currency
bill.converted_amount = self.currency_helper.exchange_currency( bill.converted_amount = self.currency_helper.exchange_currency(
bill.amount, bill.original_currency, project.default_currency bill.amount, bill.original_currency, project.default_currency
) )

View file

@ -179,6 +179,7 @@ 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
@ -192,6 +193,7 @@ 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
] ]
@ -269,6 +271,7 @@ class Project(db.Model):
{ {
"what": bill.what, "what": bill.what,
"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,

View file

@ -1008,6 +1008,8 @@ class BudgetTestCase(IhatemoneyTestCase):
) )
def test_export(self): def test_export(self):
# Export a simple project without currencies
self.post_project("raclette") self.post_project("raclette")
# add members # add members
@ -1025,7 +1027,6 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3, 4], "payed_for": [1, 2, 3, 4],
"amount": "10.0", "amount": "10.0",
"original_currency": "USD",
}, },
) )
@ -1037,7 +1038,6 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 2, "payer": 2,
"payed_for": [1, 3], "payed_for": [1, 3],
"amount": "200", "amount": "200",
"original_currency": "USD",
}, },
) )
@ -1049,7 +1049,6 @@ class BudgetTestCase(IhatemoneyTestCase):
"payer": 3, "payer": 3,
"payed_for": [2], "payed_for": [2],
"amount": "13.33", "amount": "13.33",
"original_currency": "USD",
}, },
) )
@ -1060,6 +1059,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2017-01-01", "date": "2017-01-01",
"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": ["fred"], "owers": ["fred"],
@ -1068,6 +1068,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2016-12-31", "date": "2016-12-31",
"what": "red wine", "what": "red wine",
"amount": 200.0, "amount": 200.0,
"currency": "XXX",
"payer_name": "fred", "payer_name": "fred",
"payer_weight": 1.0, "payer_weight": 1.0,
"owers": ["zorglub", "tata"], "owers": ["zorglub", "tata"],
@ -1076,6 +1077,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2016-12-31", "date": "2016-12-31",
"what": "fromage \xe0 raclette", "what": "fromage \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", "fred", "tata", "p\xe9p\xe9"], "owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
@ -1086,10 +1088,10 @@ class BudgetTestCase(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,amount,payer_name,payer_weight,owers", "date,what,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,13.33,tata,1.0,fred", "2017-01-01,refund,XXX,13.33,tata,1.0,fred",
'2016-12-31,red wine,200.0,fred,1.0,"zorglub, tata"', '2016-12-31,red wine,XXX,200.0,fred,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,zorglub,2.0,"zorglub, fred, tata, pépé"', '2016-12-31,fromage à raclette,10.0,XXX,zorglub,2.0,"zorglub, fred, tata, pépé"',
] ]
received_lines = resp.data.decode("utf-8").split("\n") received_lines = resp.data.decode("utf-8").split("\n")
@ -1101,9 +1103,19 @@ class BudgetTestCase(IhatemoneyTestCase):
# generate json export of transactions # generate json export of transactions
resp = self.client.get("/raclette/export/transactions.json") resp = self.client.get("/raclette/export/transactions.json")
expected = [ expected = [
{"amount": 2.00, "receiver": "fred", "ower": "p\xe9p\xe9"}, {
{"amount": 55.34, "receiver": "fred", "ower": "tata"}, "amount": 2.00,
{"amount": 127.33, "receiver": "fred", "ower": "zorglub"}, "currency": "XXX",
"receiver": "fred",
"ower": "p\xe9p\xe9",
},
{"amount": 55.34, "currency": "XXX", "receiver": "fred", "ower": "tata"},
{
"amount": 127.33,
"currency": "XXX",
"receiver": "fred",
"ower": "zorglub",
},
] ]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected) self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
@ -1112,10 +1124,10 @@ class BudgetTestCase(IhatemoneyTestCase):
resp = self.client.get("/raclette/export/transactions.csv") resp = self.client.get("/raclette/export/transactions.csv")
expected = [ expected = [
"amount,receiver,ower", "amount,currency,receiver,ower",
"2.0,fred,pépé", "2.0,XXX,fred,pépé",
"55.34,fred,tata", "55.34,XXX,fred,tata",
"127.33,fred,zorglub", "127.33,XXX,fred,zorglub",
] ]
received_lines = resp.data.decode("utf-8").split("\n") received_lines = resp.data.decode("utf-8").split("\n")
@ -1128,9 +1140,244 @@ class BudgetTestCase(IhatemoneyTestCase):
resp = self.client.get("/raclette/export/transactions.wrong") resp = self.client.get("/raclette/export/transactions.wrong")
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
def test_export_with_currencies(self):
self.post_project("raclette", default_currency="EUR")
# add members
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
self.client.post("/raclette/members/add", data={"name": "fred"})
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": "fromage à raclette",
"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",
"payer": 2,
"payed_for": [1, 3],
"amount": "100",
"original_currency": "CAD",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2017-01-01",
"what": "refund",
"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",
"amount": 13.33,
"currency": "EUR",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
},
{
"date": "2016-12-31",
"what": "poutine from Qu\xe9bec",
"amount": 100.0,
"currency": "CAD",
"payer_name": "fred",
"payer_weight": 1.0,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"what": "fromage \xe0 raclette",
"amount": 10.0,
"currency": "EUR",
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
"date,what,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,13.33,EUR,tata,1.0,fred",
'2016-12-31,poutine from Québec,100.0,CAD,fred,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,EUR,zorglub,2.0,"zorglub, fred, tata, pépé"',
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
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": "fred",
"ower": "p\xe9p\xe9",
},
{"amount": 10.89, "currency": "EUR", "receiver": "fred", "ower": "tata"},
{"amount": 38.45, "currency": "EUR", "receiver": "fred", "ower": "zorglub"},
]
self.assertEqual(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,fred,pépé",
"10.89,EUR,fred,tata",
"38.45,EUR,fred,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
# Change project currency to CAD
project = models.Project.query.get("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": "fred",
"ower": "p\xe9p\xe9",
},
{"amount": 16.34, "currency": "CAD", "receiver": "fred", "ower": "tata"},
{"amount": 57.67, "currency": "CAD", "receiver": "fred", "ower": "zorglub"},
]
self.assertEqual(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,fred,pépé",
"16.34,CAD,fred,tata",
"57.67,CAD,fred,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
def test_import_new_project(self): def test_import_new_project(self):
# Import JSON in an empty project # Import JSON in an empty project
self.post_project("raclette", default_currency="EUR")
self.login("raclette")
project = models.Project.query.get("raclette")
json_to_import = [
{
"date": "2017-01-01",
"what": "refund",
"amount": 13.33,
"currency": "EUR",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
},
{
"date": "2016-12-31",
"what": "poutine from québec",
"amount": 50.0,
"currency": "CAD",
"payer_name": "fred",
"payer_weight": 1.0,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"what": "fromage a raclette",
"amount": 10.0,
"currency": "EUR",
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "fred", "tata", "pepe"],
},
]
from ihatemoney.web import import_project
file = io.StringIO()
json.dump(json_to_import, file)
file.seek(0)
import_project(file, project)
bills = project.get_pretty_bills()
# Check if all bills has been add
self.assertEqual(len(bills), len(json_to_import))
# Check if name of bills are ok
b = [e["what"] for e in bills]
b.sort()
ref = [e["what"] for e in json_to_import]
ref.sort()
self.assertEqual(b, ref)
# Check if other informations in bill are ok
for i in json_to_import:
for j in bills:
if j["what"] == i["what"]:
self.assertEqual(j["payer_name"], i["payer_name"])
self.assertEqual(j["amount"], i["amount"])
self.assertEqual(j["currency"], i["currency"])
self.assertEqual(j["payer_weight"], i["payer_weight"])
self.assertEqual(j["date"], i["date"])
list_project = [ower for ower in j["owers"]]
list_project.sort()
list_json = [ower for ower in i["owers"]]
list_json.sort()
self.assertEqual(list_project, list_json)
def test_import_without_currencies(self):
# Import JSON without currencies (from ihatemoney < 5) in an empty project
self.post_project("raclette") self.post_project("raclette")
self.login("raclette") self.login("raclette")
@ -1189,6 +1436,7 @@ class BudgetTestCase(IhatemoneyTestCase):
if j["what"] == i["what"]: if j["what"] == i["what"]:
self.assertEqual(j["payer_name"], i["payer_name"]) self.assertEqual(j["payer_name"], i["payer_name"])
self.assertEqual(j["amount"], i["amount"]) self.assertEqual(j["amount"], i["amount"])
self.assertEqual(j["currency"], "XXX")
self.assertEqual(j["payer_weight"], i["payer_weight"]) self.assertEqual(j["payer_weight"], i["payer_weight"])
self.assertEqual(j["date"], i["date"]) self.assertEqual(j["date"], i["date"])
@ -1226,6 +1474,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2017-01-01", "date": "2017-01-01",
"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": ["fred"], "owers": ["fred"],
@ -1234,6 +1483,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2016-12-31", "date": "2016-12-31",
"what": "red wine", "what": "red wine",
"amount": 200.0, "amount": 200.0,
"currency": "XXX",
"payer_name": "fred", "payer_name": "fred",
"payer_weight": 1.0, "payer_weight": 1.0,
"owers": ["zorglub", "tata"], "owers": ["zorglub", "tata"],
@ -1242,6 +1492,7 @@ class BudgetTestCase(IhatemoneyTestCase):
"date": "2016-12-31", "date": "2016-12-31",
"what": "fromage a raclette", "what": "fromage a 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", "fred", "tata", "pepe"], "owers": ["zorglub", "fred", "tata", "pepe"],
@ -1274,6 +1525,7 @@ class BudgetTestCase(IhatemoneyTestCase):
if j["what"] == i["what"]: if j["what"] == i["what"]:
self.assertEqual(j["payer_name"], i["payer_name"]) self.assertEqual(j["payer_name"], i["payer_name"])
self.assertEqual(j["amount"], i["amount"]) self.assertEqual(j["amount"], i["amount"])
self.assertEqual(j["currency"], i["currency"])
self.assertEqual(j["payer_weight"], i["payer_weight"]) self.assertEqual(j["payer_weight"], i["payer_weight"])
self.assertEqual(j["date"], i["date"]) self.assertEqual(j["date"], i["date"])

View file

@ -271,7 +271,7 @@ def get_members(file):
def same_bill(bill1, bill2): def same_bill(bill1, bill2):
attr = ["what", "payer_name", "payer_weight", "amount", "date", "owers"] attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"]
for a in attr: for a in attr:
if bill1[a] != bill2[a]: if bill1[a] != bill2[a]:
return False return False
@ -370,6 +370,7 @@ def localize_list(items, surround_with_em=True):
def render_localized_currency(code, detailed=True): def render_localized_currency(code, detailed=True):
# We cannot use CurrencyConvertor.no_currency here because of circular dependencies
if code == "XXX": if code == "XXX":
return _("No Currency") return _("No Currency")
locale = get_locale() or "en_US" locale = get_locale() or "en_US"

View file

@ -36,6 +36,7 @@ from sqlalchemy_continuum import Operation
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.forms import ( from ihatemoney.forms import (
AdminAuthenticationForm, AdminAuthenticationForm,
AuthenticationForm, AuthenticationForm,
@ -412,8 +413,8 @@ def edit_project():
flash(_("Project successfully uploaded")) flash(_("Project successfully uploaded"))
return redirect(url_for("main.list_bills")) return redirect(url_for("main.list_bills"))
except ValueError: except ValueError as e:
flash(_("Invalid JSON"), category="danger") flash(e.args[0], category="danger")
# Edit form # Edit form
if edit_form.validate_on_submit(): if edit_form.validate_on_submit():
@ -447,17 +448,45 @@ def import_project(file, project):
json_file = json.load(file) json_file = json.load(file)
# Check if JSON is correct # Check if JSON is correct
attr = ["what", "payer_name", "payer_weight", "amount", "date", "owers"] attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"]
attr.sort() attr.sort()
currencies = set()
for e in json_file: for e in json_file:
# Add compatibility with versions < 5 that did not have currencies
if "currency" not in e:
e["currency"] = CurrencyConverter.no_currency
# Empty currency should be converted to "XXX"
if e["currency"] == "":
e["currency"] = CurrencyConverter.no_currency
if len(e) != len(attr): if len(e) != len(attr):
raise ValueError raise ValueError(_("Invalid JSON"))
list_attr = [] list_attr = []
for i in e: for i in e:
list_attr.append(i) list_attr.append(i)
list_attr.sort() list_attr.sort()
if list_attr != attr: if list_attr != attr:
raise ValueError raise ValueError(_("Invalid JSON"))
# If the project has a default currency, convert bills that have no currency
if (
project.default_currency != CurrencyConverter.no_currency
and e["currency"] == CurrencyConverter.no_currency
):
e["currency"] = project.default_currency
# Keep track of currencies
currencies.add(e["currency"])
# Additional checks if project has no default currency
if project.default_currency == CurrencyConverter.no_currency:
# If bills have currencies, they must be consistent
if len(currencies - {CurrencyConverter.no_currency}) >= 2:
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 e in json_file:
e["currency"] = CurrencyConverter.no_currency
# From json : export list of members # From json : export list of members
members_json = get_members(json_file) members_json = get_members(json_file)
@ -505,10 +534,10 @@ def import_project(file, project):
form = get_billform_for(project) form = get_billform_for(project)
form.what = b["what"] form.what = b["what"]
form.amount = b["amount"] form.amount = b["amount"]
form.original_currency = b["currency"]
form.date = parse(b["date"]) form.date = parse(b["date"])
form.payer = id_dict[b["payer_name"]] form.payer = id_dict[b["payer_name"]]
form.payed_for = owers_id form.payed_for = owers_id
form.original_currency = b.get("original_currency")
db.session.add(form.fake_form(bill, project)) db.session.add(form.fake_form(bill, project))