Commits

Robert Brewer committed 1509709

Moved balances/budgets from calculate-on-read to calculate-on-write. Needs some more tweaks to be fastest.

Comments (0)

Files changed (4)

flowrate/__init__.py

 
 _subaccountmap = {}
 def isSubAccount(child, parents):
+    """Return True if the child is a sub-account of any of the parents."""
     if isinstance(parents, tuple):
         pass
     elif isinstance(parents, list):
             raise cherrypy.HTTPError("400 No Body",
                 "Transactions MUST include a 'body' member.")
 
+        # Deactivate the old transaction info
+        tx.deactivate()
+
         # Accept the new transaction definition.
         vals = req.json["body"]
         tx['credit'] = popint(vals['credit'])
         tx['description'] = vals['description']
         tx.save()
 
+        # Activate the new transaction info
+        tx.activate()
+
         flows.unfulfill(tx)
         flows.fulfill(tx)
 
             raise cherrypy.NotFound()
 
         flows.unfulfill(tx)
+
+        # Deactivate the old transaction info
+        tx.deactivate()
         tx.delete()
 
         cherrypy.response.status = 204
 
         tx = ledger.Transaction(id=None, **vals)
         tx.save()
+        tx.activate()
 
         flows.fulfill(tx)
 
             dategroups = ['%04d-%02d' % (y, m) for y in years for m in months]
 
         # Calculate the budget of each requested account for each
-        # dategroup in the output.
-        budgets = dict((a, dict((dg, [0, 0]) for dg in dategroups))
-                       for a in accounts)
+        # dategroup in the output. Send [budget, actual] pairs.
+        budgets = {}
+        for a in accounts:
+            acct = ledger.accounts[a]
+            budgets[str(a)] = bucket = {}
+            for dg in dategroups:
+                # Grab transactions matching the balance "date"
+                actual = sum(v for d, v in acct.activity.iteritems()
+                             if coerce_date(d) == dg)
 
-        for tx in ledger.transactions.itervalues():
-            txdate = coerce_date(tx['postdate'])
-            if txdate not in dategroups:
-                continue
+                # Grab obligations matching the balance "date"
+                budget = sum(v for d, v in acct.budgets.iteritems()
+                             if coerce_date(d) == dg)
 
-            if (not accounts) or tx['credit'] in accounts:
-                credit_amount = tx['amount'] * tx['credit_mult']
-                budgets[tx['credit']][txdate][1] += credit_amount
+                bucket[dg] = [budget, actual]
 
-            if (not accounts) or tx['debit'] in accounts:
-                debit_amount = tx['amount'] * tx['debit_mult']
-                budgets[tx['debit']][txdate][1] += debit_amount
-
-        for flowid, flow in flows.flows.iteritems():
-            credit = (not accounts) or flow['credit'] in accounts
-            debit = (not accounts) or flow['debit'] in accounts
-            if not (credit or debit):
-                continue
-
-            if credit:
-                credit_bucket = budgets[flow['credit']]
-            if debit:
-                debit_bucket = budgets[flow['debit']]
-
-            for ob in flow.obligations:
-                obdate = coerce_date(ob['postdate'])
-                if obdate not in dategroups:
-                    continue
-
-                if credit:
-                    credit_bucket[obdate][0] += ob['amount'] * ob['credit_mult']
-
-                if debit:
-                    debit_bucket[obdate][0] += ob['amount'] * ob['debit_mult']
-
-
-        # Touch up the data for JSON, which doesn't allow non-string keys
-        b['data'] = dict(
-            (str(accountid), bucket)
-            for accountid, bucket in budgets.iteritems()
-            )
+        b['data'] = budgets
 
         return b
 
             months = range(1, 13)
 
         if dategroup == 'year':
-            coerce_date = cmpdates.units['years']
             balance_dates = [datetime.date(y, 12, 31) for y in years]
             dategroups = [str(y) for y in years]
         elif dategroup == 'day':
-            coerce_date = cmpdates.units['days']
             balance_dates = []
             for y in years:
                 for m in months:
             dategroups = [d.isoformat() for d in balance_dates]
         else:
             # dategroup == 'month' or other
-            coerce_date = cmpdates.units['months']
             balance_dates = [datetime.date(y, m, calendar.monthrange(y, m)[1])
                              for y in years for m in months]
             dategroups = ['%04d-%02d' % (y, m) for y in years for m in months]
 
         # Calculate the balance of each requested account for each
         # dategroup in the output.
