Using Rails for Batch processes (example: Moneybird export via API and command-line)
Wednesday, March 2nd, 2011My accountant needs to have my financial data in a somewhat slightly different format than the export my online invoicing application provides. Luckily Moneybird has an api so I set out to write my alternative export. I googled around for some best practices, and this is the workflow I came up with:
Create a new Rails app
> rails new moneybirdexport > cd moneybirdexport
Generate models for api access and conversion logic
Our goal is a list of incoming and outgoing invoices – so we generate models with the required fields.
> rails generate model dumped_invoice klant:string, factuurdatum:date, periode:string, factuurnummer:string, bedrag_excl_btw:decimal, btw_percentage:decimal, bedrag_incl_btw:decimal, omschrijving:text, soort:string, betaald:string, datum:date, betaalwijze:string > rails generate model dumped_incoming_invoice factuurdatum:date, periode:string, factuurnummer:string, bedrag_excl_btw:decimal, btw_bedrag:decimal, bedrag_incl_btw:decimal, omschrijving:text, soort:string, betaald:string, datum:date, betaalwijze:string > rake db:migrate
The Moneybird API exposes invoice, incoming_invoice and contact. No need to use a generator here, create the app/models/ files contact.rb, incoming_invoice.rb, and invoice.rb, to create the ActiveResources Contact, IncomingInvoice and Invoice – the source is identical apart from the classnames:
class Invoice < ActiveResource::Base self.site = "https://" + Moneybirdexport::MBX_ACCOUNT_NAME + ".moneybird.com" self.user = Moneybirdexport::MBX_USER_NAME self.password = Moneybirdexport::MBX_PASSWORD self.timeout = 5 end
Not sure what the best practice is here, but I put per-app configurable values in config/application.rb, so if you use the constant values in the same way I did in the previous lines, you need to define them there as well:
module Moneybirdexport
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# ...
end
MBX_ACCOUNT_NAME = "username"
MBX_USER_NAME = "someemailaddress@domain.com"
MBX_PASSWORD = "secret"
end
You should be able to test the api already on the command-line (YAML::dump gives a well-formatted output of objects):
> rails console irb(main):> puts YAML::dump Invoice.first irb(main):> puts YAML::dump IncomingInvoice.first irb(main):> puts YAML::dump Contact.first
Write the conversion logic
Some fields I need for my export (dumped_invoices and dumped_incoming_invoices) are not attributes of Invoices and IncomingInvoices as Moneybird sees them. So in the models DumpedInvoice and DumpedIncomingInvoice there’s a method set_attributes_from_invoice that digs out the info from somewhat deeper in the Moneybird model (looks up the related contact name, or fabricates a description from the invoice line items’ description…). Exactly how this is done is not important, I’m just copy-pasting my code here if you need to do something similar with the Moneybird API:
class DumpedInvoice < ActiveRecord::Base
# need to include ActionView::helper because I am using the truncate method
# source: http://stackoverflow.com/questions/489641/using-helpers-in-model-how-do-i-include-helper-dependencies
include ActionView::Helpers
def set_attributes_from_invoice(i) # i should be an Invoice ActiveResource
self.klant = i.company_name
self.factuurdatum = i.invoice_date
self.periode = ((i.invoice_date.month + 2)/3).to_s + "e kw"
self.factuurnummer = i.invoice_id
self.bedrag_excl_btw = i.total_price_excl_tax
self.bedrag_incl_btw = i.total_price_incl_tax
self.btw_percentage = 0 if ( self.bedrag_excl_btw == 0)
self.btw_percentage = ((self.bedrag_incl_btw - self.bedrag_excl_btw + 0.001)/ self.bedrag_excl_btw * 100).to_i if ( self.bedrag_excl_btw >0)
# concatenate the detail description and remove tabs, linebreaks with squish
concat_details_descr = i.details.map(&:description).join(" - ").squish
self.omschrijving = truncate(sanitize(concat_details_descr), :length=> 100)
self.betaald = "Ja" # shortcut - all of them paid by now...
self.datum = i.payments.last.payment_date # shortcut - everything was just one payment...
self.betaalwijze = i.payments.last.payment_method
return self
end
end
class DumpedIncomingInvoice < ActiveRecord::Base
# need to include ActionView::helper because I am using the truncate method
# source: http://stackoverflow.com/questions/489641/using-helpers-in-model-how-do-i-include-helper-dependencies
include ActionView::Helpers
def set_attributes_from_invoice(i) # i should be an IncomingInvoice ActiveResource
self.factuurdatum = i.invoice_date
self.periode = ((i.invoice_date.month + 2)/3).to_s + "e kw"
self.factuurnummer = i.invoice_id
self.bedrag_excl_btw = i.price_incl_tax - i.price_tax
self.btw_bedrag = i.price_tax
self.bedrag_incl_btw = i.price_incl_tax
invoice_company = Contact.find(i.contact_id)
self.omschrijving = invoice_company.company_name
self.betaald = "Ja" # shortcut - ze zijn idd allemaal betaald...
self.datum = i.payments.last.payment_date # shortcut - everything was just one payment...
self.betaalwijze = i.payments.last.payment_method
return self
end
end
(If this was actual code for an app, I would of course factor out the duplication…
)
Define the actual Batch script as a Rake task
There are several ways to load the Rails libraries, but using a Rake task is probably the best solution and there’s an excellent railscast on Rake. I use the FasterCSV library here because I couldn’t figure out quickly how to use tabs instead of commas as separators with the built-in CSV library. Don’t forget to include gem "fastercsv" in your Gemfile and run bundle install if you do as well. I followed the Railscast tutorial step by step to come up with a lib/tasks/export.rake that accepts an optional year parameter so you can run:
rake export:invoices rake export:incoming_invoices year=2011 rake export:all
namespace :export do
# Note: you need to add :environment as dependency so the rake "environment"
# task is called that loads the enviromennt
desc "exports invoices"
task :invoices => :environment do
param_year = ENV["year"].to_i
param_year = 2010 if param_year == 0
# make sure you have included "gem 'fastercsv'" in your Gemfile and run "bundle install"
# needs to be tab-separated because data contains commas...
FasterCSV.open("dumped_invoices.csv", 'wb', :col_sep => "\t") do |csv|
# write the columns headers
csv << DumpedInvoice.new.attributes.keys
#iterate over invoices
Invoice.all.select{|i| i.invoice_date.year == param_year}.sort_by(&:invoice_date).each do |i|
csv << DumpedInvoice.new.set_attributes_from_invoice(i).attributes.values
end
end
end
desc "exports incoming invoices"
task :incoming_invoices => :environment do
param_year = ENV["year"].to_i
param_year = 2010 if param_year == 0
# make sure you have included "gem 'fastercsv'" in your Gemfile and run "bundle install"
# needs to be tab-separated because data contains commas...
FasterCSV.open("dumped_incoming_invoices.csv", 'wb', :col_sep => "\t") do |csv|
# write the columns headers
csv << DumpedIncomingInvoice.new.attributes.keys
#iterate over invoices
IncomingInvoice.all.select{|i| i.invoice_date.year == param_year}.sort_by(&:invoice_date).each do |i|
csv << DumpedIncomingInvoice.new.set_attributes_from_invoice(i).attributes.values
end
end
end
task :all => [:invoices, :incoming_invoices]
end
That’s it! My first blogpost since I started studying Ruby and Rails again after some dabbling with it back in 2006!