2007/12/08

Rails Importing invoices from Blinksale

I use Blinksale for all my invoicing, it's great. I use a Ruby on Rails application, that I wrote myself, to handle my accounts. It's great too. What isn't so great is re-typing my invoices into my accounts application after creating them in Blinksale. That's not even a little bit great, in fact it sucks.

Earlier this year Blinksale released a REST API so I decided to see if I can use that to allow my accounts application to import invoices from Blinksale.


Getting Started

BTW, Blinksale recommend setting up and using a separate login for any API integration.

I'm going to use the ruby library that Blinksale provide, so I need to download the following files to my Rails Application's lib directory;


  • http://www.blinksale.com/api/blinksale.rb

  • http://www.blinksale.com/api/rest_client.rb

  • http://www.blinksale.com/api/xml_node.rb



Talking to Blinksale

Let's start with a quick test script in the application's script directory;



#!/usr/bin/env ruby

require File.dirname(__FILE__) + '/../config/environment'

blinksale = Blinksale.new |company|, |user|, |password|, |use_ssl|

invoice = blinksale.invoices[ |a blinksale invoice id| ]

puts invoice.data



Replace the elements between |pipes| with values for your own Blinksale account. The "use_ssl" value should be set to true if you're a paying customer using the SSL option. If not, you can just leave it off altogether.

The "blinksale invoice id" is NOT the value you see in the "ID" column when looking at a list of invoices. Instead, it's a numeric value assigned when you create an invoice. To find this ID just click on any Blinksale invoice to view it. The URL you are looking at should be something like;


https://[your account].blinksale.com/invoices/123456


If you're using a free account, or if you don't use https, it will be an http url.

The ID we want is the number at the end - in this case, 123456


Blinksale Invoice XML

Run the test script and you should see a stream of XML printed out, something like this example from the Blinksale API document;



<?xml version="1.0" encoding="UTF-8"?>
<invoice xmlns="http://www.blinksale.com/api" uri="http://example.blinksale.com/invoices/1"
status="pastdue" subtotal="19.00" total="19.00" paid="2.00" total_due="17.00"
surplus="0.00" updated_at="2006-09-20T17:27:48Z" created_at="2006-06-27T22:43:13Z">
<client name="Acme">http://example.blinksale.com/clients/2</client>
<number>100001</number>
<po_number>123456</po_number>
<date>2006-06-27</date>
<terms due_date="2006-07-12">15</terms>
<currency>USD</currency>
<tax amount="0.00">8.75%</tax>
<freight>0.00</freight>
<late_fee amount="0.00">0%</late_fee>
<tags>bob, scott</tags>
<lines total="882.00">
<line>
<name>French Hens</name>
<quantity>3.0</quantity>
<units>Product</units>
<unit_price>19.00</unit_price>
<taxed>false</taxed>
</line>
<line>
<name>Piper-Piping</name>
<quantity>11.0</quantity>
<units>Service</units>
<unit_price>75.00</unit_price>
<taxed>false</taxed>
</line>
</lines>
<deliveries uri="http://example.blinksale.com/invoices/1/deliveries">
<delivery uri="http://example.blinksale.com/invoices/1/deliveries/3" created_at="2006-09-22T23:51:42Z">
<body>Here's the invoice for the latest work - thanks!</body>
<recipient name="John Doe" email="john@acme.com">http://example.blinksale.com/clients/2/people/2</recipient>
<recipient name="Bob Smith" email="bob@example.com">http://example.blinksale.com/users/1</recipient>
</delivery>
</deliveries>
<payments uri="http://example.blinksale.com/invoices/1/payments" total="10.00">
<payment uri="http://example.blinksale.com/invoices/1/payments/5" created_at="2006-09-25T18:01:33Z">
<amount>10.00</amount>
<date>2006-09-25</date>
<payment_method>Check</method>
<number>10001</number>
</payment>
</payments>
<notes>Please reference this invoice number in your check memo.</notes>
<include_payment_link>true</include_payment_link>
</invoice>



The information I need for the object in my accounts system (in my case, a "Revenue" object) is all buried in there, but there's a lot more detail than I need, and I have to get at the stuff I do want, somehow.


Hpricot

The Hpricot parser is a great way of extracting information from XML (including HTML). If you haven't already, you will need to install it using;


