Andy Mikhailenko avatar Andy Mikhailenko committed 667d554

Added dashboard, automated currency converter, charts and a whole lot of stuff to the extension "finances" (bulk commit; some changes are *very* old already). Demo quality.

Comments (0)

Files changed (6)

orgtool/ext/finances/__init__.py

 # -*- coding: utf-8 -*-
-
+"""
+Finances
+========
+"""
+from tool.plugins import BasePlugin
 from tool.ext.templating import register_templates
 from schema import *
 from views import *
 import admin
 
 
-register_templates(__name__)
+class WebMoneyTracker(BasePlugin):
+    """Money tracker: web interface.
+    """
+    features = 'money'
+    requires = ['{templating}', '{routing}']
+
+    def make_env(self, default_currency='EUR'):
+        register_templates(__name__)
+        return {'default_currency': default_currency}

orgtool/ext/finances/schema.py

 # -*- coding: utf-8 -*-
 import datetime
-from decimal import Decimal
+import decimal
+Decimal = decimal.Decimal    # we need the module, too
+import logging
 from werkzeug import cached_property
-from docu import Document, Field as f
+import dark
+from doqu import Document, Field as f
 from orgtool.ext.events import Event, Plan
 from orgtool.ext.events.admin import PlanAdmin
+from orgtool.ext.tracking import TrackedDocument
 from tool.ext import admin
 
+import utils
+
+logger = logging.getLogger('orgtool.ext.finances')
+
+
+__all__ = ['Contract', 'Payment', 'CurrencyRate']
+
 
 NAMESPACE = 'finances'
 
 
+def utc_today():
+    "Returns the UTC equivalent for datetime.date.today()."
+    return datetime.datetime.utcnow().date()
+
+
 class Contract(Plan):
     """
     An agreement between stakeholders (2..n) that a certain service will be
     #next_payment = f(datetime.date)  # если дата в прошлом, надо подтвердить
     stakeholder = f(unicode)  # "U-tel". Can be m2m but later  (agent)
     service_id = f(unicode)   # contract number, phone number, login, etc.
+    is_automated = f(bool)    # e.g. bank unconditionally withdraws money from
+                              # the account according to an agreement
 
     def __unicode__(self):
         return u'{summary}'.format(**self)
 #               list_names=['summary', 'fee', 'currency'])
 
     def get_events(self):
+        if not self.pk:
+            return None
         payments = Payment.objects(self._saved_state.storage)
         return payments.where(plan=self).order_by('date_time', reverse=True)
 
             return Decimal(0)
         if not self.valid_since and not self.valid_until:
             return Decimal(0)
-        since = self.valid_since or datetime.date.today()
+        since = self.valid_since or utc_today()
         until = self.valid_until or self.next_payment_date #datetime.date.today()
         assert since < until
         delta = until - since
 
     @cached_property
     def actual_daily_fee(self):
-        print '---'
-        print 'DAILY FEE FOR', self
+        logger.debug('CALCULATING DAILY FEE FOR {0}'.format(self))
         if not self.payments:
             return Decimal('0')
 
         # XXX assuming that payments are sorted by date REVERSED
         first, last = self.payments[-1], self.payments[0]
 
-        print 'first:', first
-        print 'last:', last
+        logger.debug('first: {first}, last: {last}'.format(**locals()))
 
         if self.valid_since and self.valid_until:
-            print 'fixed start, end'
+            logger.debug('fixed start, end')
             delta = self.valid_until - self.valid_since
         elif self.valid_since:
-            print 'fixed start'
+            logger.debug('fixed start')
             delta = last.date_time.date() - self.valid_since
         elif self.valid_until:
-            print 'fixed end'
+            logger.debug('fixed end')
             delta = self.valid_until - first.date_time.date()
         else:
             if first == last:
-                print 'first is last'
+                logger.debug('first is last')
                 #return self.actual_total_fee
-                delta = datetime.datetime.now() - first.date_time
+                delta = datetime.datetime.utcnow() - first.date_time
             else:
-                print 'last - first'
+                logger.debug('last - first')
                 delta = last.date_time - first.date_time
 
-        print 'delta:', delta
+        logger.debug('delta: {0}'.format(delta))
 
         if delta and delta.days:
-            print 'daily fee = {0} {1} / {2} days'.format(self.actual_total_fee,
-                                            self.currency, delta.days)
+            logger.debug('daily fee = {0} {1} / {2} days'.format(
+                self.actual_total_fee, self.currency, delta.days))
             return (self.actual_total_fee / delta.days).quantize(Decimal('0.01'))
         else:
             return self.actual_total_fee
     def actual_total_fee(self):
         return sum(p.amount for p in self.payments)
 
+    @cached_property
+    def expected_payment_amount(self):
+        """
+        Returns expected amount for the next payment, based on the payments
+        history. Uses qu0.75 (median value of the last quarter of payments
+        history).
 