-        balances = dict((a, dict((dg, 0) for dg in dategroups))
-                       for a in accounts)
+        balances = {}
+        age = datetime.date.today() - datetime.timedelta(days=30)
+        dates_and_groups = zip(balance_dates, dategroups)
+        for a in accounts:
+            acct = ledger.accounts[a]
+            balances[str(a)] = bucket = {}
+            for bd, dg in dates_and_groups:
+                # Grab all transactions older than the balance date
+                actual = sum(v for d, v in acct.activity.iteritems()
+                             if d <= bd)
 
-        dates_and_groups = zip(balance_dates, dategroups)
+                if age <= bd:
+                    # Grab obligations between 30 days ago and the balance date...
+                    budget = sum(v for d, v in acct.budgets.iteritems()
+                                 if age <= d <= bd)
 
-        # Grab ledger entries from transactions
-        for tx in ledger.transactions.itervalues():
-            if (not accounts) or (tx['credit'] in accounts):
-                credit_amount = tx['amount'] * tx['credit_mult']
-                credit_bucket = balances[tx['credit']]
-            else:
-                credit_amount = None
+                    # ...but reduce by their fulfillments
+                    fulfillment = sum(v for d, v in acct.fulfillments.iteritems()
+                                      if age <= d <= bd)
+                else:
+                    budget = fulfillment = 0
 
-            if (not accounts) or (tx['debit'] in accounts):
-                debit_amount = tx['amount'] * tx['debit_mult']
-                debit_bucket = balances[tx['debit']]
-            else:
-                debit_amount = None
+                bucket[dg] = actual + budget - fulfillment
 
-            if credit_amount is None and debit_amount is None:
-                # This transaction doesn't bear on the selected accounts.
-                continue
-
-            for bd, dg in dates_and_groups:
-                if tx["postdate"] <= bd:
-                    if credit_amount is not None:
-                        credit_bucket[dg] += credit_amount
-                    if debit_amount is not None:
-                        debit_bucket[dg] += debit_amount
-
-        # Grab obligations between 30 days ago and the balance date
-        age = datetime.date.today() - datetime.timedelta(days=30)
-        for flow in flows.flows.itervalues():
-            if (not accounts) or (flow['credit'] in accounts):
-                credit_bucket = balances[flow['credit']]
-            else:
-                credit_bucket = None
-
-            if (not accounts) or (flow['debit'] in accounts):
-                debit_bucket = balances[flow['debit']]
-            else:
-                debit_bucket = None
-
-            if credit_bucket is None and debit_bucket is None:
-                # This obligation doesn't bear on the selected accounts.
-                continue
-
-            for ob in flow.obligations:
-                if ob['postdate'] < age:
-                    continue
-
-                credit_amount = ob['remaining'] * ob['credit_mult']
-                debit_amount = ob['remaining'] * ob['debit_mult']
-
-                for bd, dg in dates_and_groups:
-                    if ob["postdate"] <= bd:
-                        if credit_bucket is not None:
-                            credit_bucket[dg] += credit_amount
-
-                        if debit_bucket is not None:
-                            debit_bucket[dg] += debit_amount
-
-        # Touch up the data for JSON, which doesn't allow non-string keys
-        b['data'] = dict(
-            (str(accountid), bucket)
-            for accountid, bucket in balances.iteritems()
-            )
+        b['data'] = balances
 
         return b
 
                 "type": acct['type'],
                 },
             }
-    
+
     @cherrypy.tools.json_in(processor=json_processor)
     @cherrypy.tools.last_content_type()
     def PUT(self):

flowrate/flows.py

         for d, txs in txtable.iteritems():
             txs.sort(key=lambda tx: (0 - tx['debit'], tx['postdate']))
 
+        ca = ledger.accounts[self['credit']]
+        da = ledger.accounts[self['debit']]
+
         for ob in obs:
             sys.stdout.write(".")
             sys.stdout.flush()
             obrem = ob['remaining']
             obdate = coerce_date(ob['postdate'])
 
+            # Start by adding all of the obligation to budgets
+            ca.budgets[ob['postdate']] += ob['amount'] * ob['credit_mult']
+            da.budgets[ob['postdate']] += ob['amount'] * ob['debit_mult']
+
             for tx in txtable.get(obdate, []):
                 used = sum(f['amount'] for f in tx.fulfillments)
                 txrem = tx['amount'] - used
                 if obrem > 0 and txrem > 0:
                     f_amt = min(obrem, txrem)
                     tx.fulfillments.append({'obligation': ob, 'amount': f_amt})
+
+                    ca.fulfillments[ob['postdate']] += f_amt * ob['credit_mult']
+                    da.fulfillments[ob['postdate']] += f_amt * ob['debit_mult']
+
+                    # Likewise, reduce the remaining obligation amount.
                     obrem -= f_amt
                     if obrem <= 0:
