diff --git a/conservancy_beancount/books.py b/conservancy_beancount/books.py index 9f3a2b1b2fe1faceb6e19ff75ff824ef7c1b639c..86e4d288a30ac2b75decd13206890e9e03f31396 100644 --- a/conservancy_beancount/books.py +++ b/conservancy_beancount/books.py @@ -135,16 +135,38 @@ class Loader: errors.extend(new_errors) return entries, errors, options_map - def load_all(self) -> LoadResult: - """Load all of the books + def _path_year(self, path: Path) -> int: + return int(path.stem) - This method loads all of the books. It finds the books by simply - globbing the filesystem. It still loads each fiscal year in sequence to - provide the best cache utilization. + def load_all(self, from_year: Optional[Year]=None) -> LoadResult: + """Load all of the books from a starting FY + + This method loads all of the books, starting from the fiscal year you + specify. + + * Pass in a date to start from the FY for that date. + * Pass in an integer >= 1000 to start from that year. + * Pass in a smaller integer to start from an FY relative to today + (e.g., -2 starts two FYs before today). + * Pass is no argument to load all books from the first available FY. + + This method finds books by globbing the filesystem. It still loads + each fiscal year in sequence to provide the best cache utilization. """ path = Path(self.books_root, 'books') fy_paths = list(path.glob('[1-9][0-9][0-9][0-9].beancount')) - fy_paths.sort(key=lambda path: int(path.stem)) + fy_paths.sort(key=self._path_year) + if from_year is not None: + if not isinstance(from_year, int): + from_year = self.fiscal_year.for_date(from_year) + elif from_year < 1000: + from_year = self.fiscal_year.for_date() + from_year + for index, path in enumerate(fy_paths): + if self._path_year(path) >= from_year: + fy_paths = fy_paths[index:] + break + else: + fy_paths = [] return self._load_paths(iter(fy_paths)) def load_fy_range(self, @@ -154,9 +176,8 @@ class Loader: """Load books for a range of fiscal years This method generates a range of fiscal years by calling - FiscalYear.range() with its first two arguments. It returns a string of - Beancount directives to load the books from the first available fiscal - year through the end of the range. + FiscalYear.range() with its arguments. It loads all the books within + that range. """ fy_range = self.fiscal_year.range(from_fy, to_fy) fy_paths = self._iter_fy_books(fy_range) diff --git a/tests/test_books_loader.py b/tests/test_books_loader.py index 7b3ce6dacb1d46cef0439665f39ad6f1e3503db3..6f9b1a554945955e3367352f30d12b4989429eb2 100644 --- a/tests/test_books_loader.py +++ b/tests/test_books_loader.py @@ -27,11 +27,13 @@ from . import testutil from beancount.core import data as bc_data from conservancy_beancount import books +FY_START_MONTH = 3 + books_path = testutil.test_path('books') @pytest.fixture(scope='module') def conservancy_loader(): - return books.Loader(books_path, books.FiscalYear(3)) + return books.Loader(books_path, books.FiscalYear(FY_START_MONTH)) def check_openings(entries): openings = collections.defaultdict(int) @@ -41,18 +43,13 @@ def check_openings(entries): for account, count in openings.items(): assert count == 1, f"found {count} open directives for {account}" -def check_narrations(entries, expected): - expected = iter(expected) - expected_next = next(expected) +def txn_dates(entries): for entry in entries: - if (isinstance(entry, bc_data.Transaction) - and entry.narration == expected_next): - try: - expected_next = next(expected) - except StopIteration: - break - else: - assert None, f"{expected_next} not found in entry narrations" + if isinstance(entry, bc_data.Transaction): + yield entry.date + +def txn_years(entries): + return frozenset(date.year for date in txn_dates(entries)) @pytest.mark.parametrize('from_fy,to_fy,expect_years', [ (2019, 2019, range(2019, 2020)), @@ -70,7 +67,7 @@ def check_narrations(entries, expected): def test_load_fy_range(conservancy_loader, from_fy, to_fy, expect_years): entries, errors, options_map = conservancy_loader.load_fy_range(from_fy, to_fy) assert not errors - check_narrations(entries, [f'{year} donation' for year in expect_years]) + assert txn_years(entries).issuperset(expect_years) def test_load_fy_range_does_not_duplicate_openings(conservancy_loader): entries, errors, options_map = conservancy_loader.load_fy_range(2010, 2030) @@ -82,8 +79,25 @@ def test_load_fy_range_empty(conservancy_loader): assert not entries assert not options_map -def test_load_all(conservancy_loader): - entries, errors, options_map = conservancy_loader.load_all() +@pytest.mark.parametrize('from_year', [None, *range(2018, 2021)]) +def test_load_all(conservancy_loader, from_year): + entries, errors, options_map = conservancy_loader.load_all(from_year) + from_year = from_year or 2018 + assert not errors + check_openings(entries) + assert txn_years(entries).issuperset(range(from_year or 2018, 2021)) + +@pytest.mark.parametrize('from_date', [ + date(2019, 2, 1), + date(2019, 9, 15), + date(2020, 1, 20), + date(2020, 5, 31), +]) +def test_load_all_from_date(conservancy_loader, from_date): + from_year = from_date.year + if from_date.month < FY_START_MONTH: + from_year -= 1 + entries, errors, options_map = conservancy_loader.load_all(from_date) assert not errors - check_narrations(entries, [f'{year} donation' for year in range(2018, 2021)]) check_openings(entries) + assert txn_years(entries).issuperset(range(from_year, 2021)) diff --git a/tests/testutil.py b/tests/testutil.py index b5eea67bddb665e7838ce03c1811fc3ef43df653..f9049344533c4d8082904ebeb22d7b70dc0b2b9f 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -217,7 +217,7 @@ class TestBooksLoader(books.Loader): def __init__(self, source): self.source = source - def load_all(self): + def load_all(self, from_year=None): return bc_loader.load_file(self.source) def load_fy_range(self, from_fy, to_fy=None):