diff --git a/conservancy_beancount/reports/fund.py b/conservancy_beancount/reports/fund.py index db2ef02..e26af0e 100644 --- a/conservancy_beancount/reports/fund.py +++ b/conservancy_beancount/reports/fund.py @@ -55,6 +55,7 @@ from typing import ( Dict, Iterable, Iterator, + List, Mapping, Optional, Sequence, @@ -65,6 +66,7 @@ from ..beancount_types import ( MetaValue, ) +from decimal import Decimal from pathlib import Path import odf.table # type:ignore[import] @@ -135,8 +137,26 @@ class ODSReport(core.BaseODS[FundPosts, None]): def end_spreadsheet(self) -> None: sheet = self.copy_element(self.sheet) sheet.setAttribute('name', 'Fund Report') - for row in sheet.childNodes: + row_qname = sheet.lastChild.qname + skip_rows: List[int] = [] + report_threshold = Decimal('.5') + for index, row in enumerate(sheet.childNodes): row.childNodes = row.childNodes[:6] + # Filter out fund rows that don't have anything reportable. + if (row.qname == row_qname + # len(childNodes) makes sure this isn't a header/spacer row. + and len(row.childNodes) == 6 + and not any( + # Multiple childNodes means it's a multi-currency balance. + len(cell.childNodes) > 1 + # Some column has to round up to 1 to be reportable. + or (cell.getAttribute('valuetype') == 'currency' + and Decimal(cell.getAttribute('value')) >= report_threshold) + for cell in row.childNodes + )): + skip_rows.append(index) + for index in reversed(skip_rows): + del sheet.childNodes[index] self.lock_first_row(sheet) self.document.spreadsheet.insertBefore(sheet, self.sheet) diff --git a/setup.py b/setup.py index 663bfc0..00c177e 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='conservancy_beancount', description="Plugin, library, and reports for reading Conservancy's books", - version='1.5.2', + version='1.5.3', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', diff --git a/tests/books/fund.beancount b/tests/books/fund.beancount index 24dc807..68f7068 100644 --- a/tests/books/fund.beancount +++ b/tests/books/fund.beancount @@ -56,6 +56,11 @@ option "inferred_tolerance_default" "USD:0.01" Expenses:Other 20 USD Assets:Checking -20 USD +2018-12-03 * "Delta income" + project: "Delta" + Income:Other -0.40 USD + Assets:Checking 0.40 USD + 2019-03-03 * "Conservancy receivable paid" project: "Conservancy" Assets:Receivable:Accounts -32 EUR {1.25 USD} @ 1.5 USD @@ -103,3 +108,8 @@ option "inferred_tolerance_default" "USD:0.01" project: "Bravo" Income:Other -200 USD Assets:Checking 200 USD + +2019-12-03 * "Delta income" + project: "Delta" + Income:Other -4.60 USD + Assets:Checking 4.60 USD diff --git a/tests/test_opening_balances.py b/tests/test_opening_balances.py index 8e7b66f..a13daf2 100644 --- a/tests/test_opening_balances.py +++ b/tests/test_opening_balances.py @@ -124,12 +124,13 @@ def test_2019_opening(arg): assert not errors.getvalue() assert retcode == 0 assert list(FlatPosting.from_output(output)) == [ - FlatPosting.make(A_CHECKING, 10050), + FlatPosting.make(A_CHECKING, '10050.40'), FlatPosting.make(A_PREPAID, 20, project='Alpha'), FlatPosting.make(A_RECEIVABLE, 32, 'EUR', '1.25', '2018-03-03', 'Conservancy'), FlatPosting.make(A_RESTRICTED, -3060, project='Alpha'), FlatPosting.make(A_RESTRICTED, -1980, project='Bravo'), FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'), + FlatPosting.make(A_RESTRICTED, '-.40', project='Delta'), FlatPosting.make(A_UNRESTRICTED, -4036, project='Conservancy'), FlatPosting.make(A_PAYABLE, -4, project='Conservancy'), FlatPosting.make(A_UNEARNED, -30, project='Alpha'), @@ -141,10 +142,11 @@ def test_2020_opening(arg): assert not errors.getvalue() assert retcode == 0 assert list(FlatPosting.from_output(output)) == [ - FlatPosting.make(A_CHECKING, 10276), + FlatPosting.make(A_CHECKING, 10281), FlatPosting.make(A_EUR, 32, 'EUR', '1.5', '2019-03-03'), FlatPosting.make(A_RESTRICTED, -3064, project='Alpha'), FlatPosting.make(A_RESTRICTED, -2180, project='Bravo'), FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'), + FlatPosting.make(A_RESTRICTED, -5, project='Delta'), FlatPosting.make(A_UNRESTRICTED, -4080, project='Conservancy'), ] diff --git a/tests/test_reports_fund.py b/tests/test_reports_fund.py index 079f78d..67348bc 100644 --- a/tests/test_reports_fund.py +++ b/tests/test_reports_fund.py @@ -47,6 +47,7 @@ OPENING_BALANCES = { 'Bravo': 2000, 'Charlie': 1000, 'Conservancy': 4000, + 'Delta': 0, } BALANCES_BY_YEAR = { @@ -80,6 +81,12 @@ BALANCES_BY_YEAR = { ('Bravo', 2019): [ ('Income:Other', 200), ], + ('Delta', 2018): [ + ('Income:Other', Decimal('.40')), + ], + ('Delta', 2019): [ + ('Income:Other', Decimal('4.60')), + ], } @pytest.fixture @@ -156,31 +163,38 @@ def check_cell_balance(cell, balance): assert not cell.value def check_ods_sheet(sheet, account_balances, *, full): - account_bals = account_balances.copy() - unrestricted = account_bals.pop('Conservancy') if full: - account_bals['Unrestricted'] = unrestricted - for row in sheet.getElementsByType(odf.table.TableRow): + account_bals = account_balances.copy() + account_bals['Unrestricted'] = account_bals.pop('Conservancy') + else: + account_bals = { + key: balances + for key, balances in account_balances.items() + if key != 'Conservancy' and any(v >= .5 for v in balances.values()) + } + for row in itertools.islice(sheet.getElementsByType(odf.table.TableRow), 4, None): cells = iter(testutil.ODSCell.from_row(row)) try: fund = next(cells).firstChild.text except (AttributeError, StopIteration): - fund = None - if fund in account_bals: + continue + try: balances = account_bals.pop(fund) - check_cell_balance(next(cells), balances['opening']) - check_cell_balance(next(cells), balances['Income']) - check_cell_balance(next(cells), -balances['Expenses']) - check_cell_balance(next(cells), balances['Equity:Realized']) - check_cell_balance(next(cells), sum(balances[key] for key in [ - 'opening', 'Income', 'Expenses', 'Equity:Realized', - ])) - if full: - check_cell_balance(next(cells), balances['Assets:Receivable']) - check_cell_balance(next(cells), balances['Assets:Prepaid']) - check_cell_balance(next(cells), balances['Liabilities:Payable']) - check_cell_balance(next(cells), balances['Liabilities']) - assert next(cells, None) is None + except KeyError: + pytest.fail(f"report included unexpected fund {fund}") + check_cell_balance(next(cells), balances['opening']) + check_cell_balance(next(cells), balances['Income']) + check_cell_balance(next(cells), -balances['Expenses']) + check_cell_balance(next(cells), balances['Equity:Realized']) + check_cell_balance(next(cells), sum(balances[key] for key in [ + 'opening', 'Income', 'Expenses', 'Equity:Realized', + ])) + if full: + check_cell_balance(next(cells), balances['Assets:Receivable']) + check_cell_balance(next(cells), balances['Assets:Prepaid']) + check_cell_balance(next(cells), balances['Liabilities:Payable']) + check_cell_balance(next(cells), balances['Liabilities']) + assert next(cells, None) is None assert not account_bals, "did not see all funds in report" def check_ods_report(ods, start_date, stop_date):