Creating invoices with CouchDB.
A while ago I (and the rest of the company I worked for) was laid off, so I was forcibly thrust into the world of consulting. It’s been quite a change, keeping track of my hours and sending out invoices for the work I do. And it’s that second point that I want to talk a little more about.
Perhaps I’m too picky, but there seemed to be a fatal flaw with all the online invoicing tools I tried. One didn’t allow enough clients for me on the free plan. (Yes, I am that cheap, especially when I’ve just been laid off.) Another would have allowed the client to dispute the invoice. I mean, really. This is the invoice. You don’t get to dispute it. Or at least, I don’t particularly want to make it easy for you. So I ended up using an online time tracker, and left the invoice creation step until later.
Well, later rolled around, and I really kinda wanted to get paid, so I downloaded a report of my hours as a csv file, and whipped up a quick Python script to parse it, and import it into a locally running copy of CouchDB.
The python script looked like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import couchdb.client import csv import path server = couchdb.client.Server('http://localhost:5984/') if 'timelog' not in server: db = server.create('timelog') db = server['timelog'] print db def cleanup(): # Clear out old rows. for row in db: if not row.startswith("_"): del db[row] def input( docName ): print docName input = open( docName ) reader = csv.DictReader( input ) i = 1 for row in reader: row['Import Doc Name'] = docName row['Import Doc Row'] = i if 'Client Name' not in row: row['Client Name'] = "Client1" key = "%s_%03d" % (docName.name, i) print " ", key db[key] = row i += 1 cleanup() base = path.path("/Users/bwinton/Documents/Client1") for file in base.files("*.csv"): input( file ) |
Pretty easy, eh? After that, I had to create a couple of CouchDB views:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// hoursPerDay map=function(doc) { key = [ doc['Employee name'], doc['Client Name'], doc['Date of work'], ]; emit( key, doc ); }; reduce=function (tag, values) { sum=0; temp = [] for( var i=0; i<values.length; i++ ) { sum += parseFloat(values[i]['Time in hours']); temp[i] = { 'Order':values[i]['Import Doc Row'], 'Description':values[i]['Description'], 'Time':values[i]['Time in hours'], 'Type':values[i]['Activity Type'], } } temp = temp.sort(function(a,b) { a = parseFloat(a['Order']) b = parseFloat(b['Order']) return a - b }) return [sum, temp]; }; // totalHours // This view is just to save me re-calculating this value every time // I call the page, because it should only change when we add a new // document. map=function(doc) { key = [ doc['Employee name'], doc['Client Name'], doc['Date of work'], ]; emit( key, parseFloat(doc['Time in hours']) ); }; reduce=function(keys, values, rereduce) { function sum( values ) { retval = 0; for (i=0; i<values.length; i++) { retval += values[i]; } return retval; } return sum(values); }; |
it was a quick snippet of HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<html> <head><title>Time Log</title> <link rel="stylesheet" href="style/blueprint.css" type="text/css"> <script> // Set up some variables. months = [ "2009-02" ] users = { "Blake Winton":{ invoiceDates : [ "March 1st, 2009."], address : ["16 Forman Avenue", "Toronto, ON, M4S 2R2", "bwinton@latte.ca"] }}; </script> <script src="script/bwTimesheet.js"></script> </head> <body> <p class="prepend-1"><b>Blake Winton</b><br/> 16 Forman Avenue<br/> Toronto, ON, M4S 2R2<br/> bwinton@latte.ca</p> <p class="prepend-1"><b>Invoice Number:</b> XX-<span id="invoiceNum">000</span><br/> <b>Date:</b> <span id="invoiceDate">March 5th, 1973.</span><br/></p> <div class="prepend-1 span-1"><b>To:</b></div> <div class="span-22 last"> Client Name<br/> 16 Client Address</div> <p class="span-24">&nbsp;</p> <p><span id="timesheet">Timesheet loading...</span> <span id="summary">Summary loading...</span> </p> </body> </html> |
and Javascript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
// Get a default month, if one wasn’t passed in. month = $.url.param('month') if (typeof(month) == 'undefined') { month = "0"; } month = parseInt(month) if ( month < 0 || month >= months.length ) { month = 0; } invoiceNumInt = month+1; month = months[month]; // Get a default user, if one wasn’t passed in. // Yeah, I’ve defaulted it to me. ;) user = $.url.param('user') if (typeof(user) == 'undefined') { user = "Blake Winton"; } if (!(user in users)) { user = "Blake Winton"; } // Figure out when to start and end the invoice. invoiceDates = users[user].invoiceDates; invoiceDateStr = invoiceDates[invoiceNumInt-1]; start = [user, client, month+"-01"]; end = [user, client, month+"-31"]; // Why, oh why, doesn’t Javascript have printf? function zeroPad(num,count) { var numZeropad = num + ''; while(numZeropad.length < count) { numZeropad = "0" + numZeropad; } return numZeropad; } // Update the header, with the address, invoice number, and date. function updateHeader() { var address = $("#address").empty(); address.append( "<b>"+user+"</b><br/>" ); for (var i=0; i<users[user].address.length; i++) { var line = users[user].address[i]; address.append( line + "<br/>" ); } var invoiceNum = $("#invoiceNum").empty(); invoiceNum.append( zeroPad( invoiceNumInt, 3 )); var invoiceDate = $("#invoiceDate").empty(); invoiceDate.append( invoiceDateStr ); updateTimesheet(); } // Update the timesheet, with hours and descriptions. function updateTimesheet() { var timesheet = $("#timesheet").empty(); var dbs = $.couch.db("timelog").view("invoice/hoursPerDay",{ group: true, startkey: start, endkey: end, success: function(r) { table = "<div class='span-20 prepend-1 last'><table class='timelog'>"; rowNum = 0; response = r['rows'] for (var i = 0; i < response.length; i++) { var record = response[i]; var date = record['key'][2]; var total = record['value'][0]; var entries = record['value'][1]; table += "<tr class='row"+rowNum+"'><td>" + date + "</td><td>" + total + "h</td>"; for (var j=0; j < entries.length; j++ ) { if (j > 0) { table += "<tr class='row"+rowNum+"'><td/><td/>"; } table += "<td>" + entries[j]['Time'] + "h - " + entries[j]['Description'] + "</td></tr>"; rowNum += 1; rowNum %= 2; } } table += "</table></div>"; timesheet.append( table ); updateSummary(); } }) }; // And finally, update the important stuff. // The total hours, rate, and amount owed. function updateSummary() { var dbs = $.couch.db("timelog").view("invoice/totalHours",{ startkey: start, endkey: end, success: function(r) { summary = $("#summary").empty(); content = "<p class='span-20 prepend-1 last'>"; if ( r['rows'].length == 0 ) { content += "No data available."; } else { value = r['rows'][0]['value']; content += "<b>Total hours:</b> " + value + "<br/>" + "<b>Rate:</b> $" + rate + currency +"/hour.<br/>" + "<b>Sub-total:</b> $" + (value*rate).toFixed(2) + "<br/>" + "<b>Tax (GST @ 5%):</b> $" + (value*5).toFixed(2) + "<br/>" + "<b>Total Payable:</b> $" + (value*(rate+5)).toFixed(2) + "<br/>" } content += "</p>"; summary.append( content ); } }); }; // When the document is ready, kick off the updates. $(function() { updateHeader(); }); |
And we’re done. Not to sound too much like an infomercial, but with JQuery, Blueprint, CouchDB, and Python, it took me only 255 lines of code to get a decent-enough-quality invoice that I could save it as a PDF, send to the client, and get paid. In fact, the client liked it enough (or just wanted things to be standard enough) that they asked that the other consultant on the project use the same template, so I had to spend some time taking my original invoice script and editing it to support more than one user. Either way, I would call it a success.