Changeset - 0caf78436fe9
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2020-06-17 19:32:08
brettcsmith@brettcsmith.org
accrual: Generate an aging report in more cases.

Default to generating an aging report unless the user searched for a
specific RT ticket or invoice.
2 files changed with 26 insertions and 24 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -6,8 +6,7 @@ Liabilities:Payable) for errors and metadata consistency, and reports any
 
problems on stderr. Then it writes a report about the status of those
 
accruals.
 

	
 
If you run it with no arguments, it will generate an aging report in ODS format
 
in the current directory.
 
If you run it with no arguments, it will generate an aging report in ODS format.
 

	
 
Otherwise, the typical way to run it is to pass an RT ticket number or
 
invoice link as an argument, to report about accruals that match those
...
 
@@ -38,17 +37,19 @@ You can pass any number of search terms. For example::
 
    accrual-report 1230 entity=Doe-Jane
 

	
 
accrual-report will automatically decide what kind of report to generate
 
from the search terms you provide and the results they return. If you pass
 
no search terms, it generates an aging report. If your search terms match a
 
single outstanding payable, it writes an outgoing approval report.
 
Otherwise, it writes a basic balance report. You can specify what report
 
from the search terms you provide and the results they return. If you
 
searched on an RT ticket or invoice that returned a single outstanding
 
payable, it writes an outgoing approval report. If you searched on RT ticket
 
or invoice that returned other results, it writes a balance
 
report. Otherwise, it writes an aging report. You can specify what report
 
type you want with the ``--report-type`` option::
 

	
 
    # Write an outgoing approval report for all outstanding accruals for
 
    # Write an outgoing approval report for all outstanding payables for
 
    # Jane Doe, even if there's more than one
 
    accrual-report --report-type outgoing entity=Doe-Jane
 
    # Write an aging report for a specific project
 
    accrual-report --report-type aging project=ProjectName
 
    # Write an aging report for a single RT invoice (this can be helpful when
 
    # one invoice covers multiple parties)
 
    accrual-report --report-type aging 12/345
 
"""
 
# Copyright © 2020  Brett Smith
 
#
...
 
@@ -627,7 +628,10 @@ metadata to match. A single ticket number is a shortcut for
 
`TIK/ATT` format, is a shortcut for `invoice=LINK`.
 
""")
 
    args = parser.parse_args(arglist)
 
    if args.report_type is None and not args.search_terms:
 
    if args.report_type is None and not any(
 
            term.meta_key == 'invoice' or term.meta_key == 'rt-id'
 
            for term in args.search_terms
 
    ):
 
        args.report_type = ReportType.AGING
 
    return args
 

	
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -577,15 +577,19 @@ def test_aging_report_does_not_include_too_recent_postings(accrual_postings):
 
                             project='Development Grant'),
 
    ], [])
 

	
 
def run_main(arglist, config=None):
 
def run_main(arglist, config=None, out_type=io.StringIO):
 
    if config is None:
 
        config = testutil.TestConfig(
 
            books_path=testutil.test_path('books/accruals.beancount'),
 
            rt_client=RTClient(),
 
        )
 
    output = io.StringIO()
 
    if out_type is io.BytesIO:
 
        arglist.insert(0, '--output-file=-')
 
    output = out_type()
 
    errors = io.StringIO()
 
    retcode = accrual.main(arglist, output, errors, config)
 
    output.seek(0)
 
    errors.seek(0)
 
    return retcode, output, errors
 

	
 
def check_main_fails(arglist, config, error_flags):
...
 
@@ -593,7 +597,6 @@ def check_main_fails(arglist, config, error_flags):
 
    assert retcode > 16
 
    assert (retcode - 16) & error_flags
 
    assert not output.getvalue()
 
    errors.seek(0)
 
    return errors
 

	
 
@pytest.mark.parametrize('arglist', [
...
 
@@ -624,7 +627,7 @@ def test_output_payments_when_only_match(arglist, expect_invoice):
 
@pytest.mark.parametrize('arglist,expect_amount', [
 
    (['310'], 420),
 
    (['310/3120'], 220),
 
    (['entity=Vendor'], 420),
 
    (['-t', 'out', 'entity=Vendor'], 420),
 
])
 
def test_main_outgoing_report(arglist, expect_amount):
 
    retcode, output, errors = run_main(arglist)
...
 
@@ -643,7 +646,6 @@ def test_main_outgoing_report(arglist, expect_amount):
 
@pytest.mark.parametrize('arglist', [
 
    ['-t', 'balance'],
 
    ['515/5150'],
 
    ['entity=MatchingProgram'],
 
])
 
def test_main_balance_report(arglist):
 
    retcode, output, errors = run_main(arglist)
...
 
@@ -666,23 +668,19 @@ def test_main_balance_report_because_no_rt_id():
 

	
 
@pytest.mark.parametrize('arglist', [
 
    [],
 
    ['-t', 'aging', 'entity=Lawyer'],
 
    ['entity=Lawyer'],
 
])
 
def test_main_aging_report(tmp_path, arglist):
 
def test_main_aging_report(arglist):
 
    if arglist:
 
        recv_rows = [row for row in AGING_AR if 'Lawyer' in row.entity]
 
        pay_rows = [row for row in AGING_AP if 'Lawyer' in row.entity]
 
    else:
 
        recv_rows = AGING_AR
 
        pay_rows = AGING_AP
 
    output_path = tmp_path / 'AgingReport.ods'
 
    arglist.insert(0, f'--output-file={output_path}')
 
    retcode, output, errors = run_main(arglist)
 
    retcode, output, errors = run_main(arglist, out_type=io.BytesIO)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    assert not output.getvalue()
 
    with output_path.open('rb') as ods_file:
 
        check_aging_ods(ods_file, None, recv_rows, pay_rows)
 
    check_aging_ods(output, None, recv_rows, pay_rows)
 

	
 
def test_main_no_books():
 
    errors = check_main_fails([], testutil.TestConfig(), 1 | 8)
...
 
@@ -693,7 +691,7 @@ def test_main_no_books():
 
@pytest.mark.parametrize('arglist', [
 
    ['499'],
 
    ['505/99999'],
 
    ['entity=NonExistent'],
 
    ['-t', 'balance', 'entity=NonExistent'],
 
])
 
def test_main_no_matches(arglist, caplog):
 
    check_main_fails(arglist, None, 8)
0 comments (0 inline, 0 general)