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