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
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue