diff --git a/config.example.yml b/config.example.yml index 360dda7..574f4bc 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,7 +1,7 @@ # Set up how we will call Ledger ledger_file: /path/to/ledger.journal ledger_args: ['--pedantic', '--recursive-aliases'] -report_currency: $ +report_currency: ['$', True] # True if prefix, False if suffix # Tell ledger-pyreport about the top-level account categories assets_account: Assets diff --git a/ledger_pyreport/__init__.py b/ledger_pyreport/__init__.py index 692eb4b..97f7ee4 100644 --- a/ledger_pyreport/__init__.py +++ b/ledger_pyreport/__init__.py @@ -16,6 +16,8 @@ from . import accounting from . import ledger +from .config import config +from .model import * from datetime import datetime, timedelta from decimal import Decimal @@ -39,36 +41,45 @@ def trial(): if compare == 0: # Get trial balance - trial_balance = ledger.trial_balance(date, pstart) + l = ledger.raw_transactions_at_date(date) + trial_balance = accounting.trial_balance(l, date, pstart) - total_dr = Decimal(0) - total_cr = Decimal(0) + report_currency = Currency(*config['report_currency']) + trial_balance = accounting.add_unrealized_gains(trial_balance, report_currency) - for account in trial_balance.accounts.values(): - balance = trial_balance.get_balance(account.name) + total_dr = Amount(0, report_currency) + total_cr = Amount(0, report_currency) + + for account in l.accounts.values(): + # Display in "cost basis" as we have already accounted for unrealised gains + balance = trial_balance.get_balance(account).exchange(report_currency, True) if balance > 0: total_dr += balance else: total_cr -= balance - return flask.render_template('trial.html', trial_balance=trial_balance, total_dr=total_dr, total_cr=total_cr) + return flask.render_template('trial.html', date=date, pstart=pstart, trial_balance=trial_balance, accounts=sorted(l.accounts.values(), key=lambda a: a.name), total_dr=total_dr, total_cr=total_cr, report_currency=report_currency) else: # Get multiple trial balances for comparison 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)] - trial_balances = [ledger.trial_balance(d, p) for d, p in zip(dates, pstarts)] + report_currency = Currency(*config['report_currency']) + l = ledger.raw_transactions_at_date(date) + 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 - accounts = list(trial_balances[0].accounts.values()) + accounts = list(trial_balances[0].ledger.accounts.values()) for account in accounts[:]: - if all(t.get_balance(account.name) == 0 for t in trial_balances): + 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) + return flask.render_template('trial_multiple.html', trial_balances=trial_balances, accounts=sorted(accounts, key=lambda a: a.name), report_currency=report_currency) @app.route('/balance') def balance(): + raise Exception('NYI') + 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']) @@ -89,6 +100,8 @@ def balance(): @app.route('/pandl') def pandl(): + raise Exception('NYI') + 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']) @@ -118,36 +131,45 @@ def pandl(): 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) - trial_balance_pstart = ledger.trial_balance(pstart, pstart) - account = trial_balance_pstart.get_account(flask.request.args['account']) - opening_balance = trial_balance_pstart.get_balance(account.name) + # General ledger + l = ledger.raw_transactions_at_date(date) - balance = opening_balance - transactions = account.get_transactions(date, pstart) - for transaction in transactions: - for posting in transaction.postings[:]: - if posting.account == account.name: - transaction.postings.remove(posting) - else: - posting.amount = -posting.amount # In terms of effect on this account - balance += posting.amount - posting.balance = balance + # Unrealized gains + report_currency = Currency(*config['report_currency']) + l = accounting.add_unrealized_gains(accounting.trial_balance(l, date, pstart), report_currency).ledger - trial_balance = ledger.trial_balance(date, pstart) - closing_balance = trial_balance.get_balance(account.name) - - return flask.render_template('transactions.html', date=date, pstart=pstart, account=account, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance) + if not account: + transactions = [t for t in l.transactions if t.date <= date and t.date >= pstart] + + 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) + 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)] + + 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, account=account, ledger=l, transactions=transactions, opening_balance=opening_balance, closing_balance=closing_balance, report_currency=report_currency) @app.template_filter('a') def filter_amount(amt): - if amt < 0.005 and amt >= -0.005: + if amt.amount < 0.005 and amt.amount >= -0.005: return flask.Markup('0.00 ') elif amt > 0: - return flask.Markup('{:,.2f} '.format(amt).replace(',', ' ')) # Narrow no-break space + return flask.Markup('{:,.2f} '.format(amt.amount).replace(',', ' ')) # Narrow no-break space else: - return flask.Markup('({:,.2f})'.format(-amt).replace(',', ' ')) + return flask.Markup('({:,.2f})'.format(-amt.amount).replace(',', ' ')) @app.template_filter('b') def filter_amount_positive(amt): - return flask.Markup('{:,.2f}'.format(amt).replace(',', ' ')) + return flask.Markup('{:,.2f}'.format(amt.amount).replace(',', ' ')) + #return flask.Markup('{:,.2f} {}'.format(amt.amount, amt.currency.name).replace(',', ' ')) + +@app.template_filter('bb') +def filter_balance_positive(balance): + return flask.Markup('
'.join(filter_amount_positive(a) for a in balance.amounts)) diff --git a/ledger_pyreport/accounting.py b/ledger_pyreport/accounting.py index 3eac578..468f124 100644 --- a/ledger_pyreport/accounting.py +++ b/ledger_pyreport/accounting.py @@ -17,18 +17,36 @@ import csv from decimal import Decimal -from . import ledger +from .model import * -# Generate balance sheet +def add_unrealized_gains(tb, currency): + for account in list(tb.ledger.accounts.values()): + if not account.is_market: + continue + + total_cost = tb.get_balance(account).exchange(currency, True) + total_market = tb.get_balance(account).exchange(currency, False, tb.date, tb.ledger) + unrealized_gain = total_market - total_cost + + if unrealized_gain != 0: + transaction = Transaction(tb.ledger, None, tb.date, '') + transaction.postings.append(Posting(transaction, account, unrealized_gain)) + transaction.postings.append(Posting(transaction, tb.ledger.get_account(config['unrealized_gains']), -unrealized_gain)) + tb.ledger.transactions.append(transaction) + + return trial_balance(tb.ledger, tb.date, tb.pstart) -def balance_sheet(date, pstart): - # Get trial balance - trial_balance = ledger.trial_balance(date, pstart) +def trial_balance(ledger, date, pstart): + tb = TrialBalance(ledger, date, pstart) - # Calculate Profit/Loss - total_pandl = trial_balance.get_total(ledger.config['income_account']) + trial_balance.get_total(ledger.config['expenses_account']) + for transaction in ledger.transactions: + if transaction.date > date: + continue + + for posting in transaction.postings: + if (posting.account.is_income or posting.account.is_expense) and transaction.date < pstart: + tb.balances[config['retained_earnings']] = tb.get_balance(ledger.get_account(config['retained_earnings'])) + posting.amount + else: + tb.balances[posting.account.name] = tb.get_balance(posting.account) + posting.amount - # Add Current Year Earnings account - trial_balance.set_balance(ledger.config['current_year_earnings'], trial_balance.get_balance(ledger.config['current_year_earnings']) + total_pandl) - - return trial_balance + return tb diff --git a/ledger_pyreport/config.py b/ledger_pyreport/config.py new file mode 100644 index 0000000..0e6cb8c --- /dev/null +++ b/ledger_pyreport/config.py @@ -0,0 +1,20 @@ +# ledger-pyreport +# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import yaml + +with open('config.yml', 'r') as f: + config = yaml.safe_load(f) diff --git a/ledger_pyreport/jinja2/index.html b/ledger_pyreport/jinja2/index.html index 8f5d6ae..54768c4 100644 --- a/ledger_pyreport/jinja2/index.html +++ b/ledger_pyreport/jinja2/index.html @@ -47,6 +47,13 @@ {##} + +
  • + + + + {##} +