fund: Omit unreportable rows from the fund report sheet.

This commit is contained in:
Brett Smith 2020-06-27 21:36:52 -04:00
parent 40573cb6dc
commit 138928eebf
5 changed files with 69 additions and 23 deletions

View file

@ -55,6 +55,7 @@ from typing import (
Dict, Dict,
Iterable, Iterable,
Iterator, Iterator,
List,
Mapping, Mapping,
Optional, Optional,
Sequence, Sequence,
@ -65,6 +66,7 @@ from ..beancount_types import (
MetaValue, MetaValue,
) )
from decimal import Decimal
from pathlib import Path from pathlib import Path
import odf.table # type:ignore[import] import odf.table # type:ignore[import]
@ -135,8 +137,26 @@ class ODSReport(core.BaseODS[FundPosts, None]):
def end_spreadsheet(self) -> None: def end_spreadsheet(self) -> None:
sheet = self.copy_element(self.sheet) sheet = self.copy_element(self.sheet)
sheet.setAttribute('name', 'Fund Report') 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] 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.lock_first_row(sheet)
self.document.spreadsheet.insertBefore(sheet, self.sheet) self.document.spreadsheet.insertBefore(sheet, self.sheet)

View file

@ -5,7 +5,7 @@ from setuptools import setup
setup( setup(
name='conservancy_beancount', name='conservancy_beancount',
description="Plugin, library, and reports for reading Conservancy's books", description="Plugin, library, and reports for reading Conservancy's books",
version='1.5.2', version='1.5.3',
author='Software Freedom Conservancy', author='Software Freedom Conservancy',
author_email='info@sfconservancy.org', author_email='info@sfconservancy.org',
license='GNU AGPLv3+', license='GNU AGPLv3+',

View file

@ -56,6 +56,11 @@ option "inferred_tolerance_default" "USD:0.01"
Expenses:Other 20 USD Expenses:Other 20 USD
Assets:Checking -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" 2019-03-03 * "Conservancy receivable paid"
project: "Conservancy" project: "Conservancy"
Assets:Receivable:Accounts -32 EUR {1.25 USD} @ 1.5 USD Assets:Receivable:Accounts -32 EUR {1.25 USD} @ 1.5 USD
@ -103,3 +108,8 @@ option "inferred_tolerance_default" "USD:0.01"
project: "Bravo" project: "Bravo"
Income:Other -200 USD Income:Other -200 USD
Assets:Checking 200 USD Assets:Checking 200 USD
2019-12-03 * "Delta income"
project: "Delta"
Income:Other -4.60 USD
Assets:Checking 4.60 USD

View file

@ -124,12 +124,13 @@ def test_2019_opening(arg):
assert not errors.getvalue() assert not errors.getvalue()
assert retcode == 0 assert retcode == 0
assert list(FlatPosting.from_output(output)) == [ 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_PREPAID, 20, project='Alpha'),
FlatPosting.make(A_RECEIVABLE, 32, 'EUR', '1.25', '2018-03-03', 'Conservancy'), FlatPosting.make(A_RECEIVABLE, 32, 'EUR', '1.25', '2018-03-03', 'Conservancy'),
FlatPosting.make(A_RESTRICTED, -3060, project='Alpha'), FlatPosting.make(A_RESTRICTED, -3060, project='Alpha'),
FlatPosting.make(A_RESTRICTED, -1980, project='Bravo'), FlatPosting.make(A_RESTRICTED, -1980, project='Bravo'),
FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'), FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'),
FlatPosting.make(A_RESTRICTED, '-.40', project='Delta'),
FlatPosting.make(A_UNRESTRICTED, -4036, project='Conservancy'), FlatPosting.make(A_UNRESTRICTED, -4036, project='Conservancy'),
FlatPosting.make(A_PAYABLE, -4, project='Conservancy'), FlatPosting.make(A_PAYABLE, -4, project='Conservancy'),
FlatPosting.make(A_UNEARNED, -30, project='Alpha'), FlatPosting.make(A_UNEARNED, -30, project='Alpha'),
@ -141,10 +142,11 @@ def test_2020_opening(arg):
assert not errors.getvalue() assert not errors.getvalue()
assert retcode == 0 assert retcode == 0
assert list(FlatPosting.from_output(output)) == [ 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_EUR, 32, 'EUR', '1.5', '2019-03-03'),
FlatPosting.make(A_RESTRICTED, -3064, project='Alpha'), FlatPosting.make(A_RESTRICTED, -3064, project='Alpha'),
FlatPosting.make(A_RESTRICTED, -2180, project='Bravo'), FlatPosting.make(A_RESTRICTED, -2180, project='Bravo'),
FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'), FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'),
FlatPosting.make(A_RESTRICTED, -5, project='Delta'),
FlatPosting.make(A_UNRESTRICTED, -4080, project='Conservancy'), FlatPosting.make(A_UNRESTRICTED, -4080, project='Conservancy'),
] ]

View file

@ -47,6 +47,7 @@ OPENING_BALANCES = {
'Bravo': 2000, 'Bravo': 2000,
'Charlie': 1000, 'Charlie': 1000,
'Conservancy': 4000, 'Conservancy': 4000,
'Delta': 0,
} }
BALANCES_BY_YEAR = { BALANCES_BY_YEAR = {
@ -80,6 +81,12 @@ BALANCES_BY_YEAR = {
('Bravo', 2019): [ ('Bravo', 2019): [
('Income:Other', 200), ('Income:Other', 200),
], ],
('Delta', 2018): [
('Income:Other', Decimal('.40')),
],
('Delta', 2019): [
('Income:Other', Decimal('4.60')),
],
} }
@pytest.fixture @pytest.fixture
@ -156,18 +163,25 @@ def check_cell_balance(cell, balance):
assert not cell.value assert not cell.value
def check_ods_sheet(sheet, account_balances, *, full): def check_ods_sheet(sheet, account_balances, *, full):
account_bals = account_balances.copy()
unrestricted = account_bals.pop('Conservancy')
if full: if full:
account_bals['Unrestricted'] = unrestricted account_bals = account_balances.copy()
for row in sheet.getElementsByType(odf.table.TableRow): 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)) cells = iter(testutil.ODSCell.from_row(row))
try: try:
fund = next(cells).firstChild.text fund = next(cells).firstChild.text
except (AttributeError, StopIteration): except (AttributeError, StopIteration):
fund = None continue
if fund in account_bals: try:
balances = account_bals.pop(fund) balances = account_bals.pop(fund)
except KeyError:
pytest.fail(f"report included unexpected fund {fund}")
check_cell_balance(next(cells), balances['opening']) check_cell_balance(next(cells), balances['opening'])
check_cell_balance(next(cells), balances['Income']) check_cell_balance(next(cells), balances['Income'])
check_cell_balance(next(cells), -balances['Expenses']) check_cell_balance(next(cells), -balances['Expenses'])