aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--budget/models.py49
-rw-r--r--budget/templates/layout.html1
-rw-r--r--budget/templates/list_bills.html12
-rw-r--r--budget/templates/settle_bill.html46
-rw-r--r--budget/tests.py59
-rw-r--r--budget/web.py7
6 files changed, 163 insertions, 11 deletions
diff --git a/budget/models.py b/budget/models.py
index 586f43b..8f57ffd 100644
--- a/budget/models.py
+++ b/budget/models.py
@@ -45,10 +45,57 @@ class Project(db.Model):
for person in self.members:
balance = should_receive[person] - should_pay[person]
- balances[person.id] = round(balance, 2)
+ balances[person] = round(balance, 2)
return balances
+ def settle_bill(self):
+ """Return a list of transactions that could be made to settle the bill"""
+ balances = self.balance
+ credits, debts = list(), list()
+ transactions = list()
+ # Create lists of credits and debts
+ for person in balances.keys():
+ if balances[person] > 0:
+ credits.append({"person": person, "balance": balances[person]})
+ elif balances[person] < 0:
+ debts.append({"person": person, "balance": -balances[person]})
+ # Try and find exact matches
+ for credit in credits:
+ match = self.exactmatch(credit["balance"], debts)
+ if match:
+ for m in match:
+ transactions.append({"ower": m["person"], "payer": credit["person"], "amount": m["balance"]})
+ debts.remove(m)
+ credits.remove(credit)
+ # Split any remaining debts & credits
+ while credits and debts:
+ if credits[0]["balance"] > debts[0]["balance"]:
+ transactions.append({"ower": debts[0]["person"], "payer": credits[0]["person"], "amount": debts[0]["balance"]})
+ credits[0]["balance"] = credits[0]["balance"] - debts[0]["balance"]
+ del debts[0]
+ else:
+ transactions.append({"ower": debts[0]["person"], "payer": credits[0]["person"], "amount": credits[0]["balance"]})
+ debts[0]["balance"] = debts[0]["balance"] - credits[0]["balance"]
+ del credits[0]
+ return transactions
+
+ def exactmatch(self, credit, debts):
+ """Recursively try and find subsets of 'debts' whose sum is equal to credit"""
+ if not debts:
+ return []
+ if debts[0]["balance"] > credit:
+ return self.exactmatch(credit, debts[1:])
+ elif debts[0]["balance"] == credit:
+ return [debts[0]]
+ else:
+ match = self.exactmatch(credit-debts[0]["balance"], debts[1:])
+ if match:
+ match.append(debts[0])
+ else:
+ match = self.exactmatch(credit, debts[1:])
+ return match
+
def has_bills(self):
"""return if the project do have bills or not"""
return self.get_bills().count() > 0
diff --git a/budget/templates/layout.html b/budget/templates/layout.html
index 8e6d3b2..27f5b5b 100644
--- a/budget/templates/layout.html
+++ b/budget/templates/layout.html
@@ -42,6 +42,7 @@
{% if g.project %}
<ul class="nav primary-nav">
<li class="active"><a href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</a></li>
+ <li><a href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</a></li>
</ul>
{% endif %}
<ul class="nav pull-right secondary-nav">
diff --git a/budget/templates/list_bills.html b/budget/templates/list_bills.html
index 389e73f..a39a78a 100644
--- a/budget/templates/list_bills.html
+++ b/budget/templates/list_bills.html
@@ -61,20 +61,20 @@
<div id="table_overflow">
<table class="balance table">
{% set balance = g.project.balance %}
- {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id] != 0 %}
- <tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
+ {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member] != 0 %}
+ <tr id="bal-member-{{ member }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
<td class="balance-name">{{ member.name }}</td>
{% if member.activated %}
<td>
- <form class="action delete" action="{{ url_for(".remove_member", member_id=member.id) }}" method="POST">
+ <form class="action delete" action="{{ url_for(".remove_member", member_id=member) }}" method="POST">
<button type="submit">{{ _("delete") }}</button></form></td>
{% else %}
<td>
- <form class="action reactivate" action="{{ url_for(".reactivate", member_id=member.id) }}" method="POST">
+ <form class="action reactivate" action="{{ url_for(".reactivate", member_id=member) }}" method="POST">
<button type="submit">{{ _("reactivate") }}</button></form></td>
{% endif %}
- <td class="balance-value {% if balance[member.id] > 0 %}positive{% elif balance[member.id] < 0 %}negative{% endif %}">
- {% if balance[member.id] > 0 %}+{% endif %}{{ balance[member.id] }}
+ <td class="balance-value {% if balance[member] > 0 %}positive{% elif balance[member] < 0 %}negative{% endif %}">
+ {% if balance[member] > 0 %}+{% endif %}{{ balance[member] }}
</td>
</tr>
{% endfor %}
diff --git a/budget/templates/settle_bill.html b/budget/templates/settle_bill.html
new file mode 100644
index 0000000..d04ff63
--- /dev/null
+++ b/budget/templates/settle_bill.html
@@ -0,0 +1,46 @@
+{% extends "layout.html" %}
+
+{% block js %}
+ $('#cancel-form').click(function(){location.href={{ url_for(".list_bills") }};});
+{% endblock %}
+{% block navbar %}
+ <li><a href="{{ url_for(".list_bills") }}">{{ _("Bills") }}</a></li>
+ <li class="active"><a href="{{ url_for(".settle_bill") }}">{{ _("Settle") }}</a></li>
+{% endblock %}
+
+{% block sidebar %}
+<div id="sidebar" class="sidebar">
+
+ <div id="table_overflow">
+ <table class="balance table">
+ {% set balance = g.project.balance %}
+ {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member] != 0 %}
+ <tr id="bal-member-{{ member }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
+ <td class="balance-name">{{ member.name }}</td>
+ <td class="balance-value {% if balance[member] > 0 %}positive{% elif balance[member] < 0 %}negative{% endif %}">
+ {% if balance[member] > 0 %}+{% endif %}{{ balance[member] }}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+</div>
+{% endblock %}
+
+
+{% block content %}
+ <table id="bill_table" class="split_bills table table-striped">
+ <thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th></tr></thead>
+ <tbody>
+ {% for bill in bills %}
+ <tr class="{{ loop.cycle("odd", "even") }}" owers={{bill.owers|join(',','id')}} payer={{bill.payer.id}}>
+ <td>{{ bill.ower }}</td>
+ <td>{{ bill.payer }}</td>
+ <td>{{ "%0.2f"|format(bill.amount) }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+
+{% endblock %}
diff --git a/budget/tests.py b/budget/tests.py
index 3fdc0fa..d848112 100644
--- a/budget/tests.py
+++ b/budget/tests.py
@@ -6,6 +6,7 @@ except ImportError:
import base64
import json
+from collections import defaultdict
from flask import session
@@ -434,7 +435,11 @@ class BudgetTestCase(TestCase):
})
balance = models.Project.query.get("raclette").balance
- self.assertDictEqual(balance, {3: -8.12, 1: 8.12, 2: 0.0})
+ result = {}
+ result[models.Project.query.get("raclette").members[0]] = 8.12
+ result[models.Project.query.get("raclette").members[1]] = 0.0
+ result[models.Project.query.get("raclette").members[2]] = -8.12
+ self.assertDictEqual(balance, result)
def test_edit_project(self):
# A project should be editable
@@ -465,6 +470,58 @@ class BudgetTestCase(TestCase):
response = self.app.get("/dashboard")
self.assertEqual(response.status_code, 200)
+ def test_settle_page(self):
+ self.post_project("raclette")
+ response = self.app.get("/raclette/settle_bill")
+ self.assertEqual(response.status_code, 200)
+
+ def test_settle(self):
+ self.post_project("raclette")
+
+ # add members
+ self.app.post("/raclette/members/add", data={'name': 'alexis'})
+ self.app.post("/raclette/members/add", data={'name': 'fred'})
+ self.app.post("/raclette/members/add", data={'name': 'tata'})
+ #Add a member with a balance=0 :
+ self.app.post("/raclette/members/add", data={'name': 'toto'})
+
+ # create bills
+ self.app.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': u'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3],
+ 'amount': '10.0',
+ })
+
+ self.app.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': u'red wine',
+ 'payer': 2,
+ 'payed_for': [1],
+ 'amount': '20',
+ })
+
+ self.app.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': u'delicatessen',
+ 'payer': 1,
+ 'payed_for': [1, 2],
+ 'amount': '10',
+ })
+ project = models.Project.query.get('raclette')
+ transactions = project.settle_bill()
+ members = defaultdict(int)
+ #We should have the same values between transactions and project balances
+ for t in transactions:
+ members[t['ower']]-=t['amount']
+ members[t['payer']]+=t['amount']
+ balance = models.Project.query.get("raclette").balance
+ for m, a in members.items():
+ self.assertEqual(a, balance[m])
+ return
+
+
class APITestCase(TestCase):
"""Tests the API"""
diff --git a/budget/web.py b/budget/web.py
index defa17e..a9f70cb 100644
--- a/budget/web.py
+++ b/budget/web.py
@@ -383,10 +383,11 @@ def change_lang(lang):
return redirect(request.headers.get('Referer') or url_for('.home'))
-@main.route("/<project_id>/compute")
-def compute_bills():
+@main.route("/<project_id>/settle_bill")
+def settle_bill():
"""Compute the sum each one have to pay to each other and display it"""
- return render_template("compute_bills.html")
+ bills = g.project.settle_bill()
+ return render_template("settle_bill.html", bills=bills)
@main.route("/<project_id>/archives/create", methods=["GET", "POST"])