-                        # Don't allow obligations.remaining to be < 0
+                        # But don't allow obligations.remaining to be < 0
                         obrem = 0
                         break
 
         for tx in ledger.transactions.itervalues():
             tx.fulfillments = [f for f in tx.fulfillments
                                if f['obligation'] not in self.obligations]
-        self.obligations = []
+
+        # Remove the obligations (plus their remainders from budget/fulfillment)
+        ca = ledger.accounts[self['credit']]
+        da = ledger.accounts[self['debit']]
+        while self.obligations:
+            ob = self.obligations.pop()
+            ca.budgets[ob['postdate']] -= ob['amount'] * ob['credit_mult']
+            da.budgets[ob['postdate']] -= ob['amount'] * ob['debit_mult']
+            spent = (ob['amount'] - ob['remaining'])
+            ca.fulfillments[ob['postdate']] -= spent * ob['credit_mult']
+            da.fulfillments[ob['postdate']] -= spent * ob['debit_mult']
 
 
 def fulfill(tx):
         f_amt = min(ob['remaining'], txrem)
         if f_amt > 0:
             tx.fulfillments.append({'obligation': ob, 'amount': f_amt})
+
+            # Add the amount to the account's fulfillments.
+            ca = ledger.accounts[ob['credit']].fulfillments
+            ca[ob['postdate']] += f_amt * ob['credit_mult']
+            da = ledger.accounts[ob['debit']].fulfillments
+            da[ob['postdate']] += f_amt * ob['debit_mult']
+
             ob['remaining'] -= f_amt
             txrem -= f_amt
             if txrem <= 0:
     """Delete any fulfillments for the given transaction. Update obligations."""
     while tx.fulfillments:
         f = tx.fulfillments.pop()
-        f['obligation']['remaining'] += f['amount']
+        ob = f['obligation']
+        ob['remaining'] += f['amount']
+
+        # Subtract the amount from the accounts' fulfillments.
+        ca = ledger.accounts[ob['credit']].fulfillments
+        ca[ob['postdate']] -= f['amount'] * ob['credit_mult']
+        da = ledger.accounts[ob['debit']].fulfillments
+        da[ob['postdate']] -= f['amount'] * ob['debit_mult']
 
 
 def find_transactions(filters):

flowrate/ledger.py

+from collections import defaultdict
+
 import flowrate
 
 
         self.id = id
         self.row = dict((k, kwargs[k]) for k in self.fields)
 
+        # A dict of transaction activity on this account,
+        # one entry per datetime.date.
+        # Each value is a balance *difference* for the day.
+        # So to read the balance at the end of 2012-04-13:
+        #   sum(v for d, v in activity.iteritems() if d <= date(2012, 4, 13))
+        self.activity = defaultdict(int)
+
+        # A dict of obligation budgets on this account,
+        # one entry per datetime.date.
+        # Each value is a budget for the day.
+        # So to read the total budget for 2012-04:
+        #   sum(v for d, v in budgets.iteritems()
+        #       if d.year == 2012 and d.month = 4)
+        self.budgets = defaultdict(int)
+
+        # A dict of sum(obligation fulfillments) on this account,
+        # one entry per datetime.date.
+        # Each value is a balance *difference* for the day.
+        # So to read the balance fulfilled at the end of 2012-04-13:
+        #   sum(v for d, v in fulfillments.iteritems()
+        #       if d <= date(2012, 4, 13))
+        self.fulfillments = defaultdict(int)
+
     @classmethod
     def load_all(cls):
         """Populate the accounts dictionary from the database."""
                 data[k] = row[f]
             tx = cls(row.id, **data)
             transactions[row.id] = tx
+            tx.activate()
+
+    def activate(self):
+        """Apply this transaction's amount to daily balances."""
+        ca = accounts[self['credit']].activity
+        ca[self['postdate']] += self['amount'] * self['credit_mult']
+        da = accounts[self['debit']].activity
+        da[self['postdate']] += self['amount'] * self['debit_mult']
+
+    def deactivate(self):
+        """Apply this transaction's amount to daily balances."""
+        ca = accounts[self['credit']].activity
+        ca[self['postdate']] -= self['amount'] * self['credit_mult']
+        da = accounts[self['debit']].activity
+        da[self['postdate']] -= self['amount'] * self['debit_mult']
 
     def __getitem__(self, key):
         return self.row[key]
+import datetime
 import optparse
 import os
 thisdir = os.path.abspath(os.path.dirname(__file__))