Issue #184: Inventory averaging with can results in division by zero (blais/beancount)

14 views
Skip to first unread message

Stephan Müller

unread,
Aug 3, 2017, 1:01:07 PM8/3/17
to bean...@googlegroups.com
New issue 184: Inventory averaging with can results in division by zero
https://bitbucket.org/blais/beancount/issues/184/inventory-averaging-with-can-results-in

Stephan Müller:

Not 100% sure whether this is a bug (or just an incoherent inventory), but in my mind, averaging should still work even if the inventory is atypical. Alternatively, one could assert that `total_units != ZERO` and show a meaningful message otherwise.

With this preamble: if there are offsetting amounts of the same currency with different costs (eg a different date), the division in `cost = Cost(total_cost / total_units, cost_currency, None, None)` in the method [`Inventory.average`](https://bitbucket.org/blais/beancount/src/cbb3f348ec5e23f4d224f2b2f7a5811958d87873/beancount/core/inventory.py?at=default&fileviewer=file-view-default#inventory.py-302) (module `beancount.core.inventory`) results in a division by zero.

The following extension to [`TestInventory.test_average`](https://bitbucket.org/blais/beancount/src/cbb3f348ec5e23f4d224f2b2f7a5811958d87873/beancount/core/inventory_test.py?at=default&fileviewer=file-view-default#inventory_test.py-278) shows the problem (last test fails because the +/- 2 HOOL have different costs, so don't offset in the inventory but the overall position is 0):

def test_average(self):
# Identity, no aggregation.
inv = I('40.50 JPY, 40.51 USD {1.01 CAD}, 40.52 CAD')
self.assertEqual(inv.average(), inv)

# Identity, no aggregation, with a mix of lots at cost and without
# cost.
inv = I('40 USD {1.01 CAD}, 40 USD')
self.assertEqual(inv.average(), inv)

# Aggregation.
inv = I('40 USD {1.01 CAD}, 40 USD {1.02 CAD}')
self.assertEqual(inv.average(), I('80.00 USD {1.015 CAD}'))

# Aggregation, more units.
inv = I('2 HOOL {500 USD}, 3 HOOL {520 USD}, 4 HOOL {530 USD}')
self.assertEqual(inv.average(), I('9 HOOL {520 USD}'))

# ADDED--------------------------------------------------------
# Average on zero amount, same costs
inv = I('2 HOOL {500 USD}')
inv.add_amount(A('-2 HOOL'), Cost(D('500'), 'USD', None, None))
self.assertEqual(inv.average(), I(''))

# Average on zero amount, different costs
inv = I('2 HOOL {500 USD}')
inv.add_amount(A('-2 HOOL'),
Cost(D('500'), 'USD', datetime.date(2000, 1, 1), None))
self.assertEqual(inv.average(), I(''))

A simple fix would be to add the line `if total_units == ZERO: continue` in the method `Inventory.average` as per below:

def average(self):
"""Average all lots of the same currency together.

Returns:
An instance of Inventory.
"""
groups = collections.defaultdict(list)
for position in self:
key = (position.units.currency,
position.cost.currency if position.cost else None)
groups[key].append(position)

average_inventory = Inventory()
for (currency, cost_currency), positions in groups.items():
total_units = sum(position.units.number
for position in positions)
if total_units == ZERO: continue # ADDED-----------------------------------
units_amount = Amount(total_units, currency)

if cost_currency:
total_cost = sum(convert.get_cost(position).number
for position in positions)
cost = Cost(total_cost / total_units,
cost_currency, None, None)
else:
cost = None

average_inventory.add_amount(units_amount, cost)

return average_inventory


Reply all
Reply to author
Forward
0 new messages