balance_sheet: Support arbitrary date ranges.

This lets you do year-over-year comparisons of smaller ranges of time, like
a quarter.
This commit is contained in:
Brett Smith 2020-08-18 15:15:02 -04:00
parent 9e7df0b020
commit a0ff9e6834
2 changed files with 33 additions and 19 deletions

View file

@ -71,9 +71,11 @@ class Fund(enum.IntFlag):
class Period(enum.IntFlag): class Period(enum.IntFlag):
OPENING = enum.auto() OPENING = enum.auto()
PRIOR = enum.auto() PRIOR = enum.auto()
MIDDLE = enum.auto()
PERIOD = enum.auto() PERIOD = enum.auto()
BEFORE_PERIOD = OPENING | PRIOR THRU_PRIOR = OPENING | PRIOR
ANY = OPENING | PRIOR | PERIOD THRU_MIDDLE = THRU_PRIOR | MIDDLE
ANY = THRU_MIDDLE | PERIOD
class BalanceKey(NamedTuple): class BalanceKey(NamedTuple):
@ -92,24 +94,35 @@ class Balances:
fund_key: str='project', fund_key: str='project',
unrestricted_fund_value: str='Conservancy', unrestricted_fund_value: str='Conservancy',
) -> None: ) -> None:
self.prior_range = ranges.DateRange( year_diff = (stop_date - start_date).days // 365
cliutil.diff_year(start_date, -1), if year_diff == 0:
cliutil.diff_year(stop_date, -1), self.prior_range = ranges.DateRange(
) cliutil.diff_year(start_date, -1),
assert self.prior_range.stop <= start_date cliutil.diff_year(stop_date, -1),
)
self.period_desc = "Period"
else:
self.prior_range = ranges.DateRange(
cliutil.diff_year(start_date, -year_diff),
start_date,
)
self.period_desc = f"Year{'s' if year_diff > 1 else ''}"
self.middle_range = ranges.DateRange(self.prior_range.stop, start_date)
self.period_range = ranges.DateRange(start_date, stop_date) self.period_range = ranges.DateRange(start_date, stop_date)
self.balances: Mapping[BalanceKey, core.MutableBalance] \ self.balances: Mapping[BalanceKey, core.MutableBalance] \
= collections.defaultdict(core.MutableBalance) = collections.defaultdict(core.MutableBalance)
for post in postings: for post in postings:
post_date = post.meta.date post_date = post.meta.date
if post_date in self.period_range: if post_date >= stop_date:
continue
elif post_date in self.period_range:
period = Period.PERIOD period = Period.PERIOD
elif post_date in self.middle_range:
period = Period.MIDDLE
elif post_date in self.prior_range: elif post_date in self.prior_range:
period = Period.PRIOR period = Period.PRIOR
elif post_date < self.prior_range.start:
period = Period.OPENING
else: else:
continue period = Period.OPENING
if post.account == 'Expenses:CurrencyConversion': if post.account == 'Expenses:CurrencyConversion':
account = data.Account('Income:CurrencyConversion') account = data.Account('Income:CurrencyConversion')
else: else:
@ -214,6 +227,7 @@ class Report(core.BaseODS[Sequence[None], None]):
) -> None: ) -> None:
super().__init__() super().__init__()
self.balances = balances self.balances = balances
self.period_desc = balances.period_desc
self.date_fmt = date_fmt self.date_fmt = date_fmt
one_day = datetime.timedelta(days=1) one_day = datetime.timedelta(days=1)
date = balances.period_range.stop - one_day date = balances.period_range.stop - one_day
@ -359,7 +373,7 @@ class Report(core.BaseODS[Sequence[None], None]):
self.start_sheet("Financial Position") self.start_sheet("Financial Position")
balance_kwargs: Sequence[KWArgs] = [ balance_kwargs: Sequence[KWArgs] = [
{'period': Period.ANY}, {'period': Period.ANY},
{'period': Period.BEFORE_PERIOD}, {'period': Period.THRU_PRIOR},
] ]
asset_totals = self.write_classifications_by_account('Assets', balance_kwargs) asset_totals = self.write_classifications_by_account('Assets', balance_kwargs)
@ -400,7 +414,7 @@ class Report(core.BaseODS[Sequence[None], None]):
"Activities", "Activities",
["Without Donor", "Restrictions"], ["Without Donor", "Restrictions"],
["With Donor", "Restrictions"], ["With Donor", "Restrictions"],
totals_prefix=["Total Year Ended"], totals_prefix=[f"Total {self.period_desc} Ended"],
) )
bal_kwargs: Sequence[Dict[str, Any]] = [ bal_kwargs: Sequence[Dict[str, Any]] = [
{'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED}, {'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED},
@ -482,7 +496,7 @@ class Report(core.BaseODS[Sequence[None], None]):
for kwargs in bal_kwargs: for kwargs in bal_kwargs:
if kwargs['period'] is Period.PERIOD: if kwargs['period'] is Period.PERIOD:
kwargs['period'] = Period.BEFORE_PERIOD kwargs['period'] = Period.THRU_MIDDLE
else: else:
kwargs['period'] = Period.OPENING kwargs['period'] = Period.OPENING
equity_totals = [ equity_totals = [
@ -502,7 +516,7 @@ class Report(core.BaseODS[Sequence[None], None]):
["Program", "Services"], ["Program", "Services"],
["Management and", "Administrative"], ["Management and", "Administrative"],
["Fundraising"], ["Fundraising"],
totals_prefix=["Total Year Ended"], totals_prefix=[f"Total {self.period_desc} Ended"],
) )
totals = self.write_classifications_by_account('Expenses', [ totals = self.write_classifications_by_account('Expenses', [
{'period': Period.PERIOD, 'post_type': 'program'}, {'period': Period.PERIOD, 'post_type': 'program'},
@ -558,7 +572,7 @@ class Report(core.BaseODS[Sequence[None], None]):
self.write_totals_row("Net Increase in Cash", period_totals) self.write_totals_row("Net Increase in Cash", period_totals)
begin_totals = [ begin_totals = [
self.balances.total(classification=self.C_CASH, period=period) self.balances.total(classification=self.C_CASH, period=period)
for period in [Period.BEFORE_PERIOD, Period.OPENING] for period in [Period.THRU_MIDDLE, Period.OPENING]
] ]
self.write_totals_row("Beginning Cash", begin_totals) self.write_totals_row("Beginning Cash", begin_totals)
self.write_totals_row( self.write_totals_row(
@ -572,8 +586,8 @@ class Report(core.BaseODS[Sequence[None], None]):
"Trial Balances", "Trial Balances",
["Account Name"], ["Account Name"],
["Classification"], ["Classification"],
["Balance Ending", self.opening_name], ["Balance Beginning", self.balances.period_range.start.strftime(self.date_fmt)],
totals_prefix=["Change During", "Year Ending"], totals_prefix=["Change During", f"{self.period_desc} Ending"],
title_fmt="Chart of Accounts with DRAFT {sheet_name}", title_fmt="Chart of Accounts with DRAFT {sheet_name}",
) )
# Widen text columns # Widen text columns

View file

@ -5,7 +5,7 @@ from setuptools import setup
setup( setup(
name='conservancy_beancount', name='conservancy_beancount',
description="Plugin, library, and reports for reading Conservancy's books", description="Plugin, library, and reports for reading Conservancy's books",
version='1.8.3', version='1.8.4',
author='Software Freedom Conservancy', author='Software Freedom Conservancy',
author_email='info@sfconservancy.org', author_email='info@sfconservancy.org',
license='GNU AGPLv3+', license='GNU AGPLv3+',