Changeset - 90ae343a1ade
[Not reviewed]
0 1 0
Brett Smith - 4 years ago 2020-08-18 06:02:54
brettcsmith@brettcsmith.org
balance_sheet: Refactor out Report.start_sheet method.
1 file changed with 43 insertions and 78 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/balance_sheet.py
Show inline comments
...
 
@@ -153,285 +153,283 @@ class Balances:
 
            else:
 
                retval += balance
 
        return retval
 

	
 
    def classifications(self,
 
                        account: str,
 
                        sort_period: Optional[int]=None,
 
    ) -> Sequence[data.Account]:
 
        if sort_period is None:
 
            if account in EQUITY_ACCOUNTS:
 
                sort_period = Period.PERIOD
 
            else:
 
                sort_period = Period.ANY
 
        class_bals: Mapping[data.Account, core.MutableBalance] \
 
            = collections.defaultdict(core.MutableBalance)
 
        for key, balance in self.balances.items():
 
            if not key.account.is_under(account):
 
                pass
 
            elif key.period & sort_period:
 
                class_bals[key.classification] += balance
 
            else:
 
                # Ensure the balance exists in the mapping
 
                class_bals[key.classification]
 
        norm_func = core.normalize_amount_func(f'{account}:RootsOK')
 
        def sortkey(acct: data.Account) -> Hashable:
 
            prefix, _, _ = acct.rpartition(':')
 
            balance = norm_func(class_bals[acct])
 
            try:
 
                max_bal = max(amount.number for amount in balance.values())
 
            except ValueError:
 
                max_bal = Decimal(0)
 
            return prefix, -max_bal
 
        return sorted(class_bals, key=sortkey)
 

	
 

	
 