-@admin.register_for(Contract)
-class ContractAdmin(PlanAdmin):
-    namespace = NAMESPACE
-    list_names = [
-        'summary', 'dates_rrule_text', 'fee', 'total_fee', 'currency'
-    ]
-    order_by = 'summary'
+        .. note::
+
+            The aggregation method will produce more or less accurate results
+            only for homogenous history of regular payments. It will fail on
+            mixed data because it uses median value instead of average. You'll
+            need separate plans for each type of payments. For example, if
+            there are two kinds of payments within a certain plan (monthly fee
+            and annual refunds), you'll need to split them into "Service ABC:
+            Fee" and "Service ABD: Refunds" in order to have accurate
+            predictions for each of them.
+
+        """
+        if self.is_fee_fixed:
+            return self.fee
+        else:
+            return dark.Qu3('amount').count_for(self.events)
+
+    def get_expected_payment_amount_as(self, currency=None):
+        currency = currency or utils.get_default_currency()
+        amount_str = str(self.expected_payment_amount)
+        try:
+            amount = Decimal(amount_str)
+        except decimal.InvalidOperation:
+            # e.g. 'N/A' as returned by Dark aggregators in some cases
+            return 0
+        if amount == 0:
+            return 0
+        db = self._saved_state.storage
+        x = CurrencyRate.convert(db, self.currency, currency, amount)
+        return x.quantize(Decimal('0.01'))
 
 
 '''
     #summary = f(unicode)  # additional info
     amount = f(Decimal, required=True)
     currency = f(unicode, default=lambda p: p.plan.currency)
-    #logged = f(datetime.datetime, default=datetime.datetime.now)
+    #logged = f(datetime.datetime, default=datetime.datetime.utcnow)
     balance = f(Decimal, label=u'account balance after the payment')
 
     defaults = {'summary': u'payment'}
             return u'{date_time} {amount} {currency} for {plan}'.format(**self)
         return u'{date_time} {amount} {currency}'.format(**self)
 
-#    def save(self, *args, **kw):
-#        self
-#        return super(Payment, self).save(*args, **kwargs)
-admin.register(Payment, namespace=NAMESPACE,
-               list_names=['date_time', 'plan', 'amount', 'currency', 'summary'],
-               ordering={'names': ['date_time'], 'reverse': True})
+    def get_amount_as(self, currency=None):
+        """
+        Returns amount converted to given currency.
+
+        :param currency:
+            Currency name, e.g. "EUR" or "USD". If `None`, bundle setting
+            ``default_currency` if used.
+        """
+        currency = currency or utils.get_default_currency()
+        if self.amount == 0:
+            return 0
+        db = self._saved_state.storage
+        return CurrencyRate.convert(db, self.currency, currency, self.amount)
+
+
+class CurrencyRate(TrackedDocument):
+    from_currency = f(unicode, required=True)
+    to_currency = f(unicode, required=True)
+    rate = f(Decimal, required=True)
+    date = f(datetime.date, default=utc_today, required=True)
+
+    _cache = {}
+    _cache_date = utc_today()
+
+    def __unicode__(self):
+        return u'1 {from_currency} = {rate} {to_currency}'.format(**self)
+
+    def save(self, *args, **kwargs):
+        self.date = utc_today()   # reset, force today
+        return super(CurrencyRate, self).save(*args, **kwargs)
+
+    @classmethod
+    def convert(cls, db, from_currency, to_currency, amount):
+        """
+        Converts given amount of money from one currency to another. Exchange
+        rates are provided by Google and cached in given local database. The
+        cache expires on the next calendar day.
+
+        :param db:
+            storage adapter for local caching of exchange rates
+        :param from_currency:
+            string — currency from which to convert (e.g. "EUR")
+        :param to_currency:
+            string — currency to which to convert (e.g. "USD")
+        :param amount:
+            Decimal — the amount of money to convert
+        """
+        if from_currency == to_currency:
+            return amount
+        # TODO: class-level memory cache (avoid hitting the database)
+        _from, _to = sorted([from_currency, to_currency]) # keep single direction
+
+        rate = None
+
+        # memory cache
+        if cls._cache_date == utc_today():
+            rate = cls._cache.get(_from, {}).get(_to)
+        else:
+            # reset memory cache
+            cls._cache = {}
+            cls._cache_date = utc_today()
+            logging.debug('No memory-cached rate for '
+                          '{from_currency}→{to_currency}'.format(**locals()))
+
+        # DB cache
+        if rate is None:
+            cached = cls.objects(db).where(from_currency=_from, to_currency=_to)
+            if cached:
+                logging.debug('Found DB-cached rate for '
+                              '{from_currency}→{to_currency}'.format(**locals()))
+
+            obj = cached[0] if cached else cls(from_currency=_from, to_currency=_to)
+            if not obj.date or obj.date < utc_today():
+                # update DB cache (fetch data from external service)
+                obj.rate = utils.convert_currency(_from, _to, 1)
+                obj.save(db)
+            rate = obj.rate
+
+            # update memory cache
+            cls._cache.setdefault(_from, {})[_to] = rate
+
+        # TODO: check amount type (decimal vs float vs int)
+        if _from == from_currency:
+            return amount * rate
+        else:
+            return amount / rate
+

orgtool/ext/finances/templates/dashboard.html

+{% extends "base.html" %}
+
+{% block head %}
+<style type="text/css">
+.money { text-align:right; font-weight: bold; }
+.income { color: green }
+.expense { color: red }
+.outdated { color: gray; background: #ddd}
+.outdated a { color: gray }
+</style>
+{% endblock head %}
+
+{% block content %}
+
+<p>See also: <a href="{{ url_for('orgtool.ext.finances.plan_index') }}">all contracts</a>, 
+             <a href="{{ url_for('orgtool.ext.finances.event_index') }}">all payments</a>.</p>
+
+<img src="{{ chart_url_for_payments(payments, only_smooth=False, 
+                                    width=900, height=250,
+                                    currency=default_currency,
+                                    max_days=300) }}" alt="All payments"/>
+
+<ul style="overflow: hidden; list-style: none;">
+    {% for column in contracts.where_not(next_date_time=None)|slice(3) %}
+        <li style="width: 33%; float: left; margin: 0;">
+            {% for plan in column %}
+                {% if plan.next_date_time %}
+                    <h2><a href="{{ url_for('orgtool.ext.finances.plan', pk=plan.pk) }}">{{ plan }}</a></h2>
+                        {% if plan.next_date_time < today %}
+                            <strong>???</strong>
+                        {% endif %}
+
+                        <span class="{% if plan.expected_payment_amount|float > 0 %}income{% else %}expense{% endif %}">
+                            <strong>{{ plan.get_expected_payment_amount_as(default_currency)|string|replace("-","&minus;")}}&nbsp;{{ default_currency }}</strong>
+                            {% if not plan.currency == default_currency %}
+                                ({{ plan.expected_payment_amount|string|replace("-","&minus;") }}&nbsp;{{ plan.currency }})
+                            {% endif %}
+                            {% if not plan.is_fee_fixed %}(approx.){% endif %}
+                        </span>
+
+                        <span title="{{ plan.next_date_time.strftime("%d %B %Y") }}">{{ render_rel_delta(plan.next_date_time) }}</span>
+                        {% set chart_url = chart_url_for_payments(plan.events,
+                                    max_days=365, 
+                                    only_smooth=False,
+                                    currency=default_currency,
+                        ) %}
+                        {% if chart_url %}
+                            <img src="{{ chart_url }}" alt="payments"/>
+                        {% else %}
+                            <p>No payments within last year.</p>
+                        {% endif %}
+                {% endif %}
+            {% endfor %}
+        </li>
+    {% endfor %}
+</ul>
+
+<p>Only plans with known next payment date are shown.</p>
+
+<p>Higher is always better. Try to keep expenses closer to zero and income higher from zero. :)</p>
+
+{% endblock content %}

orgtool/ext/finances/templates/plan_detail.html

 
 <p><a href="{{ admin_url_for(object, 'finances') }}">View in admin</a></p>
 
+<img src="{{ chart_url_for_payments(object.events, width=800, height=300, only_smooth=False) }}" alt="payments"/>
+
 <div style="float: right; width: 48%;"
     {% if object.stakeholder %}
         <p>Заинтересованное лицо: {{ object.stakeholder|urlize(40, true) }}</p>
             <dt>Actual fee</dt>
                 <dd>{{ object.actual_daily_fee }}&nbsp;{{ object.currency }} a day</dd>
                 <dd>{{ object.actual_monthly_fee }}&nbsp;{{ object.currency }} a month</dd>
+                <dd>{{ object.expected_payment_amount }}&nbsp;{{ object.currency }} per payment
+                    {% if object.is_fee_fixed %}(fixed fee){% else %}(qu0.75){% endif %}
+                </dd>
             <dt>Total {% if object.actual_total_fee < 0 %}spent{% else %}earned{% endif %}</dt>
                 <dd>{{ object.actual_total_fee }}&nbsp;{{ object.currency }}</dd>
                 {# FIXME what if payments include other currencies? #}
     {#<p>{{ object.describe_recurrence() }}</p>#}
     {% if object.dates_rrule %}
         <p>Plan: <em>{{ object.dates_rrule_text }}</em>.</p>
-        <p>Next: <strong>{{ object.next_date_time.strftime("%A, %d %B %Y") }}</strong>.</p>
+        {% if object.next_date_time %} {# may be None if in the past #}
+            <p>Next: <strong>{{ object.next_date_time.strftime("%A, %d %B %Y") }}</strong>.</p>
+        {% endif %}
         {{ object.days_since_last_event }}
         {{ object.days_to_next_event }}
     {% endif %}
     {% endif %}
 </div>
 
-<div style="float: left; width: 48%">
+
+<div style="float: left; width: 48%">    
+
     <table>
         <tr>
             <th>date</th>

orgtool/ext/finances/utils.py

+# -*- coding: utf-8 -*-
+
+import datetime
+import decimal
+import logging
+import urllib2
+import re
+import yaml
+
+from tool import app
+
+
+ROOT_MODULE = __name__.rpartition('.')[0]
+
+logger = logging.getLogger(ROOT_MODULE)
+
+
+class CalculationError(Exception):
+    pass
+
+
+def convert_currency(currency_from, currency_to, amount=1):
+    """
+    Converts currency using Google API. Does not cache rates. It is a good idea
+    to store the rates in a local database and periodically update them using
+    this function.
+    """
+    if currency_from == currency_to:
+        return amount
+    logger.debug('Converting {amount} {currency_from} to {currency_to} using '
+                 'Google currency calculator'.format(**locals()))
+    url_tmpl = ('http://google.com/ig/calculator?'
+                'q={amount}{currency_from}%3D%3F{currency_to}')
+    url = url_tmpl.format(**locals())
+    response = urllib2.urlopen(url)
+    # data string example:
+    #   {lhs: "1 U.S. dollar",rhs: "0.767224183 Euros",error: "",icc: true}
+    data_string = response.read().replace('\xa0', '') # strip separators
+    data = yaml.load(data_string)
+    if data['error']:
+        raise CalculationError('Got error {error}'.format(**data))
+    rate = re.search(r'^(\d+\.\d+)', data['rhs'])
+    if not rate:
+        raise CalculationError('Unexpected rate format: {rhs}'.format(**data))
+    return decimal.Decimal(rate.group(1).strip())
+
+
+def get_default_currency():
+    ext = app.get_feature('money')
+    return unicode(ext.env['default_currency'])
+
+
+# TODO: move this to Dark(?)
+# Origin:
+#     http://stackoverflow.com/questions/488670/calculate-exponential-moving-average-in-python
+def calculate_ema(s, n=2, safe_period=True, ensure_series=True):
+    """
+    returns an n period exponential moving average for
+    the time series s
+
+    s is a list ordered from oldest (index 0) to most
+    recent (index -1)
+    n is an integer
+
+    :param safe_period:
+        automatically shrinks period if it's too large for given data set
+    :param ensure_series:
+        automatically prepends the series with a zero value if there's only one
+        value in the series (this ensures a chart can be built).
+
+    returns a numeric array of the exponential moving average
+    """
+    if not n:
+        return
+
+    s = list(s)
+    if 1 == len(s):
+        s = [0] + s
+#        print 'single item:', s
+#        return s
+
+    ema = []
+
+    if len(s) <= n:
+        if safe_period:
+            n = len(s) / 2
+        else:
+            raise ValueError('period {period} is too large for {cnt} data '
+                             'items.'.format(period=n, cnt=len(s)))
+
+    #get n sma first and calculate the next n period ema
+    sma = sum(s[:n]) / n
+    multiplier = 2 / float(1 + n)
+    ema.append(sma)
+
+    #EMA(current) = ( (Price(current) - EMA(prev) ) x Multiplier) + EMA(prev)
+    ema.append(( (s[n] - sma) * multiplier) + sma)
+
+    #now calculate the rest of the values
+    j = 1
+    for i in s[n+1:]:
+       ema.append(ema[j] + (multiplier * (i - ema[j])))
+       j += 1
+
+    return ema
+
+# TODO: extract this to a template filter
+def _get_rel_delta(dt, precision=2):
+    from dateutil.relativedelta import relativedelta
+    import datetime
+    now = datetime.datetime.utcnow()
+    if not isinstance(dt, datetime.datetime):
+        now = now.date()
+    if dt < now:
+        delta = relativedelta(now, dt)
+    else:
+        delta = relativedelta(dt, now)
+    if delta.days or delta.months or delta.years:
+        # TODO: i18n and i10n (ngettext, etc.)
+        mapping = (
+            (delta.years, u'years'),
+            (delta.months, u'months'),
+            (delta.days, u'days'),
+        )
+        parts = [(v,t) for v,t in mapping if v]
+        used_parts = parts[:precision]
+        is_past = bool(dt < now)
+        return used_parts, is_past
+    else:
+        return [], False
+
+def render_rel_delta(dt):
+    parts, is_past = _get_rel_delta(dt)
+    if parts:
+        parts = [u'{0} {1}'.format(v,t) for v,t in parts]
+        template = u'{0} ago' if is_past else u'in {0}'
+        return template.format(' '.join(parts).strip())
+    else:
+        # _within_ a day; may be another calendar day
+        return u'<strong>today</strong>'
+
+def is_date_within_a_day(dt):
+    parts, is_past = _get_rel_delta(dt)
+    return not bool(parts)
+
+def chart_url_for_payments(payments, width=300, height=100, only_smooth=True,
+                           max_days=None, currency=None):
+    payments = payments.where_not(amount=None).order_by('date_time')  # NOT reversed
+    if max_days:
+        min_date = datetime.datetime.utcnow() - datetime.timedelta(days=max_days)
+        payments = payments.where(date_time__gte=min_date)
+    if not payments:
+        return None
+
+    # http://chart.apis.google.com/chart?cht=lc&chs=400x200&chd=t:10,-20,60,40&chco=318CE7&chds=-20,60&chxt=r,r&chxr=0,-20,60|1,-20,60&chxtc=1,-400&chxp=1,0&chxs=1,0000dd,13,-1,t,FF0000&chxl=1:|zero
+
+    main_color, smooth_color = 'FFBF00', '8DB600', #'318CE7'   #, #'FE6F5E'# 'FAE7B5'
+    """
+    url_template = (
+        'http://chart.apis.google.com/chart?'
+        #'cht=lc&'                                 # line chart
+        'cht=lc:nda&'                             # hide axis
+        + (
+            'chd=t:{smooth_series}&'  # data series
+            if only_smooth else
+            'chd=t:{smooth_series}|{amount_series}&'  # data series
+        ) +
+        'chds={min_amount},{max_amount}&'         # fix axis range
+        'chs={width}x{height}&'                             # image size
+        #'chm=H,FF0000,0,0,1&'              # mark zero amount
+        'chxt=r&'                                 # show ticks on the right side
+        # axis labels:
+        'chxl=0:|{min_amount}|zero|{max_amount}&chxp=0,0|-100,100&chxr=0,{min_amount},{max_amount},0&chxtc=0,-{width}&'
+        'chco={main_color},{smooth_color}&'
+        'chm={annotations}&'
+    )
+    """
+    #print payments
+    amounts = [float(p.get_amount_as(currency) if currency else p.amount)
+               for p in payments if p.amount is not None]
+    #print 'amounts', amounts
+    _flat = lambda xs: ','.join(['{0:.2f}'.format(x) for x in xs])
+    #amount_series = _flat(amounts)
+    smoothing_period = len(amounts) / 3 or len(amounts) # 120->30, 14->6..10, 80->30
+
+#    for i in reversed(range(1,10)):
+#        if i < len(amounts) / 2:
+#            smoothing_period = len(amounts) / i
+#            break
+    # (len(amounts) / 10 if 10 < len(amounts) else 5) or 3
+    smooth_data = calculate_ema(amounts, smoothing_period)
+    if not smooth_data:
+        return ''
+
+    #smooth_series = _flat(smooth_data)
+    min_amount = min(smooth_data if only_smooth else amounts) - 1
+    max_amount = max(smooth_data if only_smooth else amounts) + 1
+    if max_amount < 0:
+        max_amount = 0
+    if 0 < min_amount:
+        min_amount = 0
+#        if min_amount == max_amount:
+#            min_amount = 0 if 0 < min_amount else -max_amount
+
+    #'''
+    # FIXME HACK -- this is broken by design:
+    year_marks = {}
+    for i, payment in enumerate(payments):
+        year_marks.setdefault(payment.date_time.year, (i, payment))
+    annotations = '|'.join([ 'A{text},,1,{point},{size}'.format(
+        text = year_marks[year][1].date_time.strftime('%b %Y'),
+        # HACK: fixing "ensure_series"
+        point = year_marks[year][0] if 1 < len(amounts) else year_marks[year][0]+1,  # payment index in query AND amount index
+        size = 8,
+    ) for year in sorted(year_marks)])
+
+
+    #return url_template.format(**locals())
+    #'''
+
+
+    from pygooglechart import Chart
+    from pygooglechart import SimpleLineChart
+    from pygooglechart import Axis
+    import math
+
+    def _prep_boundary(x):
+        value = math.ceil(abs(x))
+        return -value if x < 0 else value
+    min_y, max_y = [_prep_boundary(x) for x in (min_amount, max_amount)]
+
+    chart = SimpleLineChart(width, height, y_range=[min_y, max_y])
+
+    chart.add_data(smooth_data)
+    if not only_smooth:
+        chart.add_data(amounts)
+    chart.set_colours([smooth_color, main_color])
+
+    chart.set_axis_labels(Axis.RIGHT, [min_y, max_y])
+    chart_url = chart.get_url()
+
+    chxr = 'chxr=0,{min_y},{max_y}'.format(**locals())
+
+    # NOTE: pygooglechart does not support some stuff we need here
+    return '&'.join([
+        chart_url,
+        # visible axes
+        'chxt=r,r',
+
+        # scale with custom range
+        'chds={min_y},{max_y}'.format(**locals()),
+
+        # axis ranges
+        'chxr=0,{min_y},{max_y}|1,{min_y},{max_y}'.format(**locals()),
+
+        # axis tick styles
+        'chxtc=1,-{width}'.format(**locals()),
+
+        # axis labels
+        'chxl=1:|sea level',
+
+        # axis label positions
+        'chxp=1,0',
+
+        # axis label styles
+        'chxs=1,318CE7,13,-1,t,318CE7', #550000',
+
+#        chxr,
+#        'chxl=0:{min_y},zero,{max_y}'.format(**locals()),
+#        'chxp=0,{min_y},0,{max_y}'.format(**locals()),
+##        'chxs=|0N*sz2*,0000FF'
+##        zeromark
+
+        # annotations
+        'chm={annotations}&'.format(**locals()),
+    ])
+

orgtool/ext/finances/views.py

 # -*- coding: utf-8 -*-
 
+import datetime
+
 from tool.routing import url
-from tool.ext.documents import db
+from tool.ext.documents import default_storage
 from tool.ext.templating import as_html
 from tool.ext.breadcrumbs import entitled
 
 from schema import Contract, Payment
+from utils import (
+    get_default_currency, render_rel_delta, chart_url_for_payments
+)
 
 
+@url('/')
+@entitled(u'Finances')
+@as_html('finances/dashboard.html')
+def dashboard(request):
+    db = default_storage()
+    contracts = Contract.objects(db).order_by('next_date_time')
+    payments = Payment.objects(db).order_by('date_time')
+    return {
+        'contracts': contracts,
+        'payments': payments,
+        'render_rel_delta': render_rel_delta,
+        'chart_url_for_payments': chart_url_for_payments,
+        'today': datetime.datetime.today(),
+        'default_currency': get_default_currency(),
+    }
+
 @url('/contracts/')
 @entitled(u'Contracts')
 @as_html('finances/plan_index.html')
 def plan_index(request):
+    db = default_storage()
     plans = Contract.objects(db).order_by('next_date_time')
     return {
         'object_list': plans,
     }
 
 @url('/contracts/<string:pk>')
-@entitled(lambda pk: u'{0}'.format(db.get(Contract,pk)))
+@entitled(lambda pk: u'{0}'.format(default_storage().get(Contract,pk)))
 @as_html('finances/plan_detail.html')
 def plan(request, pk):
+    db = default_storage()
     plan = db.get(Contract, pk)
-    return {'object': plan}
+    return {
+        'object': plan,
+        'chart_url_for_payments': chart_url_for_payments,
+    }
 
 @url('/payments/')
 @url('/payments/<int(4):year>/')
 @entitled(u'Payments')
 @as_html('finances/event_index.html')
 def event_index(request, year=None, month=None, day=None):
+    db = default_storage()
     events = Payment.objects(db).order_by('date_time', reverse=True)
     if year:
         events = events.where(date_time__year=year)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.