Changeset - 9ae974009b13
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2020-06-27 20:31:27
brettcsmith@brettcsmith.org
fund: Add outstanding balances to text fund report.
2 files changed with 60 insertions and 33 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/fund.py
Show inline comments
...
 
@@ -80,7 +80,13 @@ from .. import data
 
AccountsMap = Mapping[data.Account, core.PeriodPostings]
 
FundPosts = Tuple[MetaValue, AccountsMap]
 

	
 
BALANCE_ACCOUNTS = ['Equity', 'Income', 'Expenses']
 
EQUITY_ACCOUNTS = ['Equity', 'Income', 'Expenses']
 
INFO_ACCOUNTS = [
 
    'Assets:Receivable',
 
    'Assets:Prepaid',
 
    'Liabilities:Payable',
 
    'Liabilities:UnearnedIncome',
 
]
 
PROGNAME = 'fund-report'
 
UNRESTRICTED_FUND = 'Conservancy'
 
logger = logging.getLogger('conservancy_beancount.reports.fund')
...
 
@@ -170,12 +176,17 @@ class TextReport:
 
                          account_map: AccountsMap,
 
    ) -> Iterator[Tuple[str, Sequence[str]]]:
 
        total_fmt = f'{fund} balance as of {{}}'
 
        for acct_s, balance in core.account_balances(account_map, BALANCE_ACCOUNTS):
 
        for acct_s, balance in core.account_balances(account_map, EQUITY_ACCOUNTS):
 
            if acct_s is core.OPENING_BALANCE_NAME:
 
                acct_s = total_fmt.format(self.start_date.isoformat())
 
            elif acct_s is core.ENDING_BALANCE_NAME:
 
                acct_s = total_fmt.format(self.stop_date.isoformat())
 
            yield acct_s, (-balance).format(None, sep='\0').split('\0')
 
        for _, account in core.sort_and_filter_accounts(account_map, INFO_ACCOUNTS):
 
            balance = account_map[account].stop_bal
 
            if not balance.is_zero():
 
                balance = core.normalize_amount_func(account)(balance)
 
                yield account, balance.format(None, sep='\0').split('\0')
 

	
 
    def write(self, rows: Iterable[FundPosts]) -> None:
 
        output = [
...
 
@@ -319,7 +330,6 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        post
 
        for post in data.Posting.from_entries(entries)
 
        if post.meta.date < args.stop_date
 
        and post.account.is_under(*BALANCE_ACCOUNTS)
 
    )
 
    for search_term in args.search_terms:
 
        postings = search_term.filter_postings(postings)
tests/test_reports_fund.py
Show inline comments
...
 
@@ -40,6 +40,8 @@ START_DATE = datetime.date(2018, 3, 1)
 
MID_DATE = datetime.date(2019, 3, 1)
 
STOP_DATE = datetime.date(2020, 3, 1)
 

	
 
EQUITY_ROOT_ACCOUNTS = ('Expenses:', 'Equity:', 'Income:')
 

	
 