class Report(core.BaseODS[Sequence[None], None]):
 
    C_CASH = 'Cash'
 
    C_SATISFIED = 'Satisfaction of program restrictions'
 
    NO_BALANCE = core.Balance()
 
    SPACE = ' ' * 4
 

	
 
    def __init__(self,
 
                 balances: Balances,
 
                 *,
 
                 date_fmt: str='%B %d, %Y',
 
    ) -> None:
 
        super().__init__()
 
        self.balances = balances
 
        self.date_fmt = date_fmt
 
        one_day = datetime.timedelta(days=1)
 
        date = balances.period_range.stop - one_day
 
        self.period_name = date.strftime(date_fmt)
 
        date = balances.prior_range.stop - one_day
 
        self.opening_name = date.strftime(date_fmt)
 

	
 
    def section_key(self, row: Sequence[None]) -> None:
 
        raise NotImplementedError("balance_sheet.Report.section_key")
 

	
 
    def init_styles(self) -> None:
 
        super().init_styles()
 
        self.style_header = self.merge_styles(self.style_bold, self.style_centertext)
 
        self.style_huline = self.merge_styles(
 
            self.style_header,
 
            self.border_style(core.Border.BOTTOM, '1pt'),
 
        )
 
        self.style_subtotline = self.border_style(core.Border.TOP, '1pt')
 
        self.style_totline = self.border_style(core.Border.TOP | core.Border.BOTTOM, '1pt')
 
        self.style_bottomline = self.merge_styles(
 
            self.style_subtotline,
 
            self.border_style(core.Border.BOTTOM, '2pt', 'double'),
 
        )
 

	
 
    def write_all(self) -> None:
 
        self.write_financial_position()
 
        self.write_activities()
 
        self.write_functional_expenses()
 
        self.write_cash_flows()
 

	
 
    def walk_classifications(self, cseq: Iterable[data.Account]) \
 
        -> Iterator[Tuple[str, Optional[data.Account]]]:
 
        last_prefix: Sequence[str] = []
 
        for classification in cseq:
 
            parts = classification.split(':')
 
            tail = parts.pop()
 
            space = self.SPACE * len(parts)
 
            if parts != last_prefix:
 
                yield f'{space[len(self.SPACE):]}{parts[-1]}', None
 
                last_prefix = parts
 
            yield f'{space}{tail}', classification
 

	
 
    def walk_classifications_by_account(
 
            self,
 
            account: str,
 
            sort_period: Optional[int]=None,
 
    ) -> Iterator[Tuple[str, Optional[data.Account]]]:
 
        return self.walk_classifications(self.balances.classifications(
 
            account, sort_period,
 
        ))
 

	
 
    def write_financial_position(self) -> None:
 
        self.use_sheet("Financial Position")
 
        for width in [3, 1.5, 1.5]:
 
            col_style = self.column_style(width)
 
    def start_sheet(self,
 
                    sheet_name: str,
 
                    *headers: Iterable[str],
 
                    totals_prefix: Sequence[str]=(),
 
                    first_width: Union[float, str]=3,
 
                    width: Union[float, str]=1.5,
 
    ) -> None:
 
        header_cells: Sequence[odf.table.TableCell] = [
 
            odf.table.TableCell(),
 
            *(self.multiline_cell(header_lines, stylename=self.style_huline)
 
              for header_lines in headers),
 
            *(self.multiline_cell([*totals_prefix, date_s], stylename=self.style_huline)
 
              for date_s in [self.period_name, self.opening_name]),
 
        ]
 
        self.col_count = len(header_cells)
 
        self.use_sheet(sheet_name)
 
        for index in range(self.col_count):
 
            col_style = self.column_style(width if index else first_width)
 
            self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
 
        start_date = self.balances.period_range.start.strftime(self.date_fmt)
 
        self.add_row(
 
            self.multiline_cell([
 
                "DRAFT Statement of Financial Position",
 
                self.period_name,
 
            ], numbercolumnsspanned=3, stylename=self.style_header)
 
                f"DRAFT Statement of {sheet_name}",
 
                f"{start_date}—{self.period_name}",
 
            ], numbercolumnsspanned=self.col_count, stylename=self.style_header)
 
        )
 
        self.add_row()
 
        self.add_row(
 
            odf.table.TableCell(),
 
            self.string_cell(self.period_name, stylename=self.style_huline),
 
            self.string_cell(self.opening_name, stylename=self.style_huline),
 
        )
 
        self.add_row(*header_cells)
 

	
 
    def write_financial_position(self) -> None:
 
        self.start_sheet("Financial Position")
 

	
 
        prior_assets = core.MutableBalance()
 
        period_assets = core.MutableBalance()
 
        self.add_row(self.string_cell("Assets", stylename=self.style_bold))
 
        self.add_row()
 
        for text, classification in self.walk_classifications_by_account('Assets'):
 
            text_cell = self.string_cell(text)
 
            if classification is None:
 
                self.add_row(text_cell)
 
            else:
 
                period_bal = self.balances.total(classification=classification)
 
                prior_bal = period_bal - self.balances.total(
 
                    classification=classification, period=Period.PERIOD,
 
                )
 
                self.add_row(
 
                    text_cell,
 
                    self.balance_cell(period_bal),
 
                    self.balance_cell(prior_bal),
 
                )
 
                prior_assets += prior_bal
 
                period_assets += period_bal
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Total Assets"),
 
            self.balance_cell(period_assets, stylename=self.style_bottomline),
 
            self.balance_cell(prior_assets, stylename=self.style_bottomline),
 
        )
 
        self.add_row()
 
        self.add_row()
 

	
 
        prior_liabilities = core.MutableBalance()
 
        period_liabilities = core.MutableBalance()
 
        self.add_row(self.string_cell("Liabilities and Net Assets",
 
                                      stylename=self.style_bold))
 
        self.add_row()
 
        self.add_row(self.string_cell("Liabilities", stylename=self.style_bold))
 
        self.add_row()
 
        for text, classification in self.walk_classifications_by_account('Liabilities'):
 
            text_cell = self.string_cell(text)
 
            if classification is None:
 
                self.add_row(text_cell)
 
            else:
 
                period_bal = -self.balances.total(classification=classification)
 
                prior_bal = period_bal + self.balances.total(
 
                    classification=classification, period=Period.PERIOD,
 
                )
 
                self.add_row(
 
                    text_cell,
 
                    self.balance_cell(period_bal),
 
                    self.balance_cell(prior_bal),
 
                )
 
                prior_liabilities += prior_bal
 
                period_liabilities += period_bal
 
        self.add_row(
 
            self.string_cell("Total Liabilities"),
 
            self.balance_cell(period_liabilities, stylename=self.style_totline),
 
            self.balance_cell(prior_liabilities, stylename=self.style_totline),
 
        )
 
        self.add_row()
 
        self.add_row()
 

	
 
        prior_net = core.MutableBalance()
 
        period_net = core.MutableBalance()
 
        self.add_row(self.string_cell("Net Assets", stylename=self.style_bold))
 
        self.add_row()
 
        for fund in [Fund.UNRESTRICTED, Fund.RESTRICTED]:
 
            preposition = "Without" if fund is Fund.UNRESTRICTED else "With"
 
            period_bal = -self.balances.total(account=EQUITY_ACCOUNTS, fund=fund)
 
            prior_bal = period_bal + self.balances.total(
 
                account=EQUITY_ACCOUNTS, fund=fund, period=Period.PERIOD,
 
            )
 
            self.add_row(
 
                self.string_cell(f"{preposition} donor restrictions"),
 
                self.balance_cell(period_bal),
 
                self.balance_cell(prior_bal),
 
            )
 
            prior_net += prior_bal
 
            period_net += period_bal
 
        self.add_row(
 
            self.string_cell("Total Net Assets"),
 
            self.balance_cell(period_net, stylename=self.style_subtotline),
 
            self.balance_cell(prior_net, stylename=self.style_subtotline),
 
        )
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Total Liabilities and Net Assets"),
 
            self.balance_cell(period_liabilities + period_net,
 
                              stylename=self.style_bottomline),
 
            self.balance_cell(prior_liabilities + prior_net,
 
                              stylename=self.style_bottomline),
 
        )
 

	
 
    def write_activities(self) -> None:
 
        self.use_sheet("Activities")
 
        self.start_sheet(
 
            "Activities",
 
            ["Without Donor", "Restrictions"],
 
            ["With Donor", "Restrictions"],
 
            totals_prefix=["Total Year Ended"],
 
        )
 
        bal_kwargs: Sequence[Dict[str, Any]] = [
 
            {'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED},
 
            {'period': Period.PERIOD, 'fund': Fund.RESTRICTED},
 
            {'period': Period.PERIOD},
 
            {'period': Period.PRIOR},
 
        ]
 
        col_count = len(bal_kwargs) + 1
 
        for index in range(col_count):
 
            col_style = self.column_style(1.5 if index else 3)
 
            self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
 
        self.add_row(
 
            self.multiline_cell([
 
                "DRAFT Statement of Activities",
 
                self.period_name,
 
            ], numbercolumnsspanned=col_count, stylename=self.style_header)
 
        )
 
        self.add_row()
 
        self.add_row(
 
            odf.table.TableCell(),
 
            self.multiline_cell(["Without Donor", "Restrictions"],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["With Donor", "Restrictions"],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Total Year Ended", self.period_name],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Total Year Ended", self.opening_name],
 
                                stylename=self.style_huline),
 
        )
 

	
 
        totals = [core.MutableBalance() for _ in bal_kwargs]
 
        self.add_row(self.string_cell("Support and Revenue", stylename=self.style_bold))
 
        self.add_row()
 
        for text, classification in self.walk_classifications_by_account('Income'):
 
            text_cell = self.string_cell(text)
 
            if classification is None:
 
                self.add_row(text_cell)
 
            elif classification == self.C_SATISFIED:
 
                continue
 
            else:
 
                balances = [
 
                    -self.balances.total(classification=classification, **kwargs)
 
                    for kwargs in bal_kwargs
 
                ]
 
                self.add_row(
 
                    text_cell,
 
                    *(self.balance_cell(bal) for bal in balances),
 
                )
 
                for total, bal in zip(totals, balances):
 
                    total += bal
 
        self.add_row(
 
            odf.table.TableCell(),
 
            *(self.balance_cell(total, stylename=self.style_subtotline)
 
              for total in totals),
 
        )
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Net Assets released from restrictions:"),
 
        )
 
        released = self.balances.total(
 
            account='Expenses', period=Period.PERIOD, fund=Fund.RESTRICTED,
 
        ) - self.balances.total(
 
            classification=self.C_SATISFIED, period=Period.PERIOD, fund=Fund.RESTRICTED,
 
        )
 
        totals[0] += released
 
        totals[1] -= released
 
        self.add_row(
 
            self.string_cell(self.C_SATISFIED),
 
            self.balance_cell(released),
 
            self.balance_cell(-released),
 
            self.balance_cell(self.NO_BALANCE),
 
            self.balance_cell(self.NO_BALANCE),
 
        )
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Total Support and Revenue"),
 
            *(self.balance_cell(total, stylename=self.style_totline)
...
 
@@ -468,182 +466,149 @@ class Report(core.BaseODS[Sequence[None], None]):
 
            period_bal = period_expenses
 
        else:
 
            logger.warning("Period functional expenses do not match total; math in columns B+D is wrong")
 
        prior_bal = self.balances.total(account='Expenses', period=Period.PRIOR)
 
        if (prior_expenses - prior_bal).clean_copy(1).is_zero():
 
            prior_bal = prior_expenses
 
        else:
 
            logger.warning("Prior functional expenses do not match total; math in column E is wrong")
 
        self.add_row(
 
            self.string_cell("Total Expenses"),
 
            self.balance_cell(period_bal, stylename=self.style_totline),
 
            self.balance_cell(self.NO_BALANCE, stylename=self.style_totline),
 
            self.balance_cell(period_bal, stylename=self.style_totline),
 
            self.balance_cell(prior_bal, stylename=self.style_totline),
 
        )
 

	
 
        totals[0] -= period_bal
 
        totals[2] -= period_bal
 
        totals[3] -= prior_bal
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Change in Net Assets"),
 
            *(self.balance_cell(total) for total in totals),
 
        )
 

	
 
        for kwargs in bal_kwargs:
 
            if kwargs['period'] is Period.PERIOD:
 
                kwargs['period'] = Period.BEFORE_PERIOD
 
            else:
 
                kwargs['period'] = Period.OPENING
 
        beginnings = [
 
            -self.balances.total(account=EQUITY_ACCOUNTS, **kwargs)
 
            for kwargs in bal_kwargs
 
        ]
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Beginning Net Assets"),
 
            *(self.balance_cell(beg_bal) for beg_bal in beginnings),
 
        )
 

	
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Ending Net Assets"),
 
            *(self.balance_cell(beg_bal + tot_bal, stylename=self.style_bottomline)
 
              for beg_bal, tot_bal in zip(beginnings, totals)),
 
        )
 

	
 
    def write_functional_expenses(self) -> None:
 
        self.use_sheet("Functional Expenses")
 
        self.start_sheet(
 
            "Functional Expenses",
 
            ["Program", "Services"],
 
            ["Management and", "Administrative"],
 
            ["Fundraising"],
 
            totals_prefix=["Total Year Ended"],
 
        )
 
        bal_kwargs: Sequence[Dict[str, Any]] = [
 
            {'period': Period.PERIOD, 'post_type': 'program'},
 
            {'period': Period.PERIOD, 'post_type': 'management'},
 
            {'period': Period.PERIOD, 'post_type': 'fundraising'},
 
            {'period': Period.PERIOD},
 
            {'period': Period.PRIOR},
 
        ]
 
        col_count = len(bal_kwargs) + 1
 
        for index in range(col_count):
 
            col_style = self.column_style(1.5 if index else 3)
 
            self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
 
        self.add_row(
 
            self.multiline_cell([
 
                "DRAFT Statement of Functional Expenses",
 
                self.period_name,
 
            ], numbercolumnsspanned=col_count, stylename=self.style_header)
 
        )
 
        self.add_row()
 
        self.add_row(
 
            odf.table.TableCell(),
 
            self.multiline_cell(["Program", "Services"],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Management and", "Administrative"],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Fundraising"],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Total Year Ended", self.period_name],
 
                                stylename=self.style_huline),
 
            self.multiline_cell(["Total Year Ended", self.opening_name],
 
                                stylename=self.style_huline),
 
        )
 

	
 
        totals = [core.MutableBalance() for _ in bal_kwargs]
 
        for text, classification in self.walk_classifications_by_account('Expenses'):
 
            text_cell = self.string_cell(text)
 
            if classification is None:
 
                if not text[0].isspace():
 
                    self.add_row()
 
                self.add_row(text_cell)
 
            else:
 
                balances = [
 
                    self.balances.total(classification=classification, **kwargs)
 
                    for kwargs in bal_kwargs
 
                ]
 
                self.add_row(
 
                    text_cell,
 
                    *(self.balance_cell(bal) for bal in balances),
 
                )
 
                break_bal = sum(balances[:3], core.MutableBalance())
 
                if not (break_bal - balances[3]).clean_copy(1).is_zero():
 
                    logger.warning(
 
                        "Functional expenses breakdown does not match total on row %s",
 
                        len(self.sheet.childNodes) - col_count,
 
                        len(self.sheet.childNodes) - self.col_count,
 
                    )
 
                for total, bal in zip(totals, balances):
 
                    total += bal
 
        self.add_row()
 
        self.add_row(
 
            self.string_cell("Total Expenses"),
 
            *(self.balance_cell(tot_bal, stylename=self.style_bottomline)
 
              for tot_bal in totals),
 
        )
 

	
 
    def write_cash_flows(self) -> None:
 
        self.use_sheet("Cash Flows")
 
        self.start_sheet("Cash Flows")
 
        bal_kwargs: Sequence[Dict[str, Any]] = [
 
            {'period': Period.PERIOD},
 
            {'period': Period.PRIOR},
 
        ]
 
        col_count = len(bal_kwargs) + 1
 
        for index in range(col_count):
 
            col_style = self.column_style(1.5 if index else 3)
 
            self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
 
        self.add_row(
 
            self.multiline_cell([
 
                "DRAFT Statement of Cash Flows",
 
                self.period_name,
 
            ], numbercolumnsspanned=col_count, stylename=self.style_header)
 
        )
 
        self.add_row()
 
        self.add_row(
 
            odf.table.TableCell(),
 
            self.string_cell(self.period_name, stylename=self.style_huline),
 
            self.string_cell(self.opening_name, stylename=self.style_huline),
 
        )
 

	
 
        self.add_row(self.string_cell(
 
            "Cash Flows from Operating Activities",
 
            stylename=self.style_bold,
 
        ))
 
        self.add_row()
 

	
 
        totals = [
 
            -self.balances.total(account=EQUITY_ACCOUNTS, **kwargs)
 
            for kwargs in bal_kwargs
 
        ]
 
        self.add_row(
 
            self.string_cell("Change in Net Assets"),
 
            *(self.balance_cell(bal) for bal in totals),
 
        )
 
        self.add_row(self.string_cell(
 
            "(Increase) decrease in operating assets:",
 
        ))
 
        for text, classification in self.walk_classifications_by_account('Assets'):
 
            text_cell = self.string_cell(self.SPACE + text)
 
            if classification is None:
 
                self.add_row(text_cell)
 
            elif classification == self.C_CASH:
 
                continue
 
            else:
 
                balances = [
 
                    -self.balances.total(classification=classification, **kwargs)
 
                    for kwargs in bal_kwargs
 
                ]
 
                self.add_row(
 
                    text_cell,
 
                    *(self.balance_cell(bal) for bal in balances),
 
                )
 
                for total, bal in zip(totals, balances):
 
                    total += bal
 
        self.add_row(self.string_cell(
 
            "Increase (decrease) in operating liabilities:",
 
        ))
 
        for text, classification in self.walk_classifications_by_account('Liabilities'):
 
            text_cell = self.string_cell(self.SPACE + text)
 
            if classification is None:
 
                self.add_row(text_cell)
 
            else:
 
                balances = [
 
                    -self.balances.total(classification=classification, **kwargs)
 
                    for kwargs in bal_kwargs
 
                ]
 
                self.add_row(
 
                    text_cell,
0 comments (0 inline, 0 general)