From 269b37894abdd8027847c99c8ebc3981da6fa3a0 Mon Sep 17 00:00:00 2001
From: 0livd <0livd@users.noreply.github.com>
Date: Thu, 3 Aug 2017 23:55:50 +0200
Subject: [PATCH] Add a statistics tab
---
CHANGELOG.rst | 4 ++
ihatemoney/models.py | 10 +++
ihatemoney/templates/layout.html | 1 +
ihatemoney/templates/statistics.html | 35 ++++++++++
ihatemoney/tests/tests.py | 62 ++++++++++++++++++
.../translations/fr/LC_MESSAGES/messages.mo | Bin 8629 -> 8789 bytes
.../translations/fr/LC_MESSAGES/messages.po | 20 ++++++
ihatemoney/web.py | 22 +++++++
8 files changed, 154 insertions(+)
create mode 100644 ihatemoney/templates/statistics.html
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 33e1de58..94b802c6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,10 @@ This document describes changes between each past release.
- **BREAKING CHANGE** Turn the WSGI file into a python module, renamed from budget/ihatemoney.wsgi to budget/wsgi.py. Please update your Apache configuration!
- Changed the recommended gunicorn configuration to use the wsgi module as an entrypoint
+### Added
+
+- Add a statistics tab (#257)
+- Add python3.6 support (#259)
### Removed
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index 6c71a576..cd896f3c 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -144,6 +144,16 @@ class Project(db.Model):
.order_by(Bill.date.desc())\
.order_by(Bill.id.desc())
+ def get_member_bills(self, member_id):
+ """Return the list of bills related to a specific member"""
+ return Bill.query.join(Person, Project)\
+ .filter(Bill.payer_id == Person.id)\
+ .filter(Person.project_id == Project.id)\
+ .filter(Person.id == member_id)\
+ .filter(Project.id == self.id)\
+ .order_by(Bill.date.desc())\
+ .order_by(Bill.id.desc())
+
def get_pretty_bills(self, export_format="json"):
"""Return a list of project's bills with pretty formatting"""
bills = self.get_bills()
diff --git a/ihatemoney/templates/layout.html b/ihatemoney/templates/layout.html
index 76ae8903..36f01f89 100644
--- a/ihatemoney/templates/layout.html
+++ b/ihatemoney/templates/layout.html
@@ -40,6 +40,7 @@
{% block navbar %}
{{ _("Bills") }}
{{ _("Settle") }}
+ {{ _("Statistics") }}
{% endblock %}
{% endif %}
diff --git a/ihatemoney/templates/statistics.html b/ihatemoney/templates/statistics.html
new file mode 100644
index 00000000..061c6299
--- /dev/null
+++ b/ihatemoney/templates/statistics.html
@@ -0,0 +1,35 @@
+{% extends "sidebar_table_layout.html" %}
+
+{% block sidebar %}
+
+
+ {% set balance = g.project.balance %}
+ {% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
+
+ {{ member.name }} |
+
+ {% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
+ |
+
+ {% endfor %}
+
+
+{% endblock %}
+
+
+{% block content %}
+
+ {{ _("Who?") }} | {{ _("Paid") }} | {{ _("Spent") }} | {{ _("Balance") }} |
+
+ {% for member in members %}
+
+ {{ member.name }} |
+ {{ "%0.2f"|format(paid[member.id]) }} |
+ {{ "%0.2f"|format(spent[member.id]) }} |
+ {{ "%0.2f"|format(balance[member.id]) }} |
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py
index 86f11f3e..ac3551c0 100644
--- a/ihatemoney/tests/tests.py
+++ b/ihatemoney/tests/tests.py
@@ -627,6 +627,68 @@ class BudgetTestCase(IhatemoneyTestCase):
response = self.client.get("/dashboard")
self.assertEqual(response.status_code, 200)
+ def test_statistics_page(self):
+ self.post_project("raclette")
+ response = self.client.get("/raclette/statistics")
+ self.assertEqual(response.status_code, 200)
+
+ def test_statistics(self):
+ self.post_project("raclette")
+
+ # add members
+ self.client.post("/raclette/members/add", data={'name': 'alexis', 'weight': 2})
+ self.client.post("/raclette/members/add", data={'name': 'fred'})
+ self.client.post("/raclette/members/add", data={'name': 'tata'})
+ # Add a member with a balance=0 :
+ self.client.post("/raclette/members/add", data={'name': 'toto'})
+
+ # create bills
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'fromage à raclette',
+ 'payer': 1,
+ 'payed_for': [1, 2, 3],
+ 'amount': '10.0',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'red wine',
+ 'payer': 2,
+ 'payed_for': [1],
+ 'amount': '20',
+ })
+
+ self.client.post("/raclette/add", data={
+ 'date': '2011-08-10',
+ 'what': 'delicatessen',
+ 'payer': 1,
+ 'payed_for': [1, 2],
+ 'amount': '10',
+ })
+
+ response = self.client.get("/raclette/statistics")
+ self.assertIn("alexis | \n "
+ + "20.00 | \n "
+ + "31.67 | \n "
+ + "-11.67 | \n",
+ response.data.decode('utf-8'))
+ self.assertIn("fred | \n "
+ + "20.00 | \n "
+ + "5.83 | \n "
+ + "14.17 | \n",
+ response.data.decode('utf-8'))
+ self.assertIn("tata | \n "
+ + "0.00 | \n "
+ + "2.50 | \n "
+ + "-2.50 | \n",
+ response.data.decode('utf-8'))
+ self.assertIn("toto | \n "
+ + "0.00 | \n "
+ + "0.00 | \n "
+ + "0.00 | \n",
+ response.data.decode('utf-8'))
+
def test_settle_page(self):
self.post_project("raclette")
response = self.client.get("/raclette/settle_bills")
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.mo b/ihatemoney/translations/fr/LC_MESSAGES/messages.mo
index 2f46b710be1cb1d161cc8e4412eca856bbd84c02..d6011d51457b4677969f1e073b7afaaa7c05cbb5 100644
GIT binary patch
delta 2202
zcmYM!e@N7K9LMpG=U4C2bD2L%ZR+Wo*V@XaL~IM2snxLk5ss~*4K9^BlT_|*q*UKb
z8LXf%7bC)qqBL;I^XJ7MoyUZEP(IDO@0}f*OOUwW#R%+CGqc{XV`wK0n^?_viDy
zZ>vVD-SG@(kKxbd|3&_@9Nj(t^ZSg+~J%Oft^EEPNF+@NHDAbx2dQ0@Xf*3~aWbCf;T5
z_u~}qkE7=K5q(PgEEgK^SL84g*1M>Q|G`3hYM&R7)>Q6iVj3>QVtg0#uocyB2L^FB
zD$vuY%+DZ)InU`8mHjdoDxuyg!gN%_VovHf$38DdMP7r-uonHe8dbqCDv>t(yc^Z7
z2UVGasEN-YhZ*7IYJkgJXpJXOC4Pd+AcL1hn2U$-EmQ!1pqA(cYQTT34&_k4$(Vyv
zaT=DQ#$SS}PzVFKDUJH;MIR4t;YnPC2UxfmPGAkLq&ym^A4~8gzKr9j%pB6x9w@?c
zoQ2B#BTfpm1y!LgoPj&A8NW`a{%Vj<-ATf{j>@0{3$O;2af7u9*#@T7+JRc4Zd`z$
z;+r^(3OIrF(B8>Kax-t>Y+Qn>T+HP{rP_*`XfNhtKWd`yQ4Ps5D*~Ywv^H3#UgC2B|
zxy&}y#CxoLNLO$P?_FBZLWu?K$2K!eP(GGz%tbMb*R15hN|Gs>C|5rM|m)U7qA*X
zr`2Y>hV{6L-i&R2z&SXECFrBGDp87Bvcbw6wVQwD($as5!KuzA^rpuiE1YH^fI%jwLv)jT@r9o&KUp(NHuT
ziH7SV@dN&kox~5;wZyZgY<8Twmga`|uIz^%Z)Ko0)IKyA|0HKPA+<8lFf>T#$k1SK
cZC=1%XZyFs8YA(hya!I=@>n?jVc`nTGmnVl9RL6T
delta 2049
zcmYM!duYvJ9LMqRIS#v>UCcH%W|!s+&6YD-mg{k;x#SO|5F)h3tVV8ea*39xS(8xi
zS`l(`t4%JE4u2?@C6XxS(%kRF`(sa8zx`g%^ZcIQbNN2s-?=yD%;@mrR9~a+W)`i7_&^OEf~aB3}Ae$nFj}AGL|BbY&2?!2^f!)F$rflD=~rkN=z{e+a?N~
zX=uc5xCgb6lh_4sVH)1SRBS^Z{zNXbxQ_mD$bc3=O<3mY8N=sQSU9qROYvp
z6u4}wvk^7%KFq4z)lJ6-6W+3osiOV+i&9B(f&E
zirTP)ZVI68m^{4@kpmuf&mGv#C32wRe`=}&+
z=GwdPay{Q|3Tgx4gCsx^g)68~zeO+l(>szY88vafvp;G9K_pl<4twHs_q-OlYz;RZ
zSv{8EZd8P#sPVob5hDJ4QyM4}6@mV!kd|N}4nsv^0rHNmKrY+n+7F{bd=|-9YepsE
z4Of4LiquWn|2LY_dS
z6w{GtTLo(2vyjWGxzW|Opcb+ZGnn6wQqYcDP@%br+UXtF{t`9NSJVRI_@=beEYz6?
zPy-J}CD|lbuX4{fVj1l_a6Lv{`$WPN*3M>9(8N`!&!-l1aTjX93#i;^MMdh9vl|B=
zr5?mF_#4+^h|crz64KQQ7^E0SU;wL;Q?SNt;;*wgPQyezhuWc!S(IeysL1rgzF3B<
zaV{$99-@B0zM^u#PfBxHDmR5Vh}zf?)HtJ1<4?vA&dmv%4X1F32CeidYNFq$5C?J@
z1cRsrEkL4Yt5Dh9fCF$ZDhHyd_wKs-6VyUpp%(HUwUFI{3K1{j1&
zqLHYHr=TWWggWyY)I!6kftvCf;$t60+G9iB$l!!yo`&cCoJdOIGGC;w>l$AqKI4lw
W5}SF=)9@fGC(^m+XW!}EY2Lr9guib9
diff --git a/ihatemoney/translations/fr/LC_MESSAGES/messages.po b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
index 65c295de..e8b9793d 100644
--- a/ihatemoney/translations/fr/LC_MESSAGES/messages.po
+++ b/ihatemoney/translations/fr/LC_MESSAGES/messages.po
@@ -400,6 +400,10 @@ msgstr "Factures"
msgid "Settle"
msgstr "Remboursements"
+#: templates/layout.html:50
+msgid "Statistics"
+msgstr "Statistiques"
+
#: templates/layout.html:53
msgid "options"
msgstr "options"
@@ -529,3 +533,19 @@ msgstr "Qui doit payer ?"
#: templates/settle_bills.html:31
msgid "To whom?"
msgstr "Pour qui ?"
+
+#: templates/statistics.html:22
+msgid "Who?"
+msgstr "Qui ?"
+
+#: templates/statistics.html:22
+msgid "Paid"
+msgstr "A payé"
+
+#: templates/statistics.html:22
+msgid "Spent"
+msgstr "A dépensé"
+
+#: templates/statistics.html:22
+msgid "Balance"
+msgstr "Solde"
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index cc2eeac6..82e15917 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -507,6 +507,28 @@ def settle_bill():
)
+@main.route("//statistics")
+def statistics():
+ """Compute what each member has paid and spent and display it"""
+ members = g.project.active_members
+ balance = g.project.balance
+ paid = {}
+ spent = {}
+ for member in members:
+ paid[member.id] = sum([bill.amount
+ for bill in g.project.get_member_bills(member.id).all()])
+ spent[member.id] = sum([bill.pay_each() * member.weight
+ for bill in g.project.get_bills().all() if member in bill.owers])
+ return render_template(
+ "statistics.html",
+ members=members,
+ balance=balance,
+ paid=paid,
+ spent=spent,
+ current_view='statistics',
+ )
+
+
@main.route("/dashboard")
def dashboard():
return render_template("dashboard.html", projects=Project.query.all())