2011-07-31 23:55:18 +02:00
|
|
|
from collections import defaultdict
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
from datetime import datetime
|
2013-02-18 19:18:49 +01:00
|
|
|
from flask.ext.sqlalchemy import SQLAlchemy, BaseQuery
|
2011-09-13 22:58:53 +02:00
|
|
|
from flask import g
|
2011-07-23 18:45:40 +02:00
|
|
|
|
2011-09-13 18:15:07 +02:00
|
|
|
from sqlalchemy import orm
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
db = SQLAlchemy()
|
|
|
|
|
2012-03-06 20:37:32 +01:00
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
# define models
|
2012-03-06 20:37:32 +01:00
|
|
|
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
class Project(db.Model):
|
2011-09-13 11:27:36 +02:00
|
|
|
|
2012-03-06 20:37:32 +01:00
|
|
|
_to_serialize = ("id", "name", "password", "contact_email",
|
2011-10-18 23:26:13 +02:00
|
|
|
"members", "active_members", "balance")
|
2011-09-13 11:27:36 +02:00
|
|
|
|
2015-05-01 18:28:40 +02:00
|
|
|
id = db.Column(db.String(64), primary_key=True)
|
2011-07-23 18:45:40 +02:00
|
|
|
|
|
|
|
name = db.Column(db.UnicodeText)
|
2015-05-01 18:28:40 +02:00
|
|
|
password = db.Column(db.String(128))
|
|
|
|
contact_email = db.Column(db.String(128))
|
2011-07-23 18:45:40 +02:00
|
|
|
members = db.relationship("Person", backref="project")
|
|
|
|
|
2011-07-31 23:55:18 +02:00
|
|
|
@property
|
|
|
|
def active_members(self):
|
|
|
|
return [m for m in self.members if m.activated]
|
|
|
|
|
2011-10-18 23:26:13 +02:00
|
|
|
@property
|
|
|
|
def balance(self):
|
2011-07-31 23:55:18 +02:00
|
|
|
|
2012-03-06 20:37:32 +01:00
|
|
|
balances, should_pay, should_receive = (defaultdict(int)
|
|
|
|
for time in (1, 2, 3))
|
2011-07-31 23:55:18 +02:00
|
|
|
|
|
|
|
# for each person
|
|
|
|
for person in self.members:
|
|
|
|
# get the list of bills he has to pay
|
|
|
|
bills = Bill.query.filter(Bill.owers.contains(person))
|
|
|
|
for bill in bills.all():
|
2012-03-06 20:37:32 +01:00
|
|
|
if person != bill.payer:
|
2015-08-20 10:33:43 +02:00
|
|
|
share = bill.pay_each() * person.weight
|
|
|
|
should_pay[person] += share
|
|
|
|
should_receive[bill.payer] += share
|
2011-07-31 23:55:18 +02:00
|
|
|
|
|
|
|
for person in self.members:
|
2012-03-06 20:37:32 +01:00
|
|
|
balance = should_receive[person] - should_pay[person]
|
2014-07-22 20:19:35 +02:00
|
|
|
balances[person.id] = balance
|
2011-07-31 23:55:18 +02:00
|
|
|
|
|
|
|
return balances
|
|
|
|
|
2015-08-23 11:58:27 +02:00
|
|
|
@property
|
|
|
|
def uses_weights(self):
|
|
|
|
return len([i for i in self.members if i.weight != 1]) > 0
|
|
|
|
|
2013-04-07 22:14:32 +02:00
|
|
|
def get_transactions_to_settle_bill(self):
|
2012-03-12 00:35:06 +01:00
|
|
|
"""Return a list of transactions that could be made to settle the bill"""
|
2013-05-09 23:23:23 +02:00
|
|
|
#cache value for better performance
|
|
|
|
balance = self.balance
|
2013-04-07 20:25:25 +02:00
|
|
|
credits, debts, transactions = [],[],[]
|
2012-03-12 00:35:06 +01:00
|
|
|
# Create lists of credits and debts
|
2013-04-07 20:25:25 +02:00
|
|
|
for person in self.members:
|
2017-01-02 14:52:59 +01:00
|
|
|
if round(balance[person.id], 2) > 0:
|
2013-05-09 23:23:23 +02:00
|
|
|
credits.append({"person": person, "balance": balance[person.id]})
|
2017-01-02 14:52:59 +01:00
|
|
|
elif round(balance[person.id], 2) < 0:
|
2013-05-09 23:23:23 +02:00
|
|
|
debts.append({"person": person, "balance": -balance[person.id]})
|
2012-03-12 00:35:06 +01:00
|
|
|
# Try and find exact matches
|
|
|
|
for credit in credits:
|
2015-07-05 22:16:38 +02:00
|
|
|
match = self.exactmatch(round(credit["balance"], 2), debts)
|
2012-03-12 00:35:06 +01:00
|
|
|
if match:
|
|
|
|
for m in match:
|
2013-04-07 22:14:32 +02:00
|
|
|
transactions.append({"ower": m["person"], "receiver": credit["person"], "amount": m["balance"]})
|
2012-03-12 00:35:06 +01:00
|
|
|
debts.remove(m)
|
|
|
|
credits.remove(credit)
|
|
|
|
# Split any remaining debts & credits
|
|
|
|
while credits and debts:
|
|
|
|
if credits[0]["balance"] > debts[0]["balance"]:
|
2013-04-07 22:14:32 +02:00
|
|
|
transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": debts[0]["balance"]})
|
2012-03-12 00:35:06 +01:00
|
|
|
credits[0]["balance"] = credits[0]["balance"] - debts[0]["balance"]
|
|
|
|
del debts[0]
|
|
|
|
else:
|
2013-04-07 22:14:32 +02:00
|
|
|
transactions.append({"ower": debts[0]["person"], "receiver": credits[0]["person"], "amount": credits[0]["balance"]})
|
2012-03-12 00:35:06 +01:00
|
|
|
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:
|
2013-04-07 20:25:25 +02:00
|
|
|
return None
|
2012-03-12 00:35:06 +01:00
|
|
|
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
|
|
|
|
|
2011-12-03 22:25:19 +01:00
|
|
|
def has_bills(self):
|
|
|
|
"""return if the project do have bills or not"""
|
2012-03-06 20:37:32 +01:00
|
|
|
return self.get_bills().count() > 0
|
2011-12-03 22:25:19 +01:00
|
|
|
|
2011-09-09 19:57:28 +02:00
|
|
|
def get_bills(self):
|
|
|
|
"""Return the list of bills related to this project"""
|
|
|
|
return Bill.query.join(Person, Project)\
|
|
|
|
.filter(Bill.payer_id == Person.id)\
|
|
|
|
.filter(Person.project_id == Project.id)\
|
|
|
|
.filter(Project.id == self.id)\
|
|
|
|
.order_by(Bill.date.desc())
|
|
|
|
|
|
|
|
def remove_member(self, member_id):
|
|
|
|
"""Remove a member from the project.
|
|
|
|
|
|
|
|
If the member is not bound to a bill, then he is deleted, otherwise
|
|
|
|
he is only deactivated.
|
|
|
|
|
|
|
|
This method returns the status DELETED or DEACTIVATED regarding the
|
|
|
|
changes made.
|
|
|
|
"""
|
2013-04-08 11:29:31 +02:00
|
|
|
try:
|
|
|
|
person = Person.query.get(member_id, self)
|
|
|
|
except orm.exc.NoResultFound:
|
|
|
|
return None
|
2011-10-08 13:22:18 +02:00
|
|
|
if not person.has_bills():
|
|
|
|
db.session.delete(person)
|
|
|
|
db.session.commit()
|
|
|
|
else:
|
|
|
|
person.activated = False
|
|
|
|
db.session.commit()
|
2011-09-09 19:57:28 +02:00
|
|
|
return person
|
|
|
|
|
2011-11-02 12:16:01 +01:00
|
|
|
def remove_project(self):
|
|
|
|
db.session.delete(self)
|
|
|
|
db.session.commit()
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
def __repr__(self):
|
|
|
|
return "<Project %s>" % self.name
|
|
|
|
|
|
|
|
|
|
|
|
class Person(db.Model):
|
2011-09-13 11:27:36 +02:00
|
|
|
|
2011-09-13 22:58:53 +02:00
|
|
|
class PersonQuery(BaseQuery):
|
|
|
|
def get_by_name(self, name, project):
|
|
|
|
return Person.query.filter(Person.name == name)\
|
|
|
|
.filter(Project.id == project.id).one()
|
|
|
|
|
|
|
|
def get(self, id, project=None):
|
|
|
|
if not project:
|
|
|
|
project = g.project
|
|
|
|
return Person.query.filter(Person.id == id)\
|
|
|
|
.filter(Project.id == project.id).one()
|
|
|
|
|
|
|
|
query_class = PersonQuery
|
|
|
|
|
2015-08-20 11:11:09 +02:00
|
|
|
_to_serialize = ("id", "name", "weight", "activated")
|
2011-09-13 11:27:36 +02:00
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2015-05-01 18:28:40 +02:00
|
|
|
project_id = db.Column(db.String(64), db.ForeignKey("project.id"))
|
2011-07-23 18:45:40 +02:00
|
|
|
bills = db.relationship("Bill", backref="payer")
|
|
|
|
|
|
|
|
name = db.Column(db.UnicodeText)
|
2015-08-20 10:33:43 +02:00
|
|
|
weight = db.Column(db.Float, default=1)
|
2011-07-31 23:55:18 +02:00
|
|
|
activated = db.Column(db.Boolean, default=True)
|
2011-07-23 18:45:40 +02:00
|
|
|
|
2011-08-10 17:49:35 +02:00
|
|
|
def has_bills(self):
|
2011-10-18 17:45:24 +02:00
|
|
|
"""return if the user do have bills or not"""
|
|
|
|
bills_as_ower_number = db.session.query(billowers)\
|
2012-01-28 12:40:10 +01:00
|
|
|
.filter(billowers.columns.get("person_id") == self.id)\
|
2011-08-09 19:34:46 +02:00
|
|
|
.count()
|
|
|
|
return bills_as_ower_number != 0 or len(self.bills) != 0
|
|
|
|
|
2011-07-31 00:41:28 +02:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
def __repr__(self):
|
|
|
|
return "<Person %s for project %s>" % (self.name, self.project.name)
|
|
|
|
|
|
|
|
# We need to manually define a join table for m2m relations
|
|
|
|
billowers = db.Table('billowers',
|
|
|
|
db.Column('bill_id', db.Integer, db.ForeignKey('bill.id')),
|
|
|
|
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
|
|
|
|
)
|
|
|
|
|
2012-03-06 20:37:32 +01:00
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
class Bill(db.Model):
|
2011-09-13 18:15:07 +02:00
|
|
|
|
2011-09-13 22:58:53 +02:00
|
|
|
class BillQuery(BaseQuery):
|
2011-09-13 18:15:07 +02:00
|
|
|
|
|
|
|
def get(self, project, id):
|
|
|
|
try:
|
|
|
|
return self.join(Person, Project)\
|
|
|
|
.filter(Bill.payer_id == Person.id)\
|
|
|
|
.filter(Person.project_id == Project.id)\
|
|
|
|
.filter(Project.id == project.id)\
|
|
|
|
.filter(Bill.id == id).one()
|
|
|
|
except orm.exc.NoResultFound:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def delete(self, project, id):
|
|
|
|
bill = self.get(project, id)
|
|
|
|
if bill:
|
|
|
|
db.session.delete(bill)
|
|
|
|
return bill
|
|
|
|
|
|
|
|
query_class = BillQuery
|
|
|
|
|
|
|
|
_to_serialize = ("id", "payer_id", "owers", "amount", "date", "what")
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
|
|
|
|
payer_id = db.Column(db.Integer, db.ForeignKey("person.id"))
|
|
|
|
owers = db.relationship(Person, secondary=billowers)
|
|
|
|
|
|
|
|
amount = db.Column(db.Float)
|
|
|
|
date = db.Column(db.Date, default=datetime.now)
|
|
|
|
what = db.Column(db.UnicodeText)
|
|
|
|
|
2011-09-09 19:14:19 +02:00
|
|
|
archive = db.Column(db.Integer, db.ForeignKey("archive.id"))
|
|
|
|
|
2011-07-23 18:45:40 +02:00
|
|
|
def pay_each(self):
|
2015-08-20 11:11:09 +02:00
|
|
|
"""Compute what each share has to pay"""
|
2013-10-12 17:28:15 +02:00
|
|
|
if self.owers:
|
2015-08-20 10:33:43 +02:00
|
|
|
# FIXME: SQL might dot that more efficiently
|
|
|
|
return self.amount / sum(i.weight for i in self.owers)
|
2013-10-12 17:28:15 +02:00
|
|
|
else:
|
|
|
|
return 0
|
2011-07-23 18:45:40 +02:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<Bill of %s from %s for %s>" % (self.amount,
|
|
|
|
self.payer, ", ".join([o.name for o in self.owers]))
|
|
|
|
|
2012-03-06 20:37:32 +01:00
|
|
|
|
2011-09-09 19:14:19 +02:00
|
|
|
class Archive(db.Model):
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
2015-05-01 18:28:40 +02:00
|
|
|
project_id = db.Column(db.String(64), db.ForeignKey("project.id"))
|
2011-09-09 19:14:19 +02:00
|
|
|
name = db.Column(db.UnicodeText)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def start_date(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@property
|
|
|
|
def end_date(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return "<Archive>"
|