Skip to content

Commit

Permalink
Display monthly statistics for the range of months where the project …
Browse files Browse the repository at this point in the history
…was active (spiral-project#885)

* Change the way we import datetime

This makes it easier to use datetime.date later.

* Display monthly statistics for the range of months where the project was active

Currently, we display a hard-coded "one year" range of monthly statistics
starting from today.  This generally is not the intended behaviour: for
instance, on an archived project, the bills might all be older than one
year, so the table only displays months without any operation.

Instead, display all months between the first and last bills.  There might
be empty months in the middle, but that's intended, because we want all
months to be consecutive.

If there are no bills, simply display an empty table.

Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
  • Loading branch information
zorun and Baptiste Jonglez committed Jan 18, 2022
1 parent 84e0416 commit b1456ec
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 9 deletions.
45 changes: 41 additions & 4 deletions ihatemoney/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from collections import defaultdict
from datetime import datetime
import datetime
import itertools

from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from debts import settle
from flask import current_app, g
from flask_sqlalchemy import BaseQuery, SQLAlchemy
Expand Down Expand Up @@ -264,6 +266,41 @@ def get_member_bills(self, member_id):
.order_by(Bill.id.desc())
)

def get_newest_bill(self):
"""Returns the most recent bill (according to bill date) or None if there are no bills"""
# Note that the ORM performs an optimized query with LIMIT
return self.get_bills_unordered().order_by(Bill.date.desc()).first()

def get_oldest_bill(self):
"""Returns the least recent bill (according to bill date) or None if there are no bills"""
# Note that the ORM performs an optimized query with LIMIT
return self.get_bills_unordered().order_by(Bill.date.asc()).first()

def active_months_range(self):
"""Returns a list of dates, representing the range of consecutive months
for which the project was active (i.e. has bills).
Note that the list might contain months during which there was no
bills. We only guarantee that there were bills during the first
and last month in the list.
"""
oldest_bill = self.get_oldest_bill()
newest_bill = self.get_newest_bill()
if oldest_bill is None or newest_bill is None:
return []
oldest_date = oldest_bill.date
newest_date = newest_bill.date
newest_month = datetime.date(
year=newest_date.year, month=newest_date.month, day=1
)
# Infinite iterator towards the past
all_months = (newest_month - relativedelta(months=i) for i in itertools.count())
# Stop when reaching one month before the first date
months = itertools.takewhile(
lambda x: x > oldest_date - relativedelta(months=1), all_months
)
return list(months)

def get_pretty_bills(self, export_format="json"):
"""Return a list of project's bills with pretty formatting"""
bills = self.get_bills()
Expand Down Expand Up @@ -608,8 +645,8 @@ def delete(self, project, id):
owers = db.relationship(Person, secondary=billowers)

amount = db.Column(db.Float)
date = db.Column(db.Date, default=datetime.now)
creation_date = db.Column(db.Date, default=datetime.now)
date = db.Column(db.Date, default=datetime.datetime.now)
creation_date = db.Column(db.Date, default=datetime.datetime.now)
what = db.Column(db.UnicodeText)
external_link = db.Column(db.UnicodeText)

