diff --git a/tests/test_reports_query.py b/tests/test_reports_query.py index 492069b26138c32a63b056ada4d017e23bb8e339..8f49478a671fb6e69952b4e3dcb889582afb3ee0 100644 --- a/tests/test_reports_query.py +++ b/tests/test_reports_query.py @@ -21,6 +21,7 @@ import pytest from . import testutil from beancount.core import data as bc_data +from beancount.query import query_parser as bc_query_parser from conservancy_beancount.books import FiscalYear from conservancy_beancount.reports import query as qmod from conservancy_beancount import rtutil @@ -38,6 +39,10 @@ class MockRewriteRuleset: yield post._replace(units=testutil.Amount(number, currency)) +@pytest.fixture(scope='module') +def qparser(): + return bc_query_parser.Parser() + @pytest.fixture(scope='module') def rt(): return rtutil.RT(testutil.RTClient()) @@ -130,40 +135,47 @@ def test_rewrite_query(end_index): assert expected.issubset(actual) assert frozenset(accounts).difference(expected).isdisjoint(actual) -def test_ods_amount_formatting(): +def test_ods_amount_formatting(qparser): + statement = qparser.parse('SELECT UNITS(position)') row_types = [('amount', bc_data.Amount)] row_source = [(testutil.Amount(12),), (testutil.Amount(1480, 'JPY'),)] ods = qmod.QueryODS() - ods.write_query(row_types, row_source) + ods.write_query(statement, row_types, row_source) actual = testutil.ODSCell.from_sheet(ods.document.spreadsheet.firstChild) assert next(actual)[0].text == 'Amount' assert next(actual)[0].text == '$12.00' assert next(actual)[0].text == '¥1,480' assert next(actual, None) is None -def test_ods_datetime_formatting(): +def test_ods_datetime_formatting(qparser): + statement = qparser.parse('SELECT date') row_types = [('date', datetime.date)] row_source = [(testutil.PAST_DATE,), (testutil.FUTURE_DATE,)] ods = qmod.QueryODS() - ods.write_query(row_types, row_source) + ods.write_query(statement, row_types, row_source) actual = testutil.ODSCell.from_sheet(ods.document.spreadsheet.firstChild) assert next(actual)[0].text == 'Date' assert next(actual)[0].text == testutil.PAST_DATE.isoformat() assert next(actual)[0].text == testutil.FUTURE_DATE.isoformat() assert next(actual, None) is None -@pytest.mark.parametrize('meta_key,header_text', [ - ('check', 'Check'), - ('purchase-order', 'Purchase Order'), - ('rt-id', 'Ticket'), +@pytest.mark.parametrize('meta_key,meta_func', [ + ('check', 'ANY_META'), + ('purchase-order', 'META'), + ('rt-id', 'META_DOCS'), ]) -def test_ods_link_formatting(rt, meta_key, header_text): - row_types = [(meta_key.replace('-', '_'), object)] - row_source = [('rt:1/5',), ('rt:3 Checks/9.pdf',)] +def test_ods_link_formatting(qparser, rt, meta_key, meta_func): + meta_func_returns_list = meta_func == 'META_DOCS' + statement = qparser.parse(f'SELECT {meta_func}({meta_key!r}) AS docs') + row_types = [('docs', list if meta_func_returns_list else str)] + row_source = [ + (s.split() if meta_func_returns_list else s,) + for s in ['rt:1/5', 'rt:3 Checks/9.pdf'] + ] ods = qmod.QueryODS(rt) - ods.write_query(row_types, row_source) + ods.write_query(statement, row_types, row_source) rows = iter(ods.document.spreadsheet.firstChild.getElementsByType(odf.table.TableRow)) - assert next(rows).text == header_text + assert next(rows).text == 'Docs' actual = iter( [link.text for link in row.getElementsByType(odf.text.A)] for row in rows @@ -172,50 +184,54 @@ def test_ods_link_formatting(rt, meta_key, header_text): assert next(actual) == ['rt:3', '9.pdf'] assert next(actual, None) is None -def test_ods_meta_formatting(): - row_types = [('metadata', object)] +def test_ods_meta_formatting(qparser): + statement = qparser.parse('SELECT ANY_META("entity") AS entity') + row_types = [('entity', object)] row_source = [(testutil.Amount(14),), (None,), ('foo bar',)] ods = qmod.QueryODS() - ods.write_query(row_types, row_source) + ods.write_query(statement, row_types, row_source) actual = testutil.ODSCell.from_sheet(ods.document.spreadsheet.firstChild) - assert next(actual)[0].text == 'Metadata' + assert next(actual)[0].text == 'Entity' assert next(actual)[0].text == '$14.00' assert next(actual)[0].text == '' assert next(actual)[0].text == 'foo bar' assert next(actual, None) is None -def test_ods_multicolumn_write(rt): - row_types = [('date', datetime.date), ('rt-id', object), ('desc', str)] +def test_ods_multicolumn_write(qparser, rt): + statement = qparser.parse( + 'SELECT MIN(date) AS date, SET(META_DOCS("rt-id")) AS tix, STR_META("entity") AS entity', + ) + row_types = [('date', datetime.date), ('tix', set), ('entity', str)] row_source = [ - (testutil.PAST_DATE, 'rt:1', 'aaa'), - (testutil.FY_START_DATE, 'rt:2', 'bbb'), - (testutil.FUTURE_DATE, 'rt:3', 'ccc'), + (testutil.PAST_DATE, {'rt:1'}, 'AA'), + (testutil.FY_START_DATE, {'rt:2'}, 'BB'), + (testutil.FUTURE_DATE, {'rt:3', 'rt:4'}, 'CC'), ] ods = qmod.QueryODS(rt) - ods.write_query(row_types, row_source) + ods.write_query(statement, list(row_types), list(row_source)) actual = iter( cell.text for row in testutil.ODSCell.from_sheet(ods.document.spreadsheet.firstChild) for cell in row ) - assert next(actual) == 'Date' - assert next(actual) == 'Ticket' - assert next(actual) == 'Desc' + for expected, _ in row_types: + assert next(actual) == expected.title() assert next(actual) == testutil.PAST_DATE.isoformat() assert next(actual) == 'rt:1' - assert next(actual) == 'aaa' + assert next(actual) == 'AA' assert next(actual) == testutil.FY_START_DATE.isoformat() assert next(actual) == 'rt:2' - assert next(actual) == 'bbb' + assert next(actual) == 'BB' assert next(actual) == testutil.FUTURE_DATE.isoformat() - assert next(actual) == 'rt:3' - assert next(actual) == 'ccc' + assert frozenset(next(actual).split('\0')) == row_source[-1][1] + assert next(actual) == 'CC' assert next(actual, None) is None -def test_ods_is_empty(): +def test_ods_is_empty(qparser): + statement = qparser.parse('SELECT * WHERE date < 1900-01-01') ods = qmod.QueryODS() assert ods.is_empty() - ods.write_query([], []) + ods.write_query(statement, [], []) assert not ods.is_empty() @pytest.mark.parametrize('fy,account,amt_prefix', [ @@ -232,8 +248,9 @@ def test_ods_output(fy, account, amt_prefix): ] returncode, stdout, stderr = pipe_main(arglist, config, io.BytesIO) assert returncode == 0 - stdout.seek(0) - ods_doc = odf.opendocument.load(stdout) + with stdout: + stdout.seek(0) + ods_doc = odf.opendocument.load(stdout) rows = iter(ods_doc.spreadsheet.firstChild.getElementsByType(odf.table.TableRow)) next(rows) # Skip header row amt_pattern = rf'^{re.escape(amt_prefix)}\d' @@ -243,3 +260,30 @@ def test_ods_output(fy, account, amt_prefix): assert narration.text.startswith(f'{fy} ') assert re.match(amt_pattern, amount.text) assert count + +def test_ods_aggregate_output(): + books_path = testutil.test_path(f'books/books/2020.beancount') + config = testutil.TestConfig(books_path=books_path) + arglist = [ + '-O', '-', + '-f', 'ods', + 'SELECT account, SET(narration), SUM(UNITS(position))', + 'WHERE date >= 2020-04-01 AND date <= 2020-04-02', + 'GROUP BY account ORDER BY account ASC', + ] + returncode, stdout, stderr = pipe_main(arglist, config, io.BytesIO) + assert returncode == 0 + with stdout: + stdout.seek(0) + ods_doc = odf.opendocument.load(stdout) + rows = iter(ods_doc.spreadsheet.firstChild.getElementsByType(odf.table.TableRow)) + next(rows) # Skip header row + actual = {} + for row in rows: + acct, descs, balance = row.childNodes + actual[acct.text] = (frozenset(descs.text.split('\0')), balance.text) + in_desc = {'2020 donation'} + ex_desc = {'2020 bank maintenance fee'} + assert actual['Income:Donations'] == (in_desc, '$20.20') + assert actual['Expenses:BankingFees'] == (ex_desc, '$1.00') + assert actual['Assets:Checking'] == (in_desc | ex_desc, '($21.20)')