diff options
Diffstat (limited to 'budget')
| -rw-r--r-- | budget/models.py | 49 | ||||
| -rw-r--r-- | budget/templates/layout.html | 1 | ||||
| -rw-r--r-- | budget/templates/list_bills.html | 12 | ||||
| -rw-r--r-- | budget/templates/settle_bill.html | 46 | ||||
| -rw-r--r-- | budget/tests.py | 59 | ||||
| -rw-r--r-- | budget/web.py | 7 |
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"]) |
