def setUp(self): self.u = model.User(u'chaz', u'Charles Root', False) self.e = model.Expenditure(self.u, Currency('444.88'), u'chaz buys lunch') meta.Session.add(self.u) meta.Session.add(self.e) meta.Session.commit()
def test_simpleSplit(self): """ Test simply splitting a $100 expenditure amongst 4 people """ createUsers(4) e = model.Expenditure( meta.Session.query(model.User).first(), Currency("100")) meta.Session.add(e) e.even_split() meta.Session.commit() for s in meta.Session.query(model.Split).\ filter(model.Split.expenditure==e): self.assertEqual(s.share, Currency("25.00")) deleteExpenditures() deleteUsers()
def test_index_description_blank_oops(self): response = self.app.get(url_for(controller='spend', action='index')) # Test response... response.mustcontain('Add a New Expenditure') form = response.form user = meta.Session.query(model.User).\ filter_by(name=u'Charlie Root').one() form['spender_id'] = user.id form['amount'] = '74.04' # Make sure date is today. today = date.today() assert form['date'].value == today.strftime('%m/%d/%Y') form['description'] = '' for ii in range(4): if int(form['shares-%d.user_id' % ii].value) in (1, 4): form['shares-%d.amount' % ii] = '1' else: form['shares-%d.amount' % ii] = '2' response = form.submit() response.mustcontain('<!-- for: description -->', 'Please enter a value') form = response.form form['description'] = 'Another test' response = form.submit() response = response.follow() e = meta.Session.query(model.Expenditure).\ order_by(model.Expenditure.id.desc()).first() assert e.spender.name == u'Charlie Root' assert e.amount == 7404 assert e.date == today assert e.description == u'Another test' # Test the split. shares = dict(((sp.user_id, sp.share) for sp in e.splits)) assert shares[1] == Currency('12.34') assert shares[2] == Currency('24.68') assert shares[3] == Currency('24.68') assert shares[4] == Currency('12.34')
def createExpenditures(n=None): if n is None: n = random.randint(5, 20) users = meta.Session.query(bluechips.model.User).all() for i in xrange(n): e = bluechips.model.Expenditure(random.choice(users), Currency(random.randint(1000, 100000))) meta.Session.add(e) e.even_split() meta.Session.commit()
def __init__(self, spender=None, amount=Currency(0), description=u"", date=None): self.spender = spender self.amount = amount self.description = description if self.date == None: self.date = datetime.now()
def split(self, split_dict): """ Split up an expenditure. split_dict should be a dict mapping from bluechips.model:User objects to a decimal:Decimal object representing the percentage that user is responsible for. Percentages will be normalized to sum to 100%. If the split leaks or gains money due to rounding errors, the pennies will be randomly distributed to a subset of the users. I mean, come on. You're already living together. Are you really going to squabble over a few pennies? """ map(meta.Session.delete, meta.Session.query(Split).\ filter_by(expenditure_id=self.id)) total = sum(split_dict.itervalues()) for user, share in split_dict.items(): if share == 0: del split_dict[user] amounts_dict = dict() for user, share in split_dict.iteritems(): amounts_dict[user] = Currency((share * self.amount) / total) difference = self.amount - sum(amounts_dict.itervalues()) winners = random.sample(amounts_dict.keys(), abs(int(difference))) for winner in winners: amounts_dict[winner] += Currency( 1) if difference > 0 else Currency(-1) for user, share in amounts_dict.iteritems(): s = Split(self, user, share) meta.Session.add(s)
def test_uneven(self): """ Test that expenditures can be split non-evenly """ createUsers(2) users = meta.Session.query(model.User).all() e = model.Expenditure(users[0], Currency("100")) meta.Session.add(e) split_dict = {users[0]: Decimal("20"), users[1]: Decimal("80")} amount_dict = {users[0]: Currency("20"), users[1]: Currency("80")} e.split(split_dict) meta.Session.commit() for s in meta.Session.query(model.Split): self.assertEqual(s.share, amount_dict[s.user]) deleteExpenditures() deleteUsers()
def test_sevenPeople(self): """ Test that expenses are split as evenly as possible with lots of people """ createUsers(7) users = meta.Session.query(model.User).all() e = model.Expenditure(users[0], Currency("24.00")) meta.Session.add(e) e.even_split() meta.Session.commit() splits = meta.Session.query(model.Split).all() self.assertEqual(e.amount, sum(s.share for s in splits)) max_split = max(s.share for s in splits) min_split = min(s.share for s in splits) self.assertTrue(max_split - min_split <= Currency(1)) deleteExpenditures() deleteUsers()
def share_name(self): "Return the share name that matches the splitting" shares = dict((split.user, split.share) for split in self.splits if split.share != 0) if len(shares) == 1: user = shares.keys()[0] return 'For %s' % user.name for oname, oshares in share_dict.items(): ratio = float(self.amount) * 1. / sum(oshares.values()) difference = sum( abs(shares[u] - oshares.get(u.username, 0) * ratio) for u in shares) if difference <= Currency(len(shares)): return oname
def test_unevenBadTotal(self): """ Test that transactions get split up properly when the uneven split shares don't add to 100% """ createUsers(2) users = meta.Session.query(model.User).all() e = model.Expenditure(users[0], Currency("100.00")) meta.Session.add(e) split_dict = {users[0]: Decimal(10), users[1]: Decimal(15)} amount_dict = {users[0]: Currency("40"), users[1]: Currency("60")} e.split(split_dict) meta.Session.commit() for s in meta.Session.query(model.Split): self.assertEqual(s.share, amount_dict[s.user]) deleteExpenditures() deleteUsers()
def debts(): # In this scheme, negative numbers represent money the house owes # the user, and positive numbers represent money the user owes the # house users = meta.Session.query(model.User) debts_dict = dict((u, Currency(0)) for u in users) # First, credit everyone for expenditures they've made total_expenditures = meta.Session.query(model.Expenditure).\ add_column(sqlalchemy.func.sum(model.Expenditure.amount).label('total_spend')).\ group_by(model.Expenditure.spender_id) for expenditure, total_spend in total_expenditures: debts_dict[expenditure.spender] -= total_spend # Next, debit everyone for expenditures that they have an # investment in (i.e. splits) total_splits = meta.Session.query(model.Split).\ add_column(sqlalchemy.func.sum(model.Split.share).label('total_split')).\ group_by(model.Split.user_id) for split, total_cents in total_splits: debts_dict[split.user] += total_cents # Finally, move transfers around appropriately # # To keep this from getting to be expensive, have SQL sum up # transfers for us transfer_q = meta.Session.query(model.Transfer).\ add_column(sqlalchemy.func.sum(model.Transfer.amount).label('total_amount')) total_debits = transfer_q.group_by(model.Transfer.debtor_id) total_credits = transfer_q.group_by(model.Transfer.creditor_id) for transfer, total_amount in total_debits: debts_dict[transfer.debtor] -= total_amount for transfer, total_amount in total_credits: debts_dict[transfer.creditor] += total_amount return debts_dict
def test_negativeExpenditure(self): """ Test that negative expenditures get split correctly """ createUsers(2) users = meta.Session.query(model.User).all() e = model.Expenditure(users[0], Currency("100.00")) meta.Session.add(e) # Force a split that will result in needing to distribute # pennies split_dict = {users[0]: Decimal(1), users[1]: Decimal(2)} e.split(split_dict) meta.Session.commit() self.assertEqual(e.amount, sum(s.share for s in meta.Session.query(model.Split))) deleteExpenditures() deleteUsers()
def share(self, user): "Return the share corresponding to ``user``." shares = dict((split.user, split.share) for split in self.splits) return shares.get(user, Currency(0))
def test_split_small_negative(self): self._two_way_split_test(Currency('-0.01'), Currency('-0.01'), Currency('-0.00'))
def test_even_split(self): self.e.even_split() meta.Session.commit() for sp in self.e.splits: assert sp.share == Currency('111.22')
def setUp(self): self.u = model.User('chaz', u'Charles Root', False) self.e = model.Expenditure(self.u, Currency('12.34'), u'A test expenditure') self.sp = model.Split(self.e, self.u, Currency('5.55'))
def test_constructor(self): assert self.sp.expenditure == self.e assert self.sp.user == self.u assert self.sp.share == Currency('5.55')
def test_split_rounds_up(self): self._two_way_split_test(Currency('39.99'), Currency('19.99'), Currency('20.00'))
def test_split_rounds_down(self): self._two_way_split_test(Currency('40.01'), Currency('20.00'), Currency('20.01'))
def test_constructor(self): assert self.e.spender == self.u assert self.e.amount == Currency('444.88') assert self.e.description == u'chaz buys lunch'
def test_split_small(self): self._two_way_split_test(Currency('0.01'), Currency('0.00'), Currency('0.01'))