diff --git a/config.example.yml b/config.example.yml index 574f4bc..f7293a9 100644 --- a/config.example.yml +++ b/config.example.yml @@ -17,3 +17,5 @@ current_year_earnings: 'Equity:Current Year Earnings' # Which asset accounts to consider in cash basis mode cash_asset_accounts: ['Assets:Cash on Hand', 'Assets:Cash at Bank'] +# Which account to charge non-cash transactions to in cash basis mode, when no other account is suitable +cash_other_income: 'Income:Other Income' diff --git a/ledger_pyreport/__init__.py b/ledger_pyreport/__init__.py index d34464d..dd47fce 100644 --- a/ledger_pyreport/__init__.py +++ b/ledger_pyreport/__init__.py @@ -37,14 +37,17 @@ def trial(): date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d') pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d') compare = int(flask.request.args['compare']) - #cash = flask.request.args.get('cash', False) + cash = flask.request.args.get('cash', False) + + report_currency = Currency(*config['report_currency']) if compare == 0: # Get trial balance l = ledger.raw_transactions_at_date(date) + if cash: + l = accounting.cash_basis(l, report_currency) trial_balance = accounting.trial_balance(l, date, pstart) - report_currency = Currency(*config['report_currency']) trial_balance = accounting.add_unrealized_gains(trial_balance, report_currency) total_dr = Amount(0, report_currency) @@ -64,8 +67,9 @@ def trial(): dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)] pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)] - report_currency = Currency(*config['report_currency']) l = ledger.raw_transactions_at_date(date) + if cash: + l = accounting.cash_basis(l, report_currency) trial_balances = [accounting.add_unrealized_gains(accounting.trial_balance(l, d, p), report_currency) for d, p in zip(dates, pstarts)] # Delete accounts with always zero balances @@ -74,20 +78,22 @@ def trial(): if all(t.get_balance(account) == 0 for t in trial_balances): accounts.remove(account) - return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=accounts, report_currency=report_currency) + return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=accounts, report_currency=report_currency, cash=cash) @app.route('/balance') def balance(): date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d') pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d') compare = int(flask.request.args['compare']) - #cash = flask.request.args.get('cash', False) + cash = flask.request.args.get('cash', False) dates = [date.replace(year=date.year - i) for i in range(0, compare + 1)] pstarts = [pstart.replace(year=pstart.year - i) for i in range(0, compare + 1)] report_currency = Currency(*config['report_currency']) l = ledger.raw_transactions_at_date(date) + if cash: + l = accounting.cash_basis(l, report_currency) balance_sheets = [accounting.balance_sheet(accounting.add_unrealized_gains(accounting.trial_balance(l, d, p), report_currency)) for d, p in zip(dates, pstarts)] # Delete accounts with always zero balances @@ -96,7 +102,7 @@ def balance(): if all(b.get_balance(account) == 0 and b.get_total(account) == 0 for b in balance_sheets): accounts.remove(account) - return flask.render_template('balance.html', ledger=l, balance_sheets=balance_sheets, accounts=accounts, config=config, report_currency=report_currency) + return flask.render_template('balance.html', ledger=l, balance_sheets=balance_sheets, accounts=accounts, config=config, report_currency=report_currency, cash=cash) def describe_period(date_end, date_beg): if date_end == (date_beg.replace(year=date_beg.year + 1) - timedelta(days=1)): @@ -111,13 +117,15 @@ def pandl(): date_beg = datetime.strptime(flask.request.args['date_beg'], '%Y-%m-%d') date_end = datetime.strptime(flask.request.args['date_end'], '%Y-%m-%d') compare = int(flask.request.args['compare']) - #cash = flask.request.args.get('cash', False) + cash = flask.request.args.get('cash', False) dates_beg = [date_beg.replace(year=date_beg.year - i) for i in range(0, compare + 1)] dates_end = [date_end.replace(year=date_end.year - i) for i in range(0, compare + 1)] report_currency = Currency(*config['report_currency']) l = ledger.raw_transactions_at_date(date_end) + if cash: + l = accounting.cash_basis(l, report_currency) pandls = [accounting.trial_balance(l, de, db) for de, db in zip(dates_end, dates_beg)] # Delete accounts with always zero balances @@ -126,19 +134,23 @@ def pandl(): 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_currency=report_currency) + return flask.render_template('pandl.html', period=describe_period(date_end, date_beg), ledger=l, pandls=pandls, accounts=accounts, config=config, report_currency=report_currency, cash=cash) @app.route('/transactions') def transactions(): date = datetime.strptime(flask.request.args['date'], '%Y-%m-%d') pstart = datetime.strptime(flask.request.args['pstart'], '%Y-%m-%d') account = flask.request.args.get('account', None) + cash = flask.request.args.get('cash', False) + + report_currency = Currency(*config['report_currency']) # General ledger l = ledger.raw_transactions_at_date(date) + if cash: + l = accounting.cash_basis(l, report_currency) # Unrealized gains - report_currency = Currency(*config['report_currency']) l = accounting.add_unrealized_gains(accounting.trial_balance(l, date, pstart), report_currency).ledger if not account: @@ -147,7 +159,7 @@ def transactions(): total_dr = sum((p.amount for t in transactions for p in t.postings if p.amount > 0), Balance()).exchange(report_currency, True) total_cr = sum((p.amount for t in transactions for p in t.postings if p.amount < 0), Balance()).exchange(report_currency, True) - return flask.render_template('transactions.html', date=date, pstart=pstart, account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency) + return flask.render_template('transactions.html', date=date, pstart=pstart, account=None, ledger=l, transactions=transactions, total_dr=total_dr, total_cr=total_cr, report_currency=report_currency, cash=cash) else: account = l.get_account(account) transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart and any(p.account == account for p in t.postings)] @@ -155,7 +167,7 @@ def transactions(): opening_balance = accounting.trial_balance(l, pstart, pstart).get_balance(account).exchange(report_currency, True) closing_balance = accounting.trial_balance(l, date, pstart).get_balance(account).exchange(report_currency, True) - return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, timedelta=timedelta) + return flask.render_template('transactions.html', date=date, pstart=pstart, period=describe_period(date, pstart), account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency, cash=cash, timedelta=timedelta) @app.template_filter('a') def filter_amount(amt): diff --git a/ledger_pyreport/accounting.py b/ledger_pyreport/accounting.py index fe7ad9e..3a51169 100644 --- a/ledger_pyreport/accounting.py +++ b/ledger_pyreport/accounting.py @@ -19,6 +19,8 @@ from decimal import Decimal from .model import * +# Generate a trial balance +# Perform closing of books based on specified dates def trial_balance(ledger, date, pstart): tb = TrialBalance(ledger, date, pstart) @@ -34,6 +36,7 @@ def trial_balance(ledger, date, pstart): return tb +# Adjust (in place) a trial balance for unrealized gains def add_unrealized_gains(tb, currency): for account in list(tb.ledger.accounts.values()): if not account.is_market: @@ -51,6 +54,8 @@ def add_unrealized_gains(tb, currency): return trial_balance(tb.ledger, tb.date, tb.pstart) +# Adjust (in place) a trial balance to include a Current Year Earnings account +# Suitable for display on a balance sheet def balance_sheet(tb): # Calculate Profit/Loss total_pandl = tb.get_total(tb.ledger.get_account(config['income_account'])) + tb.get_total(tb.ledger.get_account(config['expenses_account'])) @@ -59,3 +64,28 @@ def balance_sheet(tb): tb.balances[config['current_year_earnings']] = tb.get_balance(tb.ledger.get_account(config['current_year_earnings'])) + total_pandl return tb + +# Adjust (in place) a ledger to convert accounting to a cash basis +def cash_basis(ledger, currency): + for transaction in ledger.transactions: + non_cash_postings = [p for p in transaction.postings if not (p.account.is_cash or p.account.is_income or p.account.is_expense or p.account.is_equity)] + + if non_cash_postings: + # We have liabilities or non-cash assets which need to be excluded + + cash_postings = [p for p in transaction.postings if p.account.is_income or p.account.is_expense or p.account.is_equity] + cash_total = sum((p.amount for p in cash_postings), Balance()).exchange(currency, True).amount + + if cash_postings: + for posting in non_cash_postings: + posting_amount = posting.amount.exchange(currency, True).amount + for posting_xfer in cash_postings: + posting_xfer_amount = posting_xfer.amount.exchange(currency, True).amount + transaction.postings.append(Posting(transaction, posting_xfer.account, Amount(posting_amount * posting_xfer_amount / cash_total, currency))) + + transaction.postings.remove(posting) + else: + for posting in non_cash_postings: + posting.account = ledger.get_account(config['cash_other_income']) + + return ledger diff --git a/ledger_pyreport/jinja2/balance.html b/ledger_pyreport/jinja2/balance.html index 3444223..fc88f41 100644 --- a/ledger_pyreport/jinja2/balance.html +++ b/ledger_pyreport/jinja2/balance.html @@ -23,9 +23,9 @@ {% if balance_sheets|length == 1 %} {% if account.name == config['current_year_earnings'] %} - + {% else %} - + {% endif %} {{ account.bits[-1] }} @@ -38,9 +38,9 @@ {% if amount != 0 %} {% if account.name == config['current_year_earnings'] %} - + {% else %} - + {% endif %} {{ amount|a }} @@ -95,9 +95,11 @@   {# Liabilities #} - Liabilities - {{ do_accounts(ledger.get_account(config['liabilities_account']), 'Liabilities', True, False) }} -   + {% if not cash %} + Liabilities + {{ do_accounts(ledger.get_account(config['liabilities_account']), 'Liabilities', True, False) }} +   + {% endif %} {# Equity #} Equity diff --git a/ledger_pyreport/jinja2/index.html b/ledger_pyreport/jinja2/index.html index 0eea14b..1200890 100644 --- a/ledger_pyreport/jinja2/index.html +++ b/ledger_pyreport/jinja2/index.html @@ -27,7 +27,7 @@ - {##} +
  • @@ -35,7 +35,7 @@ - {##} +
  • @@ -43,14 +43,14 @@ - {##} +
  • - {##} +
  • diff --git a/ledger_pyreport/jinja2/pandl.html b/ledger_pyreport/jinja2/pandl.html index 5ca3f13..1f14fef 100644 --- a/ledger_pyreport/jinja2/pandl.html +++ b/ledger_pyreport/jinja2/pandl.html @@ -22,14 +22,14 @@ {% if pandls|length == 1 %} - {{ account.bits[-1] }} + {{ account.bits[-1] }} {% else %} {{ account.bits[-1] }} {% endif %} {% for pandl in pandls %} {% set amount = (-pandl.get_balance(account) if invert else pandl.get_balance(account)).exchange(report_currency, True) %} - {% if amount != 0 %}{{ amount|a }}{% endif %} + {% if amount != 0 %}{{ amount|a }}{% endif %} {% endfor %} diff --git a/ledger_pyreport/jinja2/transactions.html b/ledger_pyreport/jinja2/transactions.html index f5d5495..1fbcd54 100644 --- a/ledger_pyreport/jinja2/transactions.html +++ b/ledger_pyreport/jinja2/transactions.html @@ -43,12 +43,12 @@ {% if account %} {{ pstart.strftime('%Y-%m-%d') }} - Opening Balance + Opening Balance - + {% if opening_balance >= 0 %} {{ opening_balance|b }} Dr {% else %} @@ -66,7 +66,7 @@ {% if loop.first %}{{ transaction.date.strftime('%Y-%m-%d') }}{% endif %} {% if loop.first %}{{ transaction.description }}{% endif %} - {{ (posting.account.name|e).__str__().replace(':', ':')|safe }} + {{ (posting.account.name|e).__str__().replace(':', ':')|safe }} {% if account %} {# Reverse Dr/Cr so it's from the "perspective" of this account #} {% set ns.balance = ns.balance - amount %} diff --git a/ledger_pyreport/jinja2/trial.html b/ledger_pyreport/jinja2/trial.html index bec3ce8..e325e85 100644 --- a/ledger_pyreport/jinja2/trial.html +++ b/ledger_pyreport/jinja2/trial.html @@ -33,7 +33,7 @@ {% for account in accounts %} {# Display in "cost basis" as we have already accounted for unrealised gains #} {% set balance = trial_balance.get_balance(account).exchange(report_currency, True) %} - {% set trn_url = "/transactions?" + {'date': trial_balance.date.strftime('%Y-%m-%d'), 'pstart': trial_balance.pstart.strftime('%Y-%m-%d'), 'account': account.name}|urlencode %} + {% set trn_url = "/transactions?" + {'date': trial_balance.date.strftime('%Y-%m-%d'), 'pstart': trial_balance.pstart.strftime('%Y-%m-%d'), 'account': account.name, 'cash': 'on' if cash else ''}|urlencode %} {% if balance != 0 %} {{ account.name }} diff --git a/ledger_pyreport/jinja2/trial_multiple.html b/ledger_pyreport/jinja2/trial_multiple.html index a572d69..be365e5 100644 --- a/ledger_pyreport/jinja2/trial_multiple.html +++ b/ledger_pyreport/jinja2/trial_multiple.html @@ -34,7 +34,7 @@ {{ account.name }} {% for trial_balance in trial_balances %} {% set balance = trial_balance.get_balance(account).exchange(report_currency, True) %} - {% if balance != 0 %}{{ balance|a }}{% endif %} + {% if balance != 0 %}{{ balance|a }}{% endif %} {% endfor %} {% endfor %} diff --git a/ledger_pyreport/model.py b/ledger_pyreport/model.py index fa2d9b2..2112978 100644 --- a/ledger_pyreport/model.py +++ b/ledger_pyreport/model.py @@ -123,6 +123,10 @@ class Account: @property def is_liability(self): return self.matches(config['liabilities_account']) + @property + def is_cash(self): + # Is this a cash asset? + return any(self.matches(a) for a in config['cash_asset_accounts']) @property def is_cost(self):