Changeset - 175ac3bd7a30
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-06-11 19:27:36
brettcsmith@brettcsmith.org
accrual: Outgoing report groups by rt-id. RT#11594.
4 files changed with 78 insertions and 56 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -509,7 +509,8 @@ class OutgoingReport(BaseReport):
 
    def _primary_rt_id(self, posts: AccrualPostings) -> rtutil.TicketAttachmentIds:
 
        rt_ids = list(posts.first_meta_links('rt-id'))
 
        rt_ids_count = len(rt_ids)
 
        if rt_ids_count != 1:
 
            raise ValueError(f"{rt_ids_count} rt-id links found")
 
        parsed = rtutil.RT.parse(rt_ids.pop())
 
        rt_id = posts.rt_id
 
        if rt_id is None:
 
            raise ValueError("no rt-id links found")
 
        elif isinstance(rt_id, Sentinel):
 
            raise ValueError("multiple rt-id links found")
 
        parsed = rtutil.RT.parse(rt_id)
 
        if parsed is None:
...
 
@@ -600,13 +601,2 @@ class ReportType(enum.Enum):
 

	
 
    @classmethod
 
    def default_for(cls, groups: PostGroups) -> 'ReportType':
 
        if len(groups) == 1 and all(
 
                group.accrual_type is AccrualAccount.PAYABLE
 
                and not group.is_paid()
 
                for group in groups.values()
 
        ):
 
            return cls.OUTGOING
 
        else:
 
            return cls.BALANCE
 

	
 

	
...
 
@@ -614,2 +604,3 @@ class ReturnFlag(enum.IntFlag):
 
    LOAD_ERRORS = 1
 
    # 2 was used in the past, it can probably be reclaimed.
 
    REPORT_ERRORS = 4
...
 
@@ -689,2 +680,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 

	
 
    returncode = 0
 
    books_loader = config.books_loader()
...
 
@@ -697,6 +689,2 @@ def main(arglist: Optional[Sequence[str]]=None,
 
    filters.remove_opening_balance_txn(entries)
 

	
 
    returncode = 0
 
    postings = filter_search(data.Posting.from_entries(entries), args.search_terms)
 
    groups: PostGroups = dict(AccrualPostings.group_by_first_meta_link(postings, 'invoice'))
 
    for error in load_errors:
...
 
@@ -704,11 +692,24 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        returncode |= ReturnFlag.LOAD_ERRORS
 
    if not groups:
 

	
 
    postings = list(filter_search(
 
        data.Posting.from_entries(entries), args.search_terms,
 
    ))
 
    if not postings:
 
        logger.warning("no matching entries found to report")
 
        returncode |= ReturnFlag.NOTHING_TO_REPORT
 

	
 
    groups = {
 
        key: posts
 
        for source_posts in groups.values()
 
        for key, posts in source_posts.make_consistent()
 
    }
 
    groups: PostGroups
 
    if args.report_type is None or args.report_type is ReportType.OUTGOING:
 
        groups = dict(AccrualPostings.group_by_first_meta_link(postings, 'rt-id'))
 
        if (args.report_type is None
 
            and len(groups) == 1
 
            and all(g.accrual_type is AccrualAccount.PAYABLE and not g.is_paid()
 
                    for g in groups.values())
 
        ):
 
            args.report_type = ReportType.OUTGOING
 
    if args.report_type is not ReportType.OUTGOING:
 
        groups = {
 
            key: group
 
            for _, source in AccrualPostings.group_by_first_meta_link(postings, 'invoice')
 
            for key, group in source.make_consistent()
 
        }
 
    if args.report_type is not ReportType.AGING:
...
 
@@ -717,5 +718,4 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        } or groups
 
    del postings
 

	
 
    if args.report_type is None:
 
        args.report_type = ReportType.default_for(groups)
 
    report: Optional[BaseReport] = None
...
 
@@ -742,3 +742,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        out_file = cliutil.text_output(args.output_file, stdout)
 
        report = args.report_type.value(out_file)
 
        report = BalanceReport(out_file)
 

	
setup.py
Show inline comments
...
 