Expand All @@ -623,7 +660,7 @@ def delete(self, project, id):
def __init__(
self,
amount: float,
date: datetime = None,
date: datetime.datetime = None,
external_link: str = "",
original_currency: str = "",
owers: list = [],
Expand Down
109 changes: 108 additions & 1 deletion ihatemoney/tests/budget_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import defaultdict
import datetime
import re
from time import sleep
import unittest
Expand Down Expand Up @@ -913,6 +914,19 @@ def test_statistics(self):
# Add a participant with a balance at 0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})

# Check that there are no monthly statistics and no active months
project = self.get_project("raclette")
self.assertEqual(len(project.active_months_range()), 0)
self.assertEqual(len(project.monthly_stats), 0)

# Check that the "monthly expenses" table is empty
response = self.client.get("/raclette/statistics")
regex = (
r"<table id=\"monthly_stats\".*>\s*<thead>\s*<tr>\s*<th>Period</th>\s*"
r"<th>Spent</th>\s*</tr>\s*</thead>\s*<tbody>\s*</tbody>\s*</table>"
)
self.assertRegex(response.data.decode("utf-8"), regex)

# create bills
self.client.post(
"/raclette/add",
Expand Down Expand Up @@ -948,7 +962,7 @@ def test_statistics(self):
)

response = self.client.get("/raclette/statistics")
regex = r"<td class=\"d-md-none\">{}</td>\s+<td>{}</td>\s+<td>{}</td>"
regex = r"<td class=\"d-md-none\">{}</td>\s*<td>{}</td>\s*<td>{}</td>"
self.assertRegex(
response.data.decode("utf-8"),
regex.format("zorglub", r"\$20\.00", r"\$31\.67"),
Expand Down Expand Up @@ -978,6 +992,99 @@ def test_statistics(self):
self.assertRegex(response.data.decode("utf-8"), re.compile(regex1, re.DOTALL))
self.assertRegex(response.data.decode("utf-8"), re.compile(regex2, re.DOTALL))

# Check monthly expenses again: it should have a single month and the correct amount
august = datetime.date(year=2011, month=8, day=1)
self.assertEqual(project.active_months_range(), [august])
self.assertEqual(dict(project.monthly_stats[2011]), {8: 40.0})

# Add bills for other months and check monthly expenses again
self.client.post(
"/raclette/add",
data={
"date": "2011-12-20",
"what": "fromage à raclette",
"payer": 2,
"payed_for": [1, 2],
"amount": "30",
},
)
months = [
datetime.date(year=2011, month=12, day=1),
datetime.date(year=2011, month=11, day=1),
datetime.date(year=2011, month=10, day=1),
datetime.date(year=2011, month=9, day=1),
datetime.date(year=2011, month=8, day=1),
]
amounts_2011 = {
12: 30.0,
8: 40.0,
}
self.assertEqual(project.active_months_range(), months)
self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011)

# Test more corner cases: first day of month as oldest bill
self.client.post(
"/raclette/add",
data={
"date": "2011-08-01",
"what": "ice cream",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
},
)
amounts_2011[8] += 10.0
self.assertEqual(project.active_months_range(), months)
self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011)

# Last day of month as newest bill
self.client.post(
"/raclette/add",
data={
"date": "2011-12-31",
"what": "champomy",
"payer": 1,
"payed_for": [1, 2],
"amount": "10",
},
)
amounts_2011[12] += 10.0
self.assertEqual(project.active_months_range(), months)
self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011)

# Last day of month as oldest bill
self.client.post(
"/raclette/add",
data={
"date": "2011-07-31",
"what": "smoothie",
"payer": 1,
"payed_for": [1, 2],
"amount": "20",
},
)
months.append(datetime.date(year=2011, month=7, day=1))
amounts_2011[7] = 20.0
self.assertEqual(project.active_months_range(), months)
self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011)

# First day of month as newest bill
self.client.post(
"/raclette/add",
data={
"date": "2012-01-01",
"what": "more champomy",
"payer": 2,
"payed_for": [1, 2],
"amount": "30",
},
)
months.insert(0, datetime.date(year=2012, month=1, day=1))
amounts_2012 = {1: 30.0}
self.assertEqual(project.active_months_range(), months)
self.assertEqual(dict(project.monthly_stats[2011]), amounts_2011)
self.assertEqual(dict(project.monthly_stats[2012]), amounts_2012)

def test_settle_page(self):
self.post_project("raclette")
response = self.client.get("/raclette/settle_bills")
Expand Down
7 changes: 3 additions & 4 deletions ihatemoney/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@
some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview)
"""
from datetime import datetime
from functools import wraps
import json
import os

from dateutil.relativedelta import relativedelta
from flask import (
Blueprint,
abort,
Expand Down Expand Up @@ -852,12 +850,13 @@ def strip_ip_addresses():
@main.route("/<project_id>/statistics")
def statistics():
"""Compute what each participant has paid and spent and display it"""
today = datetime.now()
# Determine range of months between which there are bills
months = g.project.active_months_range()
return render_template(
"statistics.html",
members_stats=g.project.members_stats,
monthly_stats=g.project.monthly_stats,
months=[today - relativedelta(months=i) for i in range(12)],
months=months,
current_view="statistics",
)

Expand Down

0 comments on commit b1456ec

Please sign in to comment.