diff --git a/conservancy_beancount/reports/accrual.py b/conservancy_beancount/reports/accrual.py index 6bf23f00d943cb7f674f2ce7257d3985d15ca453..f2c65bd7588f4896ee6c7ab9a86aa9bbc2547f70 100644 --- a/conservancy_beancount/reports/accrual.py +++ b/conservancy_beancount/reports/accrual.py @@ -507,11 +507,29 @@ class AgingReport(BaseReport): self.ods = AgingODS(rt_client, date, self.logger) def run(self, groups: PostGroups) -> None: - rows = list( - group.since_last_nonzero() - for group in groups.values() - if not group.is_zero() - ) + rows: List[AccrualPostings] = [] + for group in groups.values(): + if group.is_zero(): + # Cheap optimization: don't slice and dice groups we're not + # going to report anyway. + continue + elif group.accrual_type is None: + group = group.since_last_nonzero() + else: + # Filter out new accruals after the report date. + # e.g., cover the case that the same invoices has multiple + # postings over time, and we don't want to report too-recent + # ones. + cutoff_date = self.ods.date - datetime.timedelta( + days=group.accrual_type.value.aging_thresholds[-1], + ) + group = AccrualPostings( + post for post in group.since_last_nonzero() + if post.meta.date <= cutoff_date + or group.accrual_type.normalize_amount(post.units.number) < 0 + ) + if group and not group.is_zero(): + rows.append(group) rows.sort(key=lambda related: ( related.account, related[0].meta.date, diff --git a/tests/books/accruals.beancount b/tests/books/accruals.beancount index afa8169cfeb0523e178514bf0429490b28747e12..31de762f64b5c8802a2650259003e8b9c80ea7e4 100644 --- a/tests/books/accruals.beancount +++ b/tests/books/accruals.beancount @@ -151,3 +151,10 @@ project: "Development Grant" Assets:Receivable:Accounts 5500 USD Income:Donations -5500 USD + +2010-09-15 * "GrantCo" "2010Q3 grant" + rt-id: "rt:470" + invoice: "rt:470/4700" + project: "Development Grant" + Assets:Receivable:Accounts 6000 USD + Income:Donations -6000 USD diff --git a/tests/test_reports_accrual.py b/tests/test_reports_accrual.py index 8b90dbb43bcf5c00ea39089d3dab602536b70ef2..147b479802e1b07055c0e616966da4317429ffa6 100644 --- a/tests/test_reports_accrual.py +++ b/tests/test_reports_accrual.py @@ -121,7 +121,7 @@ AGING_AR = [ AgingRow.make_simple('2010-03-05', 'EarlyBird', -500, 'rt:40/400'), AgingRow.make_simple('2010-05-15', 'MatchingProgram', 1500, 'rt://ticket/515/attachments/5150'), - AgingRow.make_simple('2010-06-15', 'GrantCo', 5500, 'rt:470/4700', + AgingRow.make_simple('2010-06-15', 'GrantCo', 11500, 'rt:470/4700', project='Development Grant'), ] @@ -630,6 +630,11 @@ def test_aging_report(accrual_postings): def test_aging_report_date_cutoffs(accrual_postings, date, recv_end, pay_end): expect_recv = AGING_AR[:recv_end] expect_pay = AGING_AP[:pay_end] + if 10 <= date.day < 12: + # Take the 60 USD posting out of the invoice 510/6100 payable. + expect_pay[-1] = expect_pay[-1]._replace( + at_cost=testutil.Amount(expect_pay[-1].at_cost.number - 60), + ) output = run_aging_report(accrual_postings, date) check_aging_ods(output, date, expect_recv, expect_pay) @@ -644,6 +649,20 @@ def test_aging_report_entity_consistency(accrual_postings): AgingRow.make_simple('2010-04-15', 'MultiPartyB', 125, 'rt:480/4800'), ]) +def test_aging_report_does_not_include_too_recent_postings(accrual_postings): + # This date is after the Q3 posting, but too soon after for that to be + # included in the aging report. + date = datetime.date(2010, 10, 1) + output = run_aging_report(( + post for post in accrual_postings + if post.meta.get('rt-id') == 'rt:470' + ), date) + # Date+amount are both from the Q2 posting only. + check_aging_ods(output, date, [ + AgingRow.make_simple('2010-06-15', 'GrantCo', 5500, 'rt:470/4700', + project='Development Grant'), + ], []) + def run_main(arglist, config=None): if config is None: config = testutil.TestConfig(