Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Created January 8, 2010 18:46
Show Gist options
  • Save lukeredpath/272275 to your computer and use it in GitHub Desktop.
Save lukeredpath/272275 to your computer and use it in GitHub Desktop.
Generate a HTML expense report using the FreeAgent API
# this goes in ~/.freeagent
ENV['FA_COMPANY'] = 'mycompany'
ENV['FA_USERNAME'] = 'myloginemail'
ENV['FA_PASSWORD'] = 'mypassword'
<html>
<head>
<title>Expense Report for Luke Redpath (<%= label %>)</title>
<style>
<!--
body {
margin: 0; padding: 50px;
font-family: Helvetica, Arial;
}
h1 {
font-weight: normal;
padding-top: 20px;
margin-bottom: 50px;
}
#logo {
position: absolute;
top: 50px; right: 50px;
}
table {
width: 100%;
border-collapse: collapse;
border-bottom: 2px solid #555;
margin-bottom: 30px;
}
th, td {
padding: 8px 3px;
}
th {
text-align: left;
border-bottom: 2px solid #555;
}
td {
border-bottom: 1px solid #ccc;
}
.numeric {
text-align: right;
}
tfoot td, tfoot th {
border-top: 2px solid #555;
background: #eee;
}
tfoot th {
text-align: right;
}
-->
</style>
</head>
<body>
<h1>Expense Report for Luke Redpath, <%= label %></h1>
<div id="logo">
<img src="/Users/luke/Documents/Business/Branding/logo-small.png" width="200" height="52" alt="Logo Small">
</div>
<table>
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th>Category</th>
<th>Appears on P11D?</th>
<th class="numeric">Total (GBP)</th>
</tr>
</thead>
<tbody>
<% expenses.sort_by(&:dated_on).each do |expense| %>
<tr>
<td><%= expense.dated_on.to_date.to_formatted_s(:short) %></td>
<td><%= expense.description %></td>
<td><%= expense.expense_type %></td>
<td></td>
<td class="numeric"><%= "%.2f" % -expense.gross_value %></td>
</tr>
<% end %>
</tbody>
<tfoot>
<tr>
<th colspan="4">Total</th>
<td class="numeric"><%= "%.2f" % -expenses.sum(&:gross_value) %></td>
</tr>
</tfoot>
</table>
<p><em>Generated at <%= Time.now %></em></p>
</body>
</html>
#!/usr/bin/env ruby
require File.join(File.dirname(__FILE__), *%w[freeagent freeagent])
require 'ostruct'
require 'erb'
require 'tempfile'
MY_USER_ID = XXXX
freeagent = FreeAgent::Company.new(ENV['FA_COMPANY'], ENV['FA_USERNAME'], ENV["FA_PASSWORD"])
class ExpensesReport
def initialize(expenses, label)
@expenses = expenses
@label = label
end
def to_s
output = @label.upcase + "\n" + separator + "\n"
grouped_expenses.map do |category, expenses|
output << "#{category.ljust(60)}#{(-expenses.sum(&:gross_value)).to_s.rjust(10)}\n"
end
output << separator + "\n"
output
end
def to_html
vars = OpenStruct.new(:expenses => @expenses, :label => @label)
ERB.new(File.read(template_path)).result(vars.send(:binding))
end
private
def separator
70.times.inject('') { |buff, x| buff + '-' }
end
def grouped_expenses
@grouped_expenses ||= @expenses.group_by(&:expense_type)
end
def template_path
File.join(File.dirname(__FILE__), *%w[freeagent templates expense_report.erb.html])
end
end
report = case ARGV[0]
when 'last'
ExpensesReport.new(freeagent.expenses(MY_USER_ID,
:from => Date.today.last_month.beginning_of_month,
:to => Date.today.last_month.end_of_month
).find_all, "#{Date::MONTHNAMES[Date.today.last_month.month]} #{Date.today.last_month.year}")
else
ExpensesReport.new(freeagent.expenses(MY_USER_ID,
:from => Date.today.beginning_of_month,
:to => Date.today.end_of_month
).find_all, "#{Date::MONTHNAMES[Date.today.month]} #{Date.today.year}")
end
temp_path = "/tmp/expense_report_#{Time.now.to_i}.html"
File.open(temp_path, 'w+') do |io|
io.write(report.to_html)
end
system("open -a Safari #{temp_path}")
require 'rubygems'
require 'restclient'
require 'crack'
require 'mash'
require 'active_support/all'
RestClient::Resource.class_eval do
def root
self.class.new(URI.parse(url).merge('/').to_s, options)
end
end
module FreeAgent
class Company
def initialize(domain, username, password)
@resource = RestClient::Resource.new(
"https://#{domain}.freeagentcentral.com",
:user => username, :password => password
)
end
def invoices
@invoices ||= Collection.new(@resource['invoices'], :entity => :invoice)
end
def contacts
@contacts ||= Collection.new(@resource['contacts'], :entity => :contact)
end
def expenses(user_id, options={})
options.assert_valid_keys(:view, :from, :to)
options.reverse_merge!(:view => 'recent')
if options[:from] && options[:to]
options[:view] = "#{options[:from].strftime('%Y-%m-%d')}_#{options[:to].strftime('%Y-%m-%d')}"
end
Collection.new(@resource["users/#{user_id}/expenses?view=#{options[:view]}"], :entity => :expense)
end
end
class Collection
def initialize(resource, options={})
@resource = resource
@entity = options.delete(:entity)
end
def url
@resource.url
end
def find(id)
entity_for_id(id).reload
end
def find_all
case (response = @resource.get).code
when 200
if entities = Crack::XML.parse(response.body)[@entity.to_s.pluralize]
entities.map do |attributes|
entity_for_id(attributes['id'], attributes)
end
else
[]
end
end
end
def create(attributes)
payload = attributes.to_xml(:root => @entity.to_s )
case (response = @resource.post(payload,
:content_type => 'application/xml', :accept => 'application/xml')).code
when 201
resource_path = URI.parse(response.headers[:location]).path
Entity.from(@resource.root[resource_path], @entity)
end
end
def update(id, attributes)
entity_for_id(id).update(attributes, headers)
end
def destroy(id)
entity_for_id(id).destroy
end
private
def entity_for_id(id, attributes={})
Entity.from(@resource[id], @entity, attributes)
end
end
class Entity
attr_reader :attributes
def initialize(resource, entity, attributes = {})
@resource, @entity = resource, entity
@attributes = attributes.to_mash
end
def self.from(resource, entity, attributes = {})
klass = begin
FreeAgent.const_get(entity.to_s.classify)
rescue NameError
self
end
klass.new(resource, entity, attributes)
end
def url
@resource.url
end
def get(options)
@resource.get(options)
end
def collection(path, entity)
Collection.new(@resource[path], :entity => entity)
end
def reload
returning(self) do
@attributes = Crack::XML.parse(@resource.get)[@entity.to_s].to_mash
end
end
def update(attributes = {})
@resource.put(attributes.to_xml(:root => @entity.to_s.downcase),
:content_type =>'application/xml', :accept => 'application/xml')
end
def destroy
@resource.delete
end
def id
@attributes["id"]
end
private
def method_missing(*args)
@attributes.send(*args)
end
end
class Invoice < Entity
def mark_as_sent!
@resource["mark_as_sent"].put("",
:content_type =>'application/xml', :accept => 'application/xml')
end
end
end
#!/usr/bin/env ruby
# all of my FreeAgent scripts use ENV vars to grab the login credentials
# I wrap things up in a small bin wrapper script like this which loads in
# ~/.freeagent to set the ENV vars followed by the script itself.
load('~/.freeagent')
load('~/Code/mine/utilities/expenses_report.rb')
@tetherit
Copy link

Doesn't work :(

/usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/abstract_response.rb:48:in `return!': 404 Resource Not Found (RestClient::ResourceNotFound)
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/request.rb:230:in `process_result'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/request.rb:178:in `block in transmit'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/1.9.1/net/http.rb:745:in `start'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/request.rb:172:in `transmit'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/request.rb:64:in `execute'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/request.rb:33:in `execute'
    from /usr/local/Cellar/ruby/1.9.3-p0/lib/ruby/gems/1.9.1/gems/rest-client-1.6.7/lib/restclient/resource.rb:51:in `get'
    from /Users/hackeron/github/freeagent-cli/freeagent.rb:60:in `find_all'
    from /Users/hackeron/github/freeagent-cli/expenses_report.rb:55:in `<top (required)>'
    from ./gen_expenses_report:8:in `load'
    from ./gen_expenses_report:8:in `<main>'

@lukeredpath
Copy link
Author

Try the updated version above, check you're settings are correct.

@tetherit
Copy link

I made a few minor changes, but works now, thank you :)

@tetherit
Copy link

I assume the column for P11D is information you input manually?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment