diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py
index 0fee97793..485867a28 100644
--- a/ihatemoney/forms.py
+++ b/ihatemoney/forms.py
@@ -14,6 +14,8 @@
BooleanField,
DateField,
DecimalField,
+ HiddenField,
+ IntegerField,
Label,
PasswordField,
SelectField,
@@ -437,6 +439,22 @@ def validate_original_currency(self, field):
raise ValidationError(msg)
+class HiddenCommaDecimalField(HiddenField, CommaDecimalField):
+ pass
+
+
+class HiddenIntegerField(HiddenField, IntegerField):
+ pass
+
+
+class SettlementForm(FlaskForm):
+ """Used internally for validation, not directly visible to users"""
+
+ amount = HiddenCommaDecimalField("Amount", validators=[DataRequired()])
+ sender_id = HiddenIntegerField("Sender", validators=[DataRequired()])
+ receiver_id = HiddenIntegerField("Receiver", validators=[DataRequired()])
+
+
class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index c591b85b6..af21994d8 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -447,6 +447,10 @@ def remove_member(self, member_id):
db.session.commit()
return person
+ def has_member(self, member_id):
+ person = Person.query.get(member_id, self)
+ return person is not None
+
def remove_project(self):
# We can't import at top level without circular dependencies
from ihatemoney.history import purge_history
diff --git a/ihatemoney/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html
index 67aac337e..f344c0758 100644
--- a/ihatemoney/templates/settle_bills.html
+++ b/ihatemoney/templates/settle_bills.html
@@ -1,33 +1,49 @@
-{% extends "sidebar_table_layout.html" %}
-
-{% block sidebar %}
-
- {{ balance_table(show_weight=False) }}
-
-{% endblock %}
-
-
-{% block content %}
-
- {{ _("Who pays?") }} | {{ _("To whom?") }} | {{ _("How much?") }} | {{ _("Settled?") }} |
-
- {% for bill in bills %}
-
- {{ bill.ower }} |
- {{ bill.receiver }} |
- {{ bill.amount|currency }} |
-
-
-
-
- {{ ("Settle") }}
-
-
-
- |
+{% extends "sidebar_table_layout.html" %} {% block sidebar %}
+{{ balance_table(show_weight=False) }}
+{% endblock %} {% block content %}
+
+
+
+ {{ _("Who pays?") }} |
+ {{ _("To whom?") }} |
+ {{ _("How much?") }} |
+ {{ _("Settled?") }} |
+
+
+
+ {% for transaction in transactions %}
+
+ {{ transaction.ower }} |
+ {{ transaction.receiver }} |
+ {{ transaction.amount|currency }} |
+
+
+
+
+ {{ ("Settle") }}
+
+
+
+ |
{% endfor %}
-
-
+
+
{% endblock %}
diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py
index a3fc813f8..732535bc9 100644
--- a/ihatemoney/tests/budget_test.py
+++ b/ihatemoney/tests/budget_test.py
@@ -1358,23 +1358,25 @@ def test_settle_button(self):
count = 0
for t in transactions:
count += 1
- self.client.get(
- "/raclette/settle"
- + "/"
- + str(t["amount"])
- + "/"
- + str(t["ower"].id)
- + "/"
- + str(t["receiver"].id)
+ self.client.post(
+ "/raclette/settle",
+ data={
+ "amount": t["amount"],
+ "sender_id": t["ower"].id,
+ "receiver_id": t["receiver"].id,
+ },
)
temp_transactions = project.get_transactions_to_settle_bill()
# test if the one has disappeared
assert len(temp_transactions) == len(transactions) - count
- # test if theres a new one with bill_type reimbursement
+ # test if there is a new one with bill_type reimbursement
bill = project.get_newest_bill()
assert bill.bill_type == models.BillType.REIMBURSEMENT
- return
+
+ # There should be no more settlement to do at the end
+ transactions = project.get_transactions_to_settle_bill()
+ assert len(transactions) == 0
def test_settle_zero(self):
self.post_project("raclette")
@@ -1463,6 +1465,78 @@ def test_access_other_projects(self):
# Create and log in as another project
self.post_project("tartiflette")
+ # Add a participant in this second project
+ self.client.post("/tartiflette/members/add", data={"name": "pirate"})
+ pirate = models.Person.query.filter(models.Person.id == 5).one()
+ assert pirate.name == "pirate"
+
+ # Try to add a new bill to another project
+ resp = self.client.post(
+ "/raclette/add",
+ data={
+ "date": "2017-01-01",
+ "what": "fromage frelaté",
+ "payer": 2,
+ "payed_for": [2, 3, 4],
+ "bill_type": "Expense",
+ "amount": "100.0",
+ },
+ )
+ # Ensure it has not been created
+ raclette = self.get_project("raclette")
+ assert raclette.get_bills().count() == 1
+
+ # Try to add a new bill in our project that references members of another project.
+ # First with invalid payed_for IDs.
+ resp = self.client.post(
+ "/tartiflette/add",
+ data={
+ "date": "2017-01-01",
+ "what": "soupe",
+ "payer": 5,
+ "payed_for": [3],
+ "bill_type": "Expense",
+ "amount": "5000.0",
+ },
+ )
+ # Ensure it has not been created
+ piratebill = models.Bill.query.filter(models.Bill.what == "soupe").one_or_none()
+ assert piratebill is None, "piratebill 1 should not exist"
+
+ # Then with invalid payer ID
+ self.client.post(
+ "/tartiflette/add",
+ data={
+ "date": "2017-02-01",
+ "what": "pain",
+ "payer": 3,
+ "payed_for": [5],
+ "bill_type": "Expense",
+ "amount": "5000.0",
+ },
+ )
+ # Ensure it has not been created
+ piratebill = models.Bill.query.filter(models.Bill.what == "pain").one_or_none()
+ assert piratebill is None, "piratebill 2 should not exist"
+
+ # Make sure we can actually create valid bills
+ self.client.post(
+ "/tartiflette/add",
+ data={
+ "date": "2017-03-01",
+ "what": "baguette",
+ "payer": 5,
+ "payed_for": [5],
+ "bill_type": "Expense",
+ "amount": "5.0",
+ },
+ )
+ # Ensure it has been created
+ okbill = models.Bill.query.filter(models.Bill.what == "baguette").one_or_none()
+ assert okbill is not None, "Bill baguette should exist"
+ assert okbill.what == "baguette"
+
+ # Now try to access and modify existing bills
modified_bill = {
"date": "2018-12-31",
"what": "roblochon",
@@ -1556,6 +1630,24 @@ def test_access_other_projects(self):
member = models.Person.query.filter(models.Person.id == 1).one_or_none()
assert member is None
+ # test new settle endpoint to add bills with wrong ids
+ self.client.post("/exit")
+ self.client.post(
+ "/authenticate", data={"id": "tartiflette", "password": "tartiflette"}
+ )
+ self.client.post(
+ "/tartiflette/settle",
+ data={
+ "sender_id": 4,
+ "receiver_id": 5,
+ "amount": "42.0",
+ },
+ )
+ piratebill = models.Bill.query.filter(
+ models.Bill.bill_type == models.BillType.REIMBURSEMENT
+ ).one_or_none()
+ assert piratebill is None, "piratebill 3 should not exist"
+
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currency_switch(self):
# A project should be editable
diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py
index 7af4967bc..d39199fd3 100644
--- a/ihatemoney/utils.py
+++ b/ihatemoney/utils.py
@@ -452,7 +452,9 @@ def format_form_errors(form, prefix):
)
else:
error_list = "".join(
- str(error) for (field, errors) in form.errors.items() for error in errors
+ f"{field} {error}"
+ for (field, errors) in form.errors.items()
+ for error in errors
)
errors = f""
# I18N: Form error with a list of errors
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index 43b04c213..37bd811f8 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -56,6 +56,7 @@
ProjectForm,
ProjectFormWithCaptcha,
ResetPasswordForm,
+ SettlementForm,
get_billform_for,
)
from ihatemoney.history import get_history, get_history_queries, purge_history
@@ -852,24 +853,46 @@ def change_lang(lang):
@main.route("//settle_bills")
def settle_bill():
"""Compute the sum each one have to pay to each other and display it"""
- bills = g.project.get_transactions_to_settle_bill()
- return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
+ transactions = g.project.get_transactions_to_settle_bill()
+ settlement_form = SettlementForm()
+ return render_template(
+ "settle_bills.html",
+ transactions=transactions,
+ settlement_form=settlement_form,
+ current_view="settle_bill",
+ )
+
+
+@main.route("//settle", methods=["POST"])
+def add_settlement_bill():
+ """Create a bill to register a settlement"""
+ form = SettlementForm(id=g.project.id)
+ if not form.validate():
+ flash(
+ format_form_errors(form, _("Error creating settlement bill")),
+ category="danger",
+ )
+ return redirect(url_for(".settle_bill"))
+
+ # Ensure that the sender and receiver ID are valid and part of this project
+ receiver_id = form.receiver_id.data
+ sender_id = form.sender_id.data
+ if not g.project.has_member(sender_id):
+ return redirect(url_for(".settle_bill"))
-@main.route("//settle///")
-def settle(amount, ower_id, payer_id):
- new_reinbursement = Bill(
- amount=float(amount),
+ settlement = Bill(
+ amount=form.amount.data,
date=datetime.datetime.today(),
- owers=[Person.query.get(payer_id)],
- payer_id=ower_id,
+ owers=[Person.query.get(receiver_id, g.project)],
+ payer_id=sender_id,
project_default_currency=g.project.default_currency,
bill_type=BillType.REIMBURSEMENT,
what=_("Settlement"),
)
session.update()
- db.session.add(new_reinbursement)
+ db.session.add(settlement)
db.session.commit()
flash(_("Settlement bill has been successfully added"), category="success")