sudo gem install hpricot -y


(or whatever you do on Windows, if that's your OS)

After a few minutes playing with a parsed version of the downloaded invoice, I can figure out how to get the fields I want. Now, let's expand our test script so that it can pull out the key details;



#!/usr/bin/env ruby

require File.dirname(__FILE__) + '/../config/environment'
require 'hpricot'

blinksale = Blinksale.new |company|, |user|, |password|, |use_ssl|

invoice = blinksale.invoices[ |a blinksale invoice id| ]

data = Hpricot.parse invoice.data

puts "Invoice Number: %s" % (data/'number')[0].innerHTML
puts "Date: %s" % (data/'date')[0].innerHTML
puts "Client: %s" % (data/'client')[0]['name']
puts "Net amount: %s" % (data/'lines')[0]['total']
puts "VAT: %s" % (data/'tax')[0]['amount']
puts "Total: %s" % (data/'invoice')[0]['total']




Integrating with the Rails App

Now that I know how to get an invoice from Blinksale, and what to do with it when I get it, I want to build this capability into my Rails accounts system.

My Revenue model gets a new class method (don't forget to add require 'hpricot' at the top);



def self.new_from_blinksale( url )

return nil unless url =~ /http.*\/(\d+)$/
id = $1

blinksale = Blinksale.new BLINKSALE[:company], BLINKSALE[:user], BLINKSALE[:password], BLINKSALE[:use_ssl]
invoice = blinksale.invoices[ id ]
data = Hpricot.parse invoice.data

revenue = self.new
revenue.invoice_number = (data/'number')[0].innerHTML
revenue.date = (data/'date')[0].innerHTML
revenue.amount = (data/'lines')[0]['total']
revenue.vat = (data/'tax')[0]['amount']
revenue.supplier = (data/'client')[0]['name']
revenue.description = ((data/'lines')[0]/'name')[0].innerHTML

# expect that any payment represents payment in full
if (data/'payment').size > 0
revenue.paid = true
revenue.bank_date = ((data/'payment')[0]/'date').innerHTML
end

revenue
rescue Net::HTTPServerException
logger.error "Failed to fetch blinksale invoice: #{ url }"
nil
end



I've defined my Blinksale credentials in a BLINKSALE hash in my config/environment.rb file.

I'm going to import an invoice by pasting the Blinksale invoice url into a form text field, so my class method takes a url as a parameter, extracts the ID with a regular expression and adds the data to a new Revenue object.

Blinksale records the payment of an invoice, so I check to see if there are any payments. In my case, clients only ever pay invoices in a single payment. So, my method assumes that if there are any payments recorded, then the invoice has been paid on the date of the first payment.

My Revenue model has a description field. There is nothing directly equivalent in a Blinksale invoice, so I take the text of the first invoice line and use that as the description.

At this point I can fire up script/console and use my new method to pull in invoice data from Blinksale.


The RevenuesController

Now I want to add this function to the web interface of my accounts system.

Initially, my RevenuesController's "new" method looks like this;



def new
@revenue = Revenue.new
respond_to do |format|
format.html
format.xml { render :xml => @revenue }
end
end



I'm going to extend this so that, if a blinksale_url parameter was passed in, the new Revenue object is created via my new class method;



def new
@revenue = Revenue.new

if !params[:blinksale_url].blank?
if rev = Revenue.new_from_blinksale( params[:blinksale_url] )
@revenue = rev
else
@revenue.errors.add_to_base "Failed to import Blinksale invoice from URL: #{ params[:blinksale_url] }"
end
end

respond_to do |format|
format.html
format.xml { render :xml => @revenue }
end
end



If the Blinksale import fails, for whatever reason, I fall back to simply having a new Revenue object, but with an error message that will be displayed by the usual "error_messages_for :revenue" call in my view.


The Revenues View

My app/views/revenues/index.html.erb has a "New Revenue" link like this;


link_to 'New revenue', new_revenue_path


I want an additional form, also targeting the 'new' method, which passes the 'blinksale_url' parameter;



<%= form_tag new_revenue_path, :method => :get %>
URL: <%= text_field_tag 'blinksale_url' %>
<%= submit_tag "Import from Blinksale" %>
</form>



And that's it.

Please let me know if you found this post useful, or if you have any other ideas for integration with Blinksale, or any other cool web apps.