OPENING_BALANCES = {
 
    'Alpha': 3000,
 
    'Bravo': 2000,
...
 
@@ -51,18 +53,26 @@ BALANCES_BY_YEAR = {
 
    ('Conservancy', 2018): [
 
        ('Income:Other', 40),
 
        ('Expenses:Other', -4),
 
        ('Assets:Receivable:Accounts', 40),
 
        ('Liabilities:Payable:Accounts', 4),
 
    ],
 
    ('Conservancy', 2019): [
 
        ('Income:Other', 42),
 
        ('Expenses:Other', Decimal('-4.20')),
 
        ('Equity:Realized:CurrencyConversion', Decimal('6.20')),
 
        ('Assets:Receivable:Accounts', -40),
 
        ('Liabilities:Payable:Accounts', -4),
 
    ],
 
    ('Alpha', 2018): [
 
        ('Income:Other', 60),
 
        ('Liabilities:UnearnedIncome', 30),
 
        ('Assets:Prepaid:Expenses', 20),
 
    ],
 
    ('Alpha', 2019): [
 
        ('Income:Other', 30),
 
        ('Expenses:Other', -26),
 
        ('Assets:Prepaid:Expenses', -20),
 
        ('Liabilities:UnearnedIncome', -30),
 
    ],
 
    ('Bravo', 2018): [
 
        ('Expenses:Other', -20),
...
 
@@ -76,14 +86,6 @@ BALANCES_BY_YEAR = {
 
def fund_entries():
 
    return copy.deepcopy(_ledger_load[0])
 

	
 
def fund_postings(entries, project, stop_date):
 
    return (
 
        post for post in data.Posting.from_entries(entries)
 
        if post.meta.date < stop_date
 
        and post.account.is_under('Equity', 'Income', 'Expenses')
 
        and post.meta.get('project') == project
 
    )
 

	
 
def split_text_lines(output):
 
    for line in output:
 
        account, amount = line.rsplit(None, 1)
...
 
@@ -94,6 +96,17 @@ def format_amount(amount, currency='USD'):
 
        amount, currency, format_type='accounting',
 
    )
 

	
 
def check_text_balances(actual, expected, *expect_accounts):
 
    balance = Decimal()
 
    for expect_account in expect_accounts:
 
        expect_amount = expected[expect_account]
 
        if expect_amount:
 
            actual_account, actual_amount = next(actual)
 
            assert actual_account == expect_account
 
            assert actual_amount == format_amount(expect_amount)
 
            balance += expect_amount
 
    return balance
 

	
 
def check_text_report(output, project, start_date, stop_date):
 
    _, _, project = project.rpartition('=')
 
    balance_amount = Decimal(OPENING_BALANCES[project])
...
 
@@ -105,11 +118,10 @@ def check_text_report(output, project, start_date, stop_date):
 
            pass
 
        else:
 
            for account, amount in amounts:
 
                if year < start_date.year:
 
                if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
 
                    balance_amount += amount
 
                else:
 
                    expected[account] += amount
 
    expected.default_factory = None
 
    actual = split_text_lines(output)
 
    next(actual); next(actual)  # Discard headers
 
    open_acct, open_amt = next(actual)
...
 
@@ -117,25 +129,24 @@ def check_text_report(output, project, start_date, stop_date):
 
        project, start_date.isoformat(),
 
    )
 
    assert open_amt == format_amount(balance_amount)
 
    for expect_account in [
 
            'Equity:Realized:CurrencyConversion',
 
            'Income:Other',
 
            'Expenses:Other',
 
    ]:
 
        try:
 
            expect_amount = expected[expect_account]
 
        except KeyError:
 
            continue
 
        else:
 
            actual_account, actual_amount = next(actual)
 
            assert actual_account == expect_account
 
            assert actual_amount == format_amount(expect_amount)
 
            balance_amount += expect_amount
 
    balance_amount += check_text_balances(
 
        actual, expected,
 
        'Equity:Realized:CurrencyConversion',
 
        'Income:Other',
 
        'Expenses:Other',
 
    )
 
    end_acct, end_amt = next(actual)
 
    assert end_acct == "{} balance as of {}".format(
 
        project, stop_date.isoformat(),
 
    )
 
    assert end_amt == format_amount(balance_amount)
 
    balance_amount += check_text_balances(
 
        actual, expected,
 
        'Assets:Receivable:Accounts',
 
        'Assets:Prepaid:Expenses',
 
        'Liabilities:Payable:Accounts',
 
        'Liabilities:UnearnedIncome',
 
    )
 
    assert next(actual, None) is None
 

	
 
def check_ods_report(ods, start_date, stop_date):
...
 
@@ -143,7 +154,11 @@ def check_ods_report(ods, start_date, stop_date):
 
        'opening': Decimal(amount),
 
        'Income': Decimal(0),
 
        'Expenses': Decimal(0),
 
        'Equity': Decimal(0),
 
        'Equity:Realized': Decimal(0),
 
        'Assets:Receivable': Decimal(0),
 
        'Assets:Prepaid': Decimal(0),
 
        'Liabilities:Payable': Decimal(0),
 
        'Liabilities': Decimal(0),  # UnearnedIncome
 
    }) for key, amount in sorted(OPENING_BALANCES.items()))
 
    for fund, year in itertools.product(account_bals, range(2018, stop_date.year)):
 
        try:
...
 
@@ -152,10 +167,10 @@ def check_ods_report(ods, start_date, stop_date):
 
            pass
 
        else:
 
            for account, amount in amounts:
 
                if year < start_date.year:
 
                if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
 
                    acct_key = 'opening'
 
                else:
 
                    acct_key, _, _ = account.partition(':')
 
                    acct_key, _, _ = account.rpartition(':')
 
                account_bals[fund][acct_key] += amount
 
    account_bals['Unrestricted'] = account_bals.pop('Conservancy')
 
    for row in ods.getElementsByType(odf.table.TableRow):
...
 
@@ -169,11 +184,13 @@ def check_ods_report(ods, start_date, stop_date):
 
            assert next(cells).value == balances['opening']
 
            assert next(cells).value == balances['Income']
 
            assert next(cells).value == -balances['Expenses']
 
            if balances['Equity']:
 
                assert next(cells).value == balances['Equity']
 
            if balances['Equity:Realized']:
 
                assert next(cells).value == balances['Equity:Realized']
 
            else:
 
                assert not next(cells).value
 
            assert next(cells).value == sum(balances.values())
 
            assert next(cells).value == sum(balances[key] for key in [
 
                'opening', 'Income', 'Expenses', 'Equity:Realized',
 
            ])
 
    assert not account_bals, "did not see all funds in report"
 

	
 
def run_main(out_type, arglist, config=None):
0 comments (0 inline, 0 general)