Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c64dc41
Showing
11 changed files
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
h2. GMoney | ||
|
||
h3. Automatically download your bank transactions into a Google Spreadsheet | ||
|
||
http://github.com/pauldowman/gmoney | ||
|
||
GMoney automatically downloads your bank account transactions and inserts them into a Google spreadsheet. | ||
Transactions from each account go into a different sheet, and you can write additional sheets to generate summaries, | ||
spending reports and charts. | ||
|
||
GMoney requires the "mechanize":http://mechanize.rubyforge.org/mechanize/ and "ofx-parser":http://ofx-parser.rubyforge.org/ gems. | ||
|
||
You'll need to roll up your sleeves and write a bit of code though, you'll probably need to write a few lines of code to | ||
script loggin in to your bank and clicking the "download transactions" link, but it's not that hard, see accounts/pc_financial.rb | ||
for an example. If you do that please send me a patch or pull request and I'll add your bank. | ||
|
||
Your account details (login, account id, password, etc) go into $HOME/.gmoney/config.rb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
class CibcVisa < CreditCardAccount | ||
def name | ||
"CIBC VISA" | ||
end | ||
|
||
def download_data | ||
# return File.read("#{ENV['HOME']}/Desktop/cibcvisa.qfx") | ||
|
||
agent.keep_alive = false | ||
|
||
page = agent.get "https://www.cibconline.cibc.com/olbtxn/authentication/PreSignOn.cibc?locale=en_CA" | ||
form = page.form("signonForm") | ||
form.newCardNumber = config[:userid] | ||
form.pswPassword = config[:password] | ||
form.securityUID = get_cookie("securityUID") | ||
form.isPersistentCookieDisabled = "1" | ||
|
||
page = form.submit | ||
|
||
page = agent.get "https://www.cibconline.cibc.com/olbtxn/accounts/TransactionDownload1.cibc" | ||
form = page.form("transactionDownloadForm") | ||
page = form.submit | ||
|
||
data = page.body | ||
|
||
if data =~ /There are no transactions available to download for the account and the date range you have selected/ | ||
return "" | ||
else | ||
return data | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
class IngCanada < BankAccount | ||
def name | ||
"ING account #{config[:account_id]}" | ||
end | ||
|
||
def download_data | ||
# return File.read("#{ENV['HOME']}/Desktop/ing.ofx") | ||
|
||
page = agent.get("https://secure.ingdirect.ca/InitialINGDirect.html?command=displayLogin&device=web&locale=en_CA") | ||
form = page.form("Signin") | ||
form.ACN = config[:userid] | ||
|
||
# c = WEBrick::Cookie.new("Name", "ING%20DIRECT") | ||
# c.domain = "secure.ingdirect.ca" | ||
# c.path = "/" | ||
# agent.cookie_jar.add(URI.parse("https://secure.ingdirect.ca/"), c) | ||
|
||
form.submit | ||
|
||
page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayChallengeQuestion") | ||
|
||
form = page.form("ChallengeQuestion") | ||
if page.body =~ /On what street did you grow up?/ | ||
form.Answer = config[:street] | ||
elsif page.body =~ /What colour was your first car?/ | ||
form.Answer = config[:car] | ||
elsif page.body =~ /What is your favourite colour?/ | ||
form.Answer = config[:colour] | ||
end | ||
|
||
form.submit | ||
page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayPINPad") | ||
|
||
form = page.form("Signin") | ||
form.PIN = config[:password] | ||
|
||
form.submit | ||
page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayAccountSummary") | ||
|
||
page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayInitialDownLoadTransactionsCommand") | ||
form = page.form("MainForm") | ||
form.ACCT = config[:account_id] | ||
form.DOWNLOADTYPE = "OFX" | ||
submit_button = nil | ||
form.buttons.each {|b| submit_button = b if b.name = "YES, I WANT TO CONTINUE." } | ||
|
||
agent.submit(form, submit_button) | ||
page = agent.get("https://secure.ingdirect.ca/INGDirect.html?command=displayDownLoadTransactionsCommand&fill=1") | ||
page = agent.click(page.links.text("DownLoad")) | ||
|
||
data = page.body | ||
|
||
if data =~ /<STMTTRN>/ | ||
return data | ||
else | ||
return "" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
class PcFinancial < BankAccount | ||
def name | ||
"PC account #{config[:account_id]}" | ||
end | ||
|
||
def download_data | ||
# return File.read("#{ENV['HOME']}/Desktop/PCF.qfx") | ||
|
||
page = agent.get("https://www.txn.banking.pcfinancial.ca/a/authentication/preSignOn.ams") | ||
form = page.form("SignOnForm") | ||
form.cardNumber = config[:userid] | ||
form.password = config[:password] | ||
|
||
page = agent.submit(form, form.buttons.first) | ||
|
||
page = agent.get("https://www.txn.banking.pcfinancial.ca/a/banking/accounts/downloadTransactions1.ams") | ||
form = page.form("DownloadTransactionsForm") | ||
form.fields.name('fromAccount').value = config[:account_id] | ||
submit_button = nil | ||
form.buttons.each {|b| submit_button = b if b.value = "Download transactions" } | ||
|
||
page = agent.submit(form, submit_button) | ||
|
||
data = page.body | ||
|
||
if data =~ /There are no transactions found that met your request/ | ||
return "" | ||
else | ||
return data | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
@gdata_user = 'you@gmail.com' | ||
@gdata_pass = 'secret' | ||
@gs_key = 'abc123' | ||
|
||
@accounts = [ | ||
PcFinancial.new(:worksheet => "1", :userid => "userid", :password => "password", :account_id => "account_id"), | ||
CibcVisa.new(:worksheet => "2", :userid => "userid", :password => "password"), | ||
IngCanada.new(:worksheet => "3", :userid => "userid", :password => "password", :street => "mystreet", :colour => "purple", :car => "red", :account_id => "account_id") | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#!/usr/bin/env ruby | ||
|
||
require "#{File.dirname(__FILE__)}/lib/account_base" | ||
require "#{File.dirname(__FILE__)}/lib/bank_account" | ||
require "#{File.dirname(__FILE__)}/lib/credit_card_account" | ||
require "#{File.dirname(__FILE__)}/lib/spreadsheet" | ||
|
||
Dir["#{File.dirname(__FILE__)}/accounts/*"].each do |file| | ||
require file | ||
end | ||
|
||
require "#{ENV['HOME']}/.gmoney/config.rb" | ||
|
||
spreadsheet = Spreadsheet.new(@gs_key, @gdata_user, @gdata_pass) | ||
|
||
@accounts.each do |account| | ||
begin | ||
worksheet = account.config[:worksheet] | ||
puts "Getting data from #{account.name}..." | ||
txns = account.txns | ||
puts "Inserting into spreadsheet..." if account.txns.any? | ||
txns.each do |txn| | ||
retried = false | ||
begin | ||
spreadsheet.add_row(worksheet, txn) | ||
rescue Exception => e | ||
if retried | ||
puts "error: #{e.inspect}" | ||
else | ||
retried = true | ||
puts "error: #{e.inspect}, retrying..." | ||
retry | ||
end | ||
end | ||
print "." | ||
end | ||
puts "" | ||
rescue Exception => e | ||
puts "ERROR: \n#{e.inspect}\n#{e.backtrace.join("\n")}" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
require "rubygems" | ||
require "mechanize" | ||
require "ofx-parser" | ||
|
||
class AccountBase | ||
attr :config, true | ||
|
||
def initialize(config) | ||
self.config = config | ||
end | ||
|
||
def get_data | ||
return if @data | ||
|
||
@data = download_data | ||
|
||
if @data == "" | ||
puts "No new transactions." | ||
else | ||
puts "downloaded data:" | ||
puts "#{@data}" | ||
end | ||
|
||
return if @data == "" | ||
|
||
@data.gsub!(/\r\n/, "\n") | ||
@data.gsub!(/:$/, ": ") | ||
@data.gsub!(/&/, "") | ||
|
||
@ofx = OfxParser::OfxParser.parse(@data) | ||
account = ofx_account(@ofx) | ||
|
||
if account && account.statement | ||
@ofx_txns = account.statement.transactions | ||
@balance = account.balance | ||
end | ||
end | ||
|
||
def txns | ||
get_data | ||
return [] unless @ofx_txns | ||
|
||
txns = @ofx_txns.collect do |t| | ||
{ | ||
(t.amount.to_f >= 0 ? "deposit" : "withdrawal") => t.amount, | ||
"chequenumber" => t.check_number, | ||
"date" => t.date.strftime("%Y-%m-%d"), | ||
"transactionid" => t.fit_id, | ||
"memo" => t.memo, | ||
"payee" => t.payee, | ||
"siccode" => t.sic, | ||
"type" => t.type | ||
} | ||
end | ||
if txns.size > 0 | ||
txns.last["balance"] = @balance | ||
end | ||
txns | ||
end | ||
|
||
def get_cookie(name) | ||
agent.cookies.each do |c| | ||
if c.to_s =~ /#{name}=(.*)/ | ||
return $1 | ||
end | ||
end | ||
return nil | ||
end | ||
|
||
def agent | ||
@agent ||= WWW::Mechanize.new | ||
@agent.user_agent_alias = 'Mac FireFox' | ||
@agent | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class BankAccount < AccountBase | ||
def ofx_account(ofx) | ||
ofx.bank_account | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class CreditCardAccount < AccountBase | ||
def ofx_account(ofx) | ||
ofx.credit_card | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# Adapted from http://rubyforge.org/projects/gdata-ruby | ||
|
||
require "rubygems" | ||
require "net/http" | ||
|
||
module Net | ||
class HTTPS < HTTP | ||
def initialize(address, port = nil) | ||
super(address, port) | ||
self.use_ssl = true | ||
end | ||
end | ||
end | ||
|
||
class Spreadsheet | ||
GOOGLE_LOGIN_URL = URI.parse('https://www.google.com/accounts/ClientLogin') | ||
|
||
def initialize(spreadsheet_id, email, password) | ||
$VERBOSE = nil | ||
|
||
@spreadsheet_id = spreadsheet_id | ||
service = 'wise' | ||
source = 'gdata-ruby' | ||
@url = 'spreadsheets.google.com' | ||
|
||
response = Net::HTTPS.post_form(GOOGLE_LOGIN_URL, | ||
{'Email' => email, | ||
'Passwd' => password, | ||
'source' => source, | ||
'service' => service }) | ||
|
||
response.error! unless response.kind_of? Net::HTTPSuccess | ||
|
||
@headers = { | ||
'Authorization' => "GoogleLogin auth=#{response.body.split(/=/).last}", | ||
'Content-Type' => 'application/atom+xml' | ||
} | ||
end | ||
|
||
def visibility | ||
@headers ? 'private' : 'public' | ||
end | ||
|
||
# def set_worksheet(name) | ||
# path = "/feeds/worksheets/#{@spreadsheet_id}/#{visibility}/full" | ||
# doc = Hpricot(get(path)) | ||
# result = (doc/"entry/link[@rel='self'][href]") | ||
# pp result | ||
# end | ||
|
||
def request(path) | ||
response, data = get(path) | ||
data | ||
end | ||
|
||
def get(path) | ||
response, data = http.get(path, @headers) | ||
raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess | ||
data | ||
end | ||
|
||
def post(path, entry) | ||
response = http.post(path, entry, @headers) | ||
raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess | ||
response | ||
end | ||
|
||
def put(path, entry) | ||
h = @headers | ||
h['X-HTTP-Method-Override'] = 'PUT' # just to be nice, add the method override | ||
response = http.put(path, entry, h) | ||
raise "error: #{response.inspect}, #{response.body}" unless response.kind_of? Net::HTTPSuccess | ||
response | ||
end | ||
|
||
def http | ||
conn = Net::HTTP.new(@url, 80) | ||
#conn.set_debug_output $stderr | ||
conn | ||
end | ||
|
||
def add_row(worksheet, hash) | ||
path = "/feeds/list/#{@spreadsheet_id}/#{worksheet}/#{visibility}/full" | ||
|
||
entry = "<?xml version='1.0' ?><entry xmlns='http://www.w3.org/2005/Atom' xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>" | ||
hash.each_pair do |k,v| | ||
entry += "<gsx:#{k}>#{v}</gsx:#{k}>" | ||
end | ||
entry += "</entry>" | ||
|
||
post(path, entry) | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
Provide a template or automatic way to set up the Google spreadsheet with spending reports and calculations. | ||
|
||
use gdata token? | ||
|
||
store downloaded data in case of exception | ||
|
||
easily allow input from file instead of download | ||
- maybe if a specially-named file exists use it? |