Skip to content

Commit

Permalink
issue #117 - WIP further budget balancing implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jantman committed Oct 19, 2017
1 parent a145ba6 commit dcd9fcf
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 11 deletions.
77 changes: 75 additions & 2 deletions biweeklybudget/budget_balancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def plan(self):
"amount".
- "budgets" - is a dict describing the budgets before and after
balancing; keys are budget IDs and values are dicts with keys
"before" and "after", to values of the remaining amount.
"before" and "after", to values of the remaining amount, and "name"
- "standing_before" - float, beginning balance of standing budget
- "standing_after" - float, ending balance of standing budget
Expand Down Expand Up @@ -196,7 +196,10 @@ def plan(self):
if data['remaining'] == 0:
continue
to_balance[budg_id] = data['remaining']
result['budgets'][budg_id] = {'before': data['remaining']}
result['budgets'][budg_id] = {
'before': data['remaining'],
'name': self._budgets[budg_id].name
}
# @TODO - REMOVE - DEBUGGING
logger.debug('self._budgets=%s result=%s', self._budgets, result)
# @TODO - END DEBUGGING
Expand All @@ -216,6 +219,14 @@ def plan(self):
after, transfers, st_bal = self._do_plan_transfers(
to_balance, [], self._standing.current_balance
)
logger.debug(
'Before balancing with standing budget, standing balance=%s '
'transfers=%s, budget ending balances=%s', st_bal, transfers, after
)
# ok, now handle the standing budget stuff...
after, transfers, st_bal = self._do_plan_standing_txfr(
after, transfers, st_bal
)
result['transfers'] = transfers
logger.debug(
'Budget transfers: \n%s',
Expand All @@ -237,6 +248,68 @@ def plan(self):
)
return result

def _do_plan_standing_txfr(self, id_to_remain, transfers, standing_bal):
"""
Given a dictionary of budget IDs to remaining amounts, that have already
been balanced as much as possible using the remaining amounts of each
budget, use the Standing Budget to balance them all to zero.
The arguments of this function are the final return value from
:py:meth:`~._do_plan_transfers`.
Transfers is a list of lists, each inner list describing a budget
transfer and having 3 items: "from_id", "to_id" and "amount".
:param id_to_remain: Budget ID to remaining balance
:type id_to_remain: dict
:param transfers: list of transfer dicts for transfers made so far
:type transfers: list
:param standing_bal: balance of the standing budget
:type standing_bal: decimal.Decimal
:return: tuple of new id_to_remain, transfers, standing_bal
:rtype: tuple
"""
# id_to_remain must be either all >= 0 or all <= 0
if min(id_to_remain, key=id_to_remain.get) >= 0:
# all positive
logger.debug(
'Balancing periodic budgets with standing; overall periodic sum'
' is positive'
)
keyfunc = lambda: max(id_to_remain, key=id_to_remain.get)
else:
# all negative
logger.debug(
'Balancing periodic budgets with standing; overall periodic sum'
' is negative'
)
keyfunc = lambda: min(id_to_remain, key=id_to_remain.get)
while sum(id_to_remain.values()) != 0 and standing_bal > 0:
logger.debug(
'Looping to reconcile periodic with standing; standing bal=%s,'
' id_to_remain=%s, transfers=%s', standing_bal, id_to_remain,
transfers
)
k = keyfunc()
v = id_to_remain[k]
if v > 0:
transfers.append([k, self._standing.id, v])
standing_bal += v
id_to_remain[k] = 0
continue
# v < 0; transfer FROM standing TO periodic
if abs(v) < standing_bal:
# ok, we have enough to cover it
transfers.append([self._standing.id, k, abs(v)])
standing_bal += v
id_to_remain[k] = 0
continue
# we do NOT have enough in standing to cover it
transfers.append([self._standing.id, k, standing_bal])
id_to_remain[k] += standing_bal
standing_bal = 0
return id_to_remain, transfers, standing_bal

def _do_plan_transfers(self, id_to_remain, transfers, standing_bal):
"""
Given a dictionary of budget IDs to remaining amounts, figure out
Expand Down
6 changes: 3 additions & 3 deletions biweeklybudget/flaskapp/static/js/balance_budgets_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ function balanceBudgetsConfirmationDivForm(data) {
content += '<div class="panel panel-info">' + "\n";
content += '<div class="panel-heading">Budget Balances</div>' + "\n";
content += '<div class="table-responsive">' + "\n";
content += '<table class="table table-bordered">' + "\n";
content += '<table class="table table-bordered" id="budg_bal_modal_budgets">' + "\n";
content += "<thead><tr><th>Budget</th><th>Before</th><th>After</th></tr></thead>\n";
content += "<tbody>\n";
Object.keys(data.budgets).forEach(function (budg_id) {
content += '<tr>';
content += '<td>' + budg_id + '</td>';
content += '<td>' + data.budgets[budg_id]['name'] + ' (' + budg_id + ')</td>';
content += '<td>' + fmt_currency(data.budgets[budg_id]['before']) + '</td>';
content += '<td>' + fmt_currency(data.budgets[budg_id]['after']) + '</td>';
content += "</tr>\n";
Expand All @@ -146,7 +146,7 @@ function balanceBudgetsConfirmationDivForm(data) {
content += '<div class="panel panel-info" style="padding-top: 1em;">' + "\n";
content += '<div class="panel-heading">Transfers</div>' + "\n";
content += '<div class="table-responsive">' + "\n";
content += '<table class="table table-bordered">' + "\n";
content += '<table class="table table-bordered" id="budg_bal_modal_transfers">' + "\n";
content += "<thead><tr><th>Amount</th><th>From</th><th>To</th></tr></thead>\n";
content += "<tbody>\n";
for (var txfr in data.transfers) {
Expand Down
28 changes: 28 additions & 0 deletions biweeklybudget/tests/acceptance/flaskapp/views/test_payperiods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,34 @@ def test_10_do_balance(self, base_url, selenium, testdb):
budg_sel.select_by_value('4')
selenium.find_element_by_id('modalSaveButton').click()
self.wait_for_jquery_done(selenium)
modal, title, body = self.get_modal_parts(selenium)
self.assert_modal_displayed(modal, title, body)
assert title.text == 'Confirm Balance Budgets (%s)' % p_date_str
table = selenium.find_element_by_id('budg_bal_modal_budgets')
elems = self.tbody2elemlist(table)
htmls = []
for row in elems:
htmls.append(
[x.get_attribute('innerHTML') for x in row]
)
assert htmls == [
['Periodic1 (1)', '-$75.00', '$0.00'],
['Periodic8 (8)', '$350.00', '$0.00'],
['Periodic9 (9)', '-$25.00', '$0.00'],
['<strong>Standing1 (4)</strong>', '$1,284.23', '$1,384.23']
]
table = selenium.find_element_by_id('budg_bal_modal_transfers')
elems = self.tbody2elemlist(table)
htmls = []
for row in elems:
htmls.append(
[x.get_attribute('innerHTML') for x in row]
)
assert htmls == [
['$75.00', 'Periodic8 (8)', 'Periodic1 (1)'],
['$25.00', 'Periodic8 (8)', 'Periodic9 (9)'],
['$250.00', 'Periodic8 (8)', 'Standing1 (4)']
]


@pytest.mark.acceptance
Expand Down
Loading

0 comments on commit dcd9fcf

Please sign in to comment.