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.
This commit is contained in:
parent
d7e2ab34b9
commit
0caf78436f
2 changed files with 26 additions and 24 deletions
|
@ -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
|
problems on stderr. Then it writes a report about the status of those
|
||||||
accruals.
|
accruals.
|
||||||
|
|
||||||
If you run it with no arguments, it will generate an aging report in ODS format
|
If you run it with no arguments, it will generate an aging report in ODS format.
|
||||||
in the current directory.
|
|
||||||
|
|
||||||
Otherwise, the typical way to run it is to pass an RT ticket number or
|
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
|
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 1230 entity=Doe-Jane
|
||||||
|
|
||||||
accrual-report will automatically decide what kind of report to generate
|
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
|
from the search terms you provide and the results they return. If you
|
||||||
no search terms, it generates an aging report. If your search terms match a
|
searched on an RT ticket or invoice that returned a single outstanding
|
||||||
single outstanding payable, it writes an outgoing approval report.
|
payable, it writes an outgoing approval report. If you searched on RT ticket
|
||||||
Otherwise, it writes a basic balance report. You can specify what report
|
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::
|
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
|
# Jane Doe, even if there's more than one
|
||||||
accrual-report --report-type outgoing entity=Doe-Jane
|
accrual-report --report-type outgoing entity=Doe-Jane
|
||||||
# Write an aging report for a specific project
|
# Write an aging report for a single RT invoice (this can be helpful when
|
||||||
accrual-report --report-type aging project=ProjectName
|
# one invoice covers multiple parties)
|
||||||
|
accrual-report --report-type aging 12/345
|
||||||
"""
|
"""
|
||||||
# Copyright © 2020 Brett Smith
|
# 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`.
|
`TIK/ATT` format, is a shortcut for `invoice=LINK`.
|
||||||
""")
|
""")
|
||||||
args = parser.parse_args(arglist)
|
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
|
args.report_type = ReportType.AGING
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
|
@ -577,15 +577,19 @@ def test_aging_report_does_not_include_too_recent_postings(accrual_postings):
|
||||||
project='Development Grant'),
|
project='Development Grant'),
|
||||||
], [])
|
], [])
|
||||||
|
|
||||||
def run_main(arglist, config=None):
|
def run_main(arglist, config=None, out_type=io.StringIO):
|
||||||
if config is None:
|
if config is None:
|
||||||
config = testutil.TestConfig(
|
config = testutil.TestConfig(
|
||||||
books_path=testutil.test_path('books/accruals.beancount'),
|
books_path=testutil.test_path('books/accruals.beancount'),
|
||||||
rt_client=RTClient(),
|
rt_client=RTClient(),
|
||||||
)
|
)
|
||||||
output = io.StringIO()
|
if out_type is io.BytesIO:
|
||||||
|
arglist.insert(0, '--output-file=-')
|
||||||
|
output = out_type()
|
||||||
errors = io.StringIO()
|
errors = io.StringIO()
|
||||||
retcode = accrual.main(arglist, output, errors, config)
|
retcode = accrual.main(arglist, output, errors, config)
|
||||||
|
output.seek(0)
|
||||||
|
errors.seek(0)
|
||||||
return retcode, output, errors
|
return retcode, output, errors
|
||||||
|
|
||||||
def check_main_fails(arglist, config, error_flags):
|
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
|
||||||
assert (retcode - 16) & error_flags
|
assert (retcode - 16) & error_flags
|
||||||
assert not output.getvalue()
|
assert not output.getvalue()
|
||||||
errors.seek(0)
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@pytest.mark.parametrize('arglist', [
|
@pytest.mark.parametrize('arglist', [
|
||||||
|
@ -624,7 +627,7 @@ def test_output_payments_when_only_match(arglist, expect_invoice):
|
||||||
@pytest.mark.parametrize('arglist,expect_amount', [
|
@pytest.mark.parametrize('arglist,expect_amount', [
|
||||||
(['310'], 420),
|
(['310'], 420),
|
||||||
(['310/3120'], 220),
|
(['310/3120'], 220),
|
||||||
(['entity=Vendor'], 420),
|
(['-t', 'out', 'entity=Vendor'], 420),
|
||||||
])
|
])
|
||||||
def test_main_outgoing_report(arglist, expect_amount):
|
def test_main_outgoing_report(arglist, expect_amount):
|
||||||
retcode, output, errors = run_main(arglist)
|
retcode, output, errors = run_main(arglist)
|
||||||
|
@ -643,7 +646,6 @@ def test_main_outgoing_report(arglist, expect_amount):
|
||||||
@pytest.mark.parametrize('arglist', [
|
@pytest.mark.parametrize('arglist', [
|
||||||
['-t', 'balance'],
|
['-t', 'balance'],
|
||||||
['515/5150'],
|
['515/5150'],
|
||||||
['entity=MatchingProgram'],
|
|
||||||
])
|
])
|
||||||
def test_main_balance_report(arglist):
|
def test_main_balance_report(arglist):
|
||||||
retcode, output, errors = run_main(arglist)
|
retcode, output, errors = run_main(arglist)
|
||||||
|
@ -666,23 +668,19 @@ def test_main_balance_report_because_no_rt_id():
|
||||||
|
|
||||||
@pytest.mark.parametrize('arglist', [
|
@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:
|
if arglist:
|
||||||
recv_rows = [row for row in AGING_AR if 'Lawyer' in row.entity]
|
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]
|
pay_rows = [row for row in AGING_AP if 'Lawyer' in row.entity]
|
||||||
else:
|
else:
|
||||||
recv_rows = AGING_AR
|
recv_rows = AGING_AR
|
||||||
pay_rows = AGING_AP
|
pay_rows = AGING_AP
|
||||||
output_path = tmp_path / 'AgingReport.ods'
|
retcode, output, errors = run_main(arglist, out_type=io.BytesIO)
|
||||||
arglist.insert(0, f'--output-file={output_path}')
|
|
||||||
retcode, output, errors = run_main(arglist)
|
|
||||||
assert not errors.getvalue()
|
assert not errors.getvalue()
|
||||||
assert retcode == 0
|
assert retcode == 0
|
||||||
assert not output.getvalue()
|
check_aging_ods(output, None, recv_rows, pay_rows)
|
||||||
with output_path.open('rb') as ods_file:
|
|
||||||
check_aging_ods(ods_file, None, recv_rows, pay_rows)
|
|
||||||
|
|
||||||
def test_main_no_books():
|
def test_main_no_books():
|
||||||
errors = check_main_fails([], testutil.TestConfig(), 1 | 8)
|
errors = check_main_fails([], testutil.TestConfig(), 1 | 8)
|
||||||
|
@ -693,7 +691,7 @@ def test_main_no_books():
|
||||||
@pytest.mark.parametrize('arglist', [
|
@pytest.mark.parametrize('arglist', [
|
||||||
['499'],
|
['499'],
|
||||||
['505/99999'],
|
['505/99999'],
|
||||||
['entity=NonExistent'],
|
['-t', 'balance', 'entity=NonExistent'],
|
||||||
])
|
])
|
||||||
def test_main_no_matches(arglist, caplog):
|
def test_main_no_matches(arglist, caplog):
|
||||||
check_main_fails(arglist, None, 8)
|
check_main_fails(arglist, None, 8)
|
||||||
|
|
Loading…
Reference in a new issue