@@ -7,3 +7,3 @@ setup(
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.1.8',
 
    version='1.1.9',
 
    author='Software Freedom Conservancy',
tests/books/accruals.beancount
Show inline comments
...
 
@@ -73,3 +73,3 @@
 

	
 
2010-04-30 ! "Vendor" "Travel reimbursement"
 
2010-04-25 ! "Vendor" "First trip travel reimbursement"
 
  rt-id: "rt:310"
...
 
@@ -81,2 +81,10 @@
 

	
 
2010-04-30 * "Vendor" "Second trip travel reimbursement"
 
  rt-id: "rt:310"
 
  contract: "rt:310/3100"
 
  invoice: "rt:310/3120"
 
  project: "Conservancy"
 
  Liabilities:Payable:Accounts  -220 USD
 
  Expenses:Travel  220 USD
 

	
 
2010-05-05 * "DonorA" "Donation pledge"
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -108,3 +108,4 @@ AGING_AP = [
 
    AgingRow.make_simple('2010-03-30', 'EarlyBird', 75, 'rt:490/4900'),
 
    AgingRow.make_simple('2010-04-30', 'Vendor', 200, 'FIXME'),
 
    AgingRow.make_simple('2010-04-25', 'Vendor', 200, 'FIXME'),
 
    AgingRow.make_simple('2010-04-30', 'Vendor', 220, 'rt:310/3120'),
 
    AgingRow.make_simple('2010-06-10', 'Lawyer', 280, 'rt:510/6100'),
...
 
@@ -130,2 +131,6 @@ class RTClient(testutil.RTClient):
 
        ],
 
        '310': [
 
            ('3100', 'VendorContract.pdf', 'application/pdf', '1.7m'),
 
            ('3120', 'VendorInvoiceB.pdf', 'application/pdf', '1.8m'),
 
        ],
 
        '490': [],
...
 
@@ -419,3 +424,3 @@ def check_output(output, expect_patterns):
 

	
 
def run_outgoing(invoice, postings, rt_client=None):
 
def run_outgoing(rt_id, postings, rt_client=None):
 
    if rt_client is None:
...
 
@@ -423,6 +428,6 @@ def run_outgoing(invoice, postings, rt_client=None):
 
    if not isinstance(postings, core.RelatedPostings):
 
        postings = accruals_by_meta(postings, invoice, wrap_type=accrual.AccrualPostings)
 
        postings = accruals_by_meta(postings, rt_id, 'rt-id', wrap_type=accrual.AccrualPostings)
 
    output = io.StringIO()
 
    report = accrual.OutgoingReport(rt_client, output)
 
    report.run({invoice: postings})
 
    report.run({rt_id: postings})
 
    return output
...
 
@@ -444,4 +449,3 @@ def test_balance_report(accrual_postings, invoice, expected, caplog):
 
def test_outgoing_report(accrual_postings, caplog):
 
    invoice = 'rt:510/6100'
 
    output = run_outgoing(invoice, accrual_postings)
 
    output = run_outgoing('rt:510', accrual_postings)
 
    rt_url = RTClient.DEFAULT_URL[:-9]
...
 
@@ -469,5 +473,4 @@ def test_outgoing_report(accrual_postings, caplog):
 
def test_outgoing_report_custom_field_fallbacks(accrual_postings, caplog):
 
    invoice = 'rt:510/6100'
 
    rt_client = RTClient(want_cfs=False)
 
    output = run_outgoing(invoice, accrual_postings, rt_client)
 
    output = run_outgoing('rt:510', accrual_postings, rt_client)
 
    assert not caplog.records
...
 
@@ -481,4 +484,3 @@ def test_outgoing_report_custom_field_fallbacks(accrual_postings, caplog):
 
def test_outgoing_report_fx_amounts(accrual_postings, caplog):
 
    invoice = 'rt:520/5200'
 
    output = run_outgoing(invoice, accrual_postings)
 
    output = run_outgoing('rt:520 rt:525', accrual_postings)
 
    assert not caplog.records
...
 
@@ -490,5 +492,17 @@ def test_outgoing_report_fx_amounts(accrual_postings, caplog):
 

	
 
def test_outgoing_report_multi_invoice(accrual_postings, caplog):
 
    output = run_outgoing('rt:310', accrual_postings)
 
    assert not caplog.records
 
    check_output(output, [
 
        r'^PAYMENT FOR APPROVAL:$',
 
        r'^REQUESTOR: Mx\. 310 <mx310@example\.org>$',
 
        r'^TOTAL TO PAY: \$420.00$',
 
    ])
 

	
 
def test_outgoing_report_without_rt_id(accrual_postings, caplog):
 
    invoice = 'rt://ticket/515/attachments/5150'
 
    output = run_outgoing(invoice, accrual_postings)
 
    related = accruals_by_meta(
 
        accrual_postings, invoice, wrap_type=accrual.AccrualPostings,
 
    )
 
    output = run_outgoing(None, related)
 
    assert caplog.records
...
 
@@ -526,4 +540,4 @@ def test_aging_report(accrual_postings):
 
    # the second is exactly 60 days after the 2010-05-15 receivable.
 
    (datetime.date(2010, 7, 10), 1, 4),
 
    (datetime.date(2010, 7, 14), 2, 4),
 
    (datetime.date(2010, 7, 10), 1, 5),
 
    (datetime.date(2010, 7, 14), 2, 5),
 
])
...
 
@@ -608,8 +622,8 @@ def test_output_payments_when_only_match(arglist, expect_invoice):
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['510'],
 
    ['510/6100'],
 
    ['entity=Lawyer'],
 
@pytest.mark.parametrize('arglist,expect_amount', [
 
    (['310'], 420),
 
    (['310/3120'], 220),
 
    (['entity=Vendor'], 420),
 
])
 
def test_main_outgoing_report(arglist):
 
def test_main_outgoing_report(arglist, expect_amount):
 
    retcode, output, errors = run_main(arglist)
...
 
@@ -618,9 +632,9 @@ def test_main_outgoing_report(arglist):
 
    rt_url = RTClient.DEFAULT_URL[:-9]
 
    rt_id_url = re.escape(f'<{rt_url}Ticket/Display.html?id=510>')
 
    contract_url = re.escape(f'<{rt_url}Ticket/Attachment/4000/4000/contract.pdf>')
 
    rt_id_url = re.escape(f'<{rt_url}Ticket/Display.html?id=310>')
 
    contract_url = re.escape(f'<{rt_url}Ticket/Attachment/3120/3120/VendorContract.pdf>')
 
    check_output(output, [
 
        r'^REQUESTOR: Mx\. 510 <mx510@example\.org>$',
 
        r'^TOTAL TO PAY: \$280\.00$',
 
        r'^\s*2010-06-12\s',
 
        r'^\s+Expenses:FilingFees\s+60\.00 USD$',
 
        r'^REQUESTOR: Mx\. 310 <mx310@example\.org>$',
 
        rf'^TOTAL TO PAY: \${expect_amount}\.00$',
 
        r'^\s*2010-04-30\s',
 
        r'^\s+Expenses:Travel\s+220 USD$',
 
    ])
0 comments (0 inline, 0 general)