fund: Omit unreportable rows from the fund report sheet.
This commit is contained in:
parent
40573cb6dc
commit
138928eebf
5 changed files with 69 additions and 23 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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+',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,31 +163,38 @@ 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)
|
||||||
check_cell_balance(next(cells), balances['opening'])
|
except KeyError:
|
||||||
check_cell_balance(next(cells), balances['Income'])
|
pytest.fail(f"report included unexpected fund {fund}")
|
||||||
check_cell_balance(next(cells), -balances['Expenses'])
|
check_cell_balance(next(cells), balances['opening'])
|
||||||
check_cell_balance(next(cells), balances['Equity:Realized'])
|
check_cell_balance(next(cells), balances['Income'])
|
||||||
check_cell_balance(next(cells), sum(balances[key] for key in [
|
check_cell_balance(next(cells), -balances['Expenses'])
|
||||||
'opening', 'Income', 'Expenses', 'Equity:Realized',
|
check_cell_balance(next(cells), balances['Equity:Realized'])
|
||||||
]))
|
check_cell_balance(next(cells), sum(balances[key] for key in [
|
||||||
if full:
|
'opening', 'Income', 'Expenses', 'Equity:Realized',
|
||||||
check_cell_balance(next(cells), balances['Assets:Receivable'])
|
]))
|
||||||
check_cell_balance(next(cells), balances['Assets:Prepaid'])
|
if full:
|
||||||
check_cell_balance(next(cells), balances['Liabilities:Payable'])
|
check_cell_balance(next(cells), balances['Assets:Receivable'])
|
||||||
check_cell_balance(next(cells), balances['Liabilities'])
|
check_cell_balance(next(cells), balances['Assets:Prepaid'])
|
||||||
assert next(cells, None) is None
|
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"
|
assert not account_bals, "did not see all funds in report"
|
||||||
|
|
||||||
def check_ods_report(ods, start_date, stop_date):
|
def check_ods_report(ods, start_date, stop_date):
|
||||||
|
|
Loading…
Reference in a new issue