diff --git a/README.md b/README.md index 6731cf4..a3be701 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ ledger-pyreport is a lightweight Flask webapp for generating interactive and pri * Accounts for both profit and loss, and other comprehensive income * Simulates annual closing of books, with presentation of income/expenses on the balance sheet as retained earnings and current year earnings * Can simulate cash basis accounting, using FIFO methodology to recode transactions involving liabilities and non-cash assets +* Can separately report specified categories of income and expense, reporting per-category net profit * Extensible through custom programming hooks ## Background, demo and screenshots diff --git a/config.example.yml b/config.example.yml index 5076372..919427f 100644 --- a/config.example.yml +++ b/config.example.yml @@ -11,6 +11,9 @@ income_account: Income expenses_account: Expenses oci_account: OCI # Other Comprehensive Income +# These income and expense categories appear separately on the income statement +separate_pandl: [] + # These accounts will automatically be populated on reports unrealized_gains: 'OCI:Unrealized Gains' diff --git a/demo/config.yml b/demo/config.yml index f20804d..6d34118 100644 --- a/demo/config.yml +++ b/demo/config.yml @@ -11,6 +11,9 @@ income_account: Income expenses_account: Expenses oci_account: OCI # Other Comprehensive Income +# These income and expense categories appear separately on the income statement +separate_pandl: ['Business'] + # These accounts will automatically be populated on reports unrealized_gains: 'OCI:Unrealized Gains' diff --git a/demo/ledger.journal b/demo/ledger.journal index eaf15a9..c2069a2 100644 --- a/demo/ledger.journal +++ b/demo/ledger.journal @@ -17,6 +17,14 @@ Assets:Current:Inventory 50 Widgets {$7.00} Assets:Current:Cash at Bank +2019-07-04 Sale + Assets:Current:Cash at Bank $100 + Income:Business:Sales + +2019-07-04 Sale + Expenses:Business:Cost of Goods Sold 10 Widgets {$5.00} + Assets:Current:Inventory + 2019-08-01 Interest on business loan Expenses:Interest $100.00 Liabilities:Non-current:Business Loan diff --git a/ledger_pyreport/__init__.py b/ledger_pyreport/__init__.py index 212b7a7..289a24f 100644 --- a/ledger_pyreport/__init__.py +++ b/ledger_pyreport/__init__.py @@ -136,13 +136,35 @@ def pandl(): l = accounting.ledger_to_cash(l, report_commodity) pandls = [accounting.trial_balance(l.clone(), de, db, report_commodity) for de, db in zip(dates_end, dates_beg)] + # Process separate P&L accounts + separate_pandls = [] + for separate_pandl_name in config['separate_pandl']: + acc_income = l.get_account(config['income_account'] + ':' + separate_pandl_name) + acc_expenses = l.get_account(config['expenses_account'] + ':' + separate_pandl_name) + separate_pandls.append((acc_income, acc_expenses)) + + # Unlink from parents so raw figures not counted in income/expense total + acc_income.parent.children.remove(acc_income) + acc_expenses.parent.children.remove(acc_expenses) + + # Add summary account + for i, de, db in zip(range(compare + 1), dates_end, dates_beg): + balance = (pandls[i].get_total(acc_income) + pandls[i].get_total(acc_expenses)).exchange(report_commodity, True) + + if balance <= 0: # Credit + summary_account = l.get_account(config['income_account'] + ':' + separate_pandl_name + ' Profit') + else: + summary_account = l.get_account(config['expenses_account'] + ':' + separate_pandl_name + ' Loss') + + pandls[i].balances[summary_account.name] = pandls[i].get_balance(summary_account) + balance + # Delete accounts with always zero balances accounts = list(l.accounts.values()) for account in accounts[:]: if all(p.get_balance(account) == 0 and p.get_total(account) == 0 for p in pandls): accounts.remove(account) - return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, config=config, report_commodity=report_commodity, cash=cash, scope=scope) + return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, separate_pandls=separate_pandls, config=config, report_commodity=report_commodity, cash=cash, scope=scope) @app.route('/cashflow') def cashflow(): diff --git a/ledger_pyreport/jinja2/pandl.html b/ledger_pyreport/jinja2/pandl.html index 6e47196..05d835f 100644 --- a/ledger_pyreport/jinja2/pandl.html +++ b/ledger_pyreport/jinja2/pandl.html @@ -68,7 +68,55 @@ {# Profit and loss #} {% if scope != 'oci' %} - {{ do_accounts(ledger.get_account(config['income_account']), 'Income', True, True) }} + {# Separate P&L accounts #} + {% for acc_income, acc_expenses in separate_pandls %} + + + {# Income #} + + + {% if loop.first %} + + {% for pandl in pandls %}{% endfor %} + {% else %} + + {% endif %} + + + {% for account in acc_income.children|sort(attribute='name') if account in accounts %} + {{ print_rows(account, invert=True) }} + {% endfor %} + + + + {% for pandl in pandls %}{% endfor %} + + + + {# Expenses #} + + + + {% for account in acc_expenses.children|sort(attribute='name') if account in accounts %} + {{ print_rows(account) }} + {% endfor %} + + + + {% for pandl in pandls %}{% endfor %} + + + + {# Net Profit #} + + + + {% for pandl in pandls %}{% endfor %} + + + {% endfor %} + + {{ do_accounts(ledger.get_account(config['income_account']), 'Income', True, separate_pandls|length == 0) }} {{ do_accounts(ledger.get_account(config['expenses_account']), 'Expenses', False, False) }}
{{ acc_income.bits[-1] }} Account
{{ acc_income.bits[-1] }} Income{{ pandl.date.strftime('%Y') }} {{ acc_income.bits[-1] }} Income
Total {{ acc_income.bits[-1] }} Income{{ -pandl.get_total(acc_income).exchange(report_commodity, True)|a }}
 
{{ acc_income.bits[-1] }} Expenses
Total {{ acc_income.bits[-1] }} Expenses{{ pandl.get_total(acc_expenses).exchange(report_commodity, True)|a }}
 
{{ acc_income.bits[-1] }} Profit (Loss){{ -(pandl.get_total(acc_income) + pandl.get_total(acc_expenses)).exchange(report_commodity, True)|a }}