diff --git a/conservancy_beancount/reconcile/statement_reconciler.py b/conservancy_beancount/reconcile/statement_reconciler.py index 17dc91201d2a6fe70540b713a93ce3a66bfc32c5..f42000cf7da4faa641992687d04f6d8c6ba062fa 100644 --- a/conservancy_beancount/reconcile/statement_reconciler.py +++ b/conservancy_beancount/reconcile/statement_reconciler.py @@ -295,6 +295,20 @@ def format_multirecord(r1s: List[dict], r2s: List[dict], note: str) -> List[list return match_output +def _start_of_month(time, offset_months=0): + if offset_months > 0: + return _start_of_month(time.replace(day=28) + datetime.timedelta(days=4), offset_months - 1) + else: + return time.replace(day=1) + + +def round_to_month(begin_date, end_date): + """Round a beginning and end date to beginning and end of months respectively.""" + return ( + _start_of_month(begin_date), + _start_of_month(end_date, offset_months=1) - datetime.timedelta(days=1)) + + def sort_records(records: List) -> List: return sorted(records, key=lambda x: (x['date'], x['amount'])) @@ -596,6 +610,7 @@ def parse_arguments(argv: List[str]) -> argparse.Namespace: # parser.add_argument('--report-group-regex') parser.add_argument('--show-reconciled-matches', action='store_true') parser.add_argument('--non-interactive', action='store_true', help="Don't prompt to write to the books") # parser.add_argument('--statement-balance', type=parse_decimal_with_separator, required=True, help="A.K.A \"cleared balance\" taken from the end of the period on the PDF statement. Required because CSV statements don't include final or running totals") + parser.add_argument('--full-months', action='store_true', help='Match payments over the full month, rather that just between the beginning and end dates of the CSV statement') args = parser.parse_args(args=argv) return args @@ -676,6 +691,9 @@ def main(arglist: Optional[Sequence[str]] = None, begin_date = statement_trans[0]['date'] end_date = statement_trans[-1]['date'] + if args.full_months: + begin_date, end_date = round_to_month(begin_date, end_date) + # Query for the Beancount books data for this above period. # # There are pros and cons for using Beancount's in-memory entries diff --git a/tests/test_reconcile.py b/tests/test_reconcile.py index 292de3648d6476d04415aba9ffc7300da6fe9dc9..ba6d6ac655118e61351e7ca7e679fc6afed46f9d 100644 --- a/tests/test_reconcile.py +++ b/tests/test_reconcile.py @@ -5,6 +5,8 @@ import os import tempfile import textwrap +import pytest + from conservancy_beancount.reconcile.statement_reconciler import ( date_proximity, format_output, @@ -15,6 +17,7 @@ from conservancy_beancount.reconcile.statement_reconciler import ( read_fr_csv, remove_duplicate_words, remove_payee_junk, + round_to_month, subset_match, totals, write_metadata_to_books, @@ -388,3 +391,20 @@ def test_format_output(): matches, _, _ = match_statement_and_books(statement, books) output = format_output(matches, datetime.date(2022, 1, 1), datetime.date(2022, 2, 1), 'test.csv', True) assert '2022-01-01: 10.00 Patreon / Patreon / 12345 → 2022-01-01: 10.00 Patreon ✓ Matched' in output + + +month_test_data = [ + ((datetime.date(2022, 1, 2), datetime.date(2022, 1, 30)), + (datetime.date(2022, 1, 1), datetime.date(2022, 1, 31))), + ((datetime.date(2022, 4, 2), datetime.date(2022, 4, 29)), + (datetime.date(2022, 4, 1), datetime.date(2022, 4, 30))), + ((datetime.date(2022, 2, 2), datetime.date(2022, 2, 27)), + (datetime.date(2022, 2, 1), datetime.date(2022, 2, 28))), + ((datetime.date(2024, 2, 2), datetime.date(2024, 2, 27)), + (datetime.date(2024, 2, 1), datetime.date(2024, 2, 29))), +] + + +@pytest.mark.parametrize('input_dates,rounded_dates', month_test_data) +def test_rounds_to_full_month(input_dates, rounded_dates): + assert round_to_month(*input_dates) == rounded_dates