bankjob 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/PostInstall.txt +4 -0
- data/README.rdoc +77 -0
- data/bin/bankjob +10 -0
- data/lib/bankjob.rb +12 -0
- data/lib/bankjob/bankjob_runner.rb +184 -0
- data/lib/bankjob/cli.rb +258 -0
- data/lib/bankjob/payee.rb +114 -0
- data/lib/bankjob/scraper.rb +495 -0
- data/lib/bankjob/statement.rb +355 -0
- data/lib/bankjob/support.rb +217 -0
- data/lib/bankjob/transaction.rb +400 -0
- data/scrapers/base_scraper.rb +133 -0
- data/scrapers/bpi_scraper.rb +190 -0
- data/spec/bankjob_cli_spec.rb +15 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/statement_spec.rb +121 -0
- data/spec/transaction_spec.rb +81 -0
- metadata +114 -0
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'builder'
|
3
|
+
require 'fastercsv'
|
4
|
+
require 'bankjob'
|
5
|
+
|
6
|
+
module Bankjob
|
7
|
+
|
8
|
+
##
|
9
|
+
# A Statement object represents a bank statement and is generally the result of running a Bankjob scraper.
|
10
|
+
# The Statement holds an array of Transaction objects and specifies the closing balance and the currency in use.
|
11
|
+
#
|
12
|
+
# A Scraper will create a Statement by scraping web pages in an online banking site.
|
13
|
+
# The Statement can then be stored as a file in CSV (Comma Separated Values) format
|
14
|
+
# using +to_csv+ or in OFX (Open Financial eXchange http://www.ofx.net) format
|
15
|
+
# using +to_ofx+.
|
16
|
+
#
|
17
|
+
# One special ability of Statement is the ability to merge with an existing statement,
|
18
|
+
# automatically eliminating overlapping transactions.
|
19
|
+
# This means that when writing subsequent statements to the same CSV file
|
20
|
+
# <em>(note well: CSV only)</em> a continous transaction record can be built up
|
21
|
+
# over a long period.
|
22
|
+
#
|
23
|
+
class Statement
|
24
|
+
|
25
|
+
# OFX value for the ACCTTYPE of a checking account
|
26
|
+
CHECKING = "CHECKING"
|
27
|
+
|
28
|
+
# OFX value for the ACCTTYPE of a savings account
|
29
|
+
SAVINGS = "SAVINGS"
|
30
|
+
|
31
|
+
# OFX value for the ACCTTYPE of a money market account
|
32
|
+
MONEYMRKT = "MONEYMRKT"
|
33
|
+
|
34
|
+
# OFX value for the ACCTTYPE of a loan account
|
35
|
+
CREDITLINE = "CREDITLINE"
|
36
|
+
|
37
|
+
# the account balance after the last transaction in the statement
|
38
|
+
# Translates to the OFX element BALAMT in LEDGERBAL
|
39
|
+
attr_accessor :closing_balance
|
40
|
+
|
41
|
+
# the avaliable funds in the account after the last transaction in the statement (generally the same as closing_balance)
|
42
|
+
# Translates to the OFX element BALAMT in AVAILBAL
|
43
|
+
attr_accessor :closing_available
|
44
|
+
|
45
|
+
# the array of Transaction objects that comprise the statement
|
46
|
+
attr_accessor :transactions
|
47
|
+
|
48
|
+
# the three-letter currency symbol generated into the OFX output (defaults to EUR)
|
49
|
+
# This is passed into the initializer (usually by the Scraper - see Scraper#currency)
|
50
|
+
attr_reader :currency
|
51
|
+
|
52
|
+
# the identifier of the bank - a 1-9 char string (may be empty)
|
53
|
+
# Translates to the OFX element BANKID
|
54
|
+
attr_accessor :bank_id
|
55
|
+
|
56
|
+
# the account number of the statement - a 1-22 char string that must be passed
|
57
|
+
# into the initalizer of the Statement
|
58
|
+
# Translates to the OFX element ACCTID
|
59
|
+
attr_accessor :account_number
|
60
|
+
|
61
|
+
# the type of bank account the statement is for
|
62
|
+
# Tranlsates to the OFX type ACCTTYPE and must be one of
|
63
|
+
# * CHECKING
|
64
|
+
# * SAVINGS
|
65
|
+
# * MONEYMRKT
|
66
|
+
# * CREDITLINE
|
67
|
+
# Use a constant to set this - defaults to CHECKING
|
68
|
+
attr_accessor :account_type
|
69
|
+
|
70
|
+
##
|
71
|
+
# Creates a new empty Statement with no transactions.
|
72
|
+
# The +account_number+ must be specified as a 1-22 character string.
|
73
|
+
# The specified +currency+ defaults to EUR if nothing is passed in.
|
74
|
+
#
|
75
|
+
def initialize(account_number, currency = "EUR")
|
76
|
+
@account_number = account_number
|
77
|
+
@currency = currency
|
78
|
+
@transactions = []
|
79
|
+
@account_type = CHECKING
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Appends a new Transaction to the end of this Statement
|
84
|
+
#
|
85
|
+
def add_transaction(transaction)
|
86
|
+
@transactions << transaction
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Overrides == to allow comparison of Statement objects.
|
91
|
+
# Two Statements are considered equal (that is, ==) if
|
92
|
+
# and only iff they have the same values for:
|
93
|
+
# * +to_date+
|
94
|
+
# * +from_date+
|
95
|
+
# * +closing_balance+
|
96
|
+
# * +closing_available+
|
97
|
+
# * each and every transaction.
|
98
|
+
# Note that the transactions are compared with Transaction.==
|
99
|
+
#
|
100
|
+
def ==(other) # :nodoc:
|
101
|
+
if other.kind_of?(Statement)
|
102
|
+
return (from_date == other.from_date and
|
103
|
+
to_date == other.to_date and
|
104
|
+
closing_balance == other.closing_balance and
|
105
|
+
closing_available == other.closing_available and
|
106
|
+
transactions == other.transactions)
|
107
|
+
end
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Merges the transactions of +other+ into the transactions of this statement
|
113
|
+
# and returns the resulting array of transactions
|
114
|
+
# Raises an exception if the two statements overlap in a discontiguous fashion.
|
115
|
+
#
|
116
|
+
def merge_transactions(other)
|
117
|
+
if (other.kind_of?(Statement))
|
118
|
+
union = transactions | other.transactions # the set union of both
|
119
|
+
# now check that the union contains all of the originals, otherwise
|
120
|
+
# we have merged some sort of non-contiguous range
|
121
|
+
raise "Failed to merge transactions properly." unless union.first(@transactions.length) == @transactions
|
122
|
+
return union
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Merges the transactions of +other+ into the transactions of this statement
|
128
|
+
# and returns the result.
|
129
|
+
# Neither statement is changed. See #merge! if you want to modify the statement.
|
130
|
+
# Raises an exception if the two statements overlap in a discontiguous fashion.
|
131
|
+
#
|
132
|
+
def merge(other)
|
133
|
+
union = merge_transactions(other)
|
134
|
+
merged = self.dup
|
135
|
+
merged.closing_balance = nil
|
136
|
+
merged.closing_available = nil
|
137
|
+
merged.transactions = union
|
138
|
+
return merged
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Merges the transactions of +other+ into the transactions of this statement.
|
143
|
+
# Causes this statement to be changed. See #merge for details.
|
144
|
+
#
|
145
|
+
def merge!(other)
|
146
|
+
@closing_balance = nil
|
147
|
+
@closing_available = nil
|
148
|
+
@transactions = merge_transactions(other)
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Returns the statement's start date.
|
153
|
+
# The +from_date+ is taken from the date of the last transaction in the statement
|
154
|
+
#
|
155
|
+
def from_date()
|
156
|
+
return nil if @transactions.empty?
|
157
|
+
@transactions.last.date
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Returns the statement's end date.
|
162
|
+
# The +to_date+ is taken from the date of the first transaction in the statement
|
163
|
+
#
|
164
|
+
def to_date()
|
165
|
+
return nil if @transactions.empty?
|
166
|
+
@transactions.first.date
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Returns the closing balance by looking at the
|
171
|
+
# new balance of the first transaction.
|
172
|
+
# If there are no transactions, +nil+ is returned.
|
173
|
+
#
|
174
|
+
def closing_balance()
|
175
|
+
return nil if @closing_balance.nil? and @transactions.empty?
|
176
|
+
@closing_balance ||= @transactions.first.new_balance
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Returns the closing available balance by looking at the
|
181
|
+
# new balance of the first transaction.
|
182
|
+
# If there are no transactions, +nil+ is returned.
|
183
|
+
# Note that this is the same value returned as +closing_balance+.
|
184
|
+
#
|
185
|
+
def closing_available()
|
186
|
+
return nil if @closing_available.nil? and @transactions.empty?
|
187
|
+
@closing_available ||= @transactions.first.new_balance
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Generates a CSV (comma separated values) string with a single
|
192
|
+
# row for each transaction.
|
193
|
+
# Note that no header row is generated as it would make it
|
194
|
+
# difficult to concatenate and merge subsequent CSV strings
|
195
|
+
# (but we should consider it as a user option in the future)
|
196
|
+
#
|
197
|
+
def to_csv
|
198
|
+
buf = ""
|
199
|
+
transactions.each do |transaction|
|
200
|
+
buf << transaction.to_csv
|
201
|
+
end
|
202
|
+
return buf
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# Generates a string for use as a header in a CSV file for a statement.
|
207
|
+
#
|
208
|
+
# Delegates to Transaction#csv_header
|
209
|
+
#
|
210
|
+
def self.csv_header
|
211
|
+
return Transaction.csv_header
|
212
|
+
end
|
213
|
+
|
214
|
+
##
|
215
|
+
# Reads in transactions from a CSV file or string specified by +source+
|
216
|
+
# and adds them to this statement.
|
217
|
+
#
|
218
|
+
# Uses a simple (dumb) heuristic to determine if the +source+ is a file
|
219
|
+
# or a string: if it contains a comma (,) then it is a string
|
220
|
+
# otherwise it is treated as a file path.
|
221
|
+
#
|
222
|
+
def from_csv(source, decimal = ".")
|
223
|
+
if (source =~ /,/)
|
224
|
+
# assume source is a string
|
225
|
+
FasterCSV.parse(source) do |row|
|
226
|
+
add_transaction(Transaction.from_csv(row, decimal))
|
227
|
+
end
|
228
|
+
else
|
229
|
+
# assume source is a filepath
|
230
|
+
FasterCSV.foreach(source) do |row|
|
231
|
+
add_transaction(Transaction.from_csv(row, decimal))
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Generates an XML string adhering to the OFX standard
|
238
|
+
# (see Open Financial eXchange http://www.ofx.net)
|
239
|
+
# representing a single bank statement holding a list
|
240
|
+
# of transactions.
|
241
|
+
# The XML for the individual transactions is generated
|
242
|
+
# by the Transaction class itself.
|
243
|
+
#
|
244
|
+
# The OFX 2 schema for a statement response (STMTRS) is:
|
245
|
+
#
|
246
|
+
# <xsd:complexType name="StatementResponse">
|
247
|
+
# <xsd:annotation>
|
248
|
+
# <xsd:documentation>
|
249
|
+
# The OFX element "STMTRS" is of type "StatementResponse"
|
250
|
+
# </xsd:documentation>
|
251
|
+
# </xsd:annotation>
|
252
|
+
#
|
253
|
+
# <xsd:sequence>
|
254
|
+
# <xsd:element name="CURDEF" type="ofx:CurrencyEnum"/>
|
255
|
+
# <xsd:element name="BANKACCTFROM" type="ofx:BankAccount"/>
|
256
|
+
# <xsd:element name="BANKTRANLIST" type="ofx:BankTransactionList" minOccurs="0"/>
|
257
|
+
# <xsd:element name="LEDGERBAL" type="ofx:LedgerBalance"/>
|
258
|
+
# <xsd:element name="AVAILBAL" type="ofx:AvailableBalance" minOccurs="0"/>
|
259
|
+
# <xsd:element name="BALLIST" type="ofx:BalanceList" minOccurs="0"/>
|
260
|
+
# <xsd:element name="MKTGINFO" type="ofx:InfoType" minOccurs="0"/>
|
261
|
+
# </xsd:sequence>
|
262
|
+
# </xsd:complexType>
|
263
|
+
#
|
264
|
+
# Where the BANKTRANLIST (Bank Transaction List) is defined as:
|
265
|
+
#
|
266
|
+
# <xsd:complexType name="BankTransactionList">
|
267
|
+
# <xsd:annotation>
|
268
|
+
# <xsd:documentation>
|
269
|
+
# The OFX element "BANKTRANLIST" is of type "BankTransactionList"
|
270
|
+
# </xsd:documentation>
|
271
|
+
# </xsd:annotation>
|
272
|
+
# <xsd:sequence>
|
273
|
+
# <xsd:element name="DTSTART" type="ofx:DateTimeType"/>
|
274
|
+
# <xsd:element name="DTEND" type="ofx:DateTimeType"/>
|
275
|
+
# <xsd:element name="STMTTRN" type="ofx:StatementTransaction" minOccurs="0" maxOccurs="unbounded"/>
|
276
|
+
# </xsd:sequence>
|
277
|
+
# </xsd:complexType>
|
278
|
+
#
|
279
|
+
# And this is the definition of the type BankAccount.
|
280
|
+
#
|
281
|
+
# <xsd:complexType name="BankAccount">
|
282
|
+
# <xsd:annotation>
|
283
|
+
# <xsd:documentation>
|
284
|
+
# The OFX elements BANKACCTFROM and BANKACCTTO are of type "BankAccount"
|
285
|
+
# </xsd:documentation>
|
286
|
+
# </xsd:annotation>
|
287
|
+
# <xsd:complexContent>
|
288
|
+
# <xsd:extension base="ofx:AbstractAccount">
|
289
|
+
# <xsd:sequence>
|
290
|
+
# <xsd:element name="BANKID" type="ofx:BankIdType"/>
|
291
|
+
# <xsd:element name="BRANCHID" type="ofx:AccountIdType" minOccurs="0"/>
|
292
|
+
# <xsd:element name="ACCTID" type="ofx:AccountIdType"/>
|
293
|
+
# <xsd:element name="ACCTTYPE" type="ofx:AccountEnum"/>
|
294
|
+
# <xsd:element name="ACCTKEY" type="ofx:AccountIdType" minOccurs="0"/>
|
295
|
+
# </xsd:sequence>
|
296
|
+
# </xsd:extension>
|
297
|
+
# </xsd:complexContent>
|
298
|
+
# </xsd:complexType>
|
299
|
+
#
|
300
|
+
# The to_ofx method will only generate the essential elements which are
|
301
|
+
# * BANKID - the bank identifier (a 1-9 char string - may be empty)
|
302
|
+
# * ACCTID - the account number (a 1-22 char string - may not be empty!)
|
303
|
+
# * ACCTTYPE - the type of account - must be one of:
|
304
|
+
# "CHECKING", "SAVINGS", "MONEYMRKT", "CREDITLINE"
|
305
|
+
#
|
306
|
+
# (See Transaction for a definition of STMTTRN)
|
307
|
+
#
|
308
|
+
def to_ofx
|
309
|
+
buf = ""
|
310
|
+
# Use Builder to generate XML. Builder works by catching missing_method
|
311
|
+
# calls and generating an XML element with the name of the missing method,
|
312
|
+
# nesting according to the nesting of the calls and using arguments for content
|
313
|
+
x = Builder::XmlMarkup.new(:target => buf, :indent => 2)
|
314
|
+
x.OFX {
|
315
|
+
x.BANKMSGSRSV1 { #Bank Message Response
|
316
|
+
x.STMTTRNRS { #Statement-transaction aggregate response
|
317
|
+
x.STMTRS { #Statement response
|
318
|
+
x.CURDEF currency #Currency
|
319
|
+
x.BANKACCTFROM {
|
320
|
+
x.BANKID bank_id # bank identifier
|
321
|
+
x.ACCTID account_number
|
322
|
+
x.ACCTTYPE account_type # acct type: checking/savings/...
|
323
|
+
}
|
324
|
+
x.BANKTRANLIST { #Transactions
|
325
|
+
x.DTSTART Bankjob.date_time_to_ofx(from_date)
|
326
|
+
x.DTEND Bankjob.date_time_to_ofx(to_date)
|
327
|
+
transactions.each { |transaction|
|
328
|
+
buf << transaction.to_ofx
|
329
|
+
}
|
330
|
+
}
|
331
|
+
x.LEDGERBAL { # the final balance at the end of the statement
|
332
|
+
x.BALAMT closing_balance # balance amount
|
333
|
+
x.DTASOF Bankjob.date_time_to_ofx(to_date) # balance date
|
334
|
+
}
|
335
|
+
x.AVAILBAL { # the final Available balance
|
336
|
+
x.BALAMT closing_available
|
337
|
+
x.DTASOF Bankjob.date_time_to_ofx(to_date)
|
338
|
+
}
|
339
|
+
}
|
340
|
+
}
|
341
|
+
}
|
342
|
+
}
|
343
|
+
return buf
|
344
|
+
end
|
345
|
+
|
346
|
+
def to_s
|
347
|
+
buf = "#{self.class}: close_bal = #{closing_balance}, avail = #{closing_available}, curr = #{currency}, transactions:"
|
348
|
+
transactions.each do |tx|
|
349
|
+
buf << "\n\t\t#{tx.to_s}"
|
350
|
+
end
|
351
|
+
buf << "\n---\n"
|
352
|
+
return buf
|
353
|
+
end
|
354
|
+
end # class Statement
|
355
|
+
end # module
|
@@ -0,0 +1,217 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bankjob'
|
4
|
+
|
5
|
+
module Bankjob
|
6
|
+
|
7
|
+
##
|
8
|
+
# Takes a date-time as a string or as a Time or DateTime object and returns
|
9
|
+
# it as either a Time or a DateTime object.
|
10
|
+
#
|
11
|
+
# This is useful in the setter method of a date attribute allowing the date
|
12
|
+
# to be set as any type but stored internally as an object compatible with
|
13
|
+
# conversion through +strftime()+
|
14
|
+
# (Bankjob::Transaction uses this internally in the setter for +date+ for example
|
15
|
+
#
|
16
|
+
def self.create_date_time(date_time_raw)
|
17
|
+
if (date_time_raw.respond_to?(:rfc822)) then
|
18
|
+
# It's already a Time or DateTime
|
19
|
+
return date_time_raw
|
20
|
+
elsif (date_time_raw.to_s.strip.empty?)
|
21
|
+
# Nil or non dates are returned as nil
|
22
|
+
return nil
|
23
|
+
else
|
24
|
+
# Assume it can be converted to a time
|
25
|
+
return Time.parse(date_time_raw.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Takes a Time or DateTime and formats it in the correct format for OFX date elements.
|
31
|
+
#
|
32
|
+
# The OFX format is a string of digits in the format "YYMMDDHHMMSS".
|
33
|
+
# For example, the 1st of February 2009 at 2:34PM and 56 second becomes "20090201143456"
|
34
|
+
#
|
35
|
+
# Note must use a Time, or DateTime, not a String, nor a Date.
|
36
|
+
#
|
37
|
+
def self.date_time_to_ofx(time)
|
38
|
+
time.nil? ? "" : "#{time.strftime( '%Y%m%d%H%M%S' )}"
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Takes a Time or DateTime and formats in a suitable format for comma separated values files.
|
43
|
+
# The format produced is suitable for loading into an Excel-like spreadsheet program
|
44
|
+
# being automatically treated as a date.
|
45
|
+
#
|
46
|
+
# A string is returned with the format "YY-MM-DD HH:MM:SS".
|
47
|
+
# For example, the 1st of February 2009 at 2:34PM and 56 second becomes "2009-02-01 14:34:56"
|
48
|
+
#
|
49
|
+
# Note must use a Time, or DateTime, not a String, nor a Date.
|
50
|
+
#
|
51
|
+
def self.date_time_to_csv(time)
|
52
|
+
time.nil? ? "" : "#{time.strftime( '%Y-%m-%d %H:%M:%S' )}"
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Takes a string and capitalizes the first letter of every word
|
57
|
+
# and forces the rest of the word to be lowercase.
|
58
|
+
#
|
59
|
+
# This is a utility method for use in scrapers to make descriptions
|
60
|
+
# more readable.
|
61
|
+
#
|
62
|
+
def self.capitalize_words(message)
|
63
|
+
message.downcase.gsub(/\b\w/){$&.upcase}
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# converts a numeric +string+ to a float given the specified +decimal+
|
68
|
+
# separator.
|
69
|
+
#
|
70
|
+
def self.string_to_float(string, decimal)
|
71
|
+
return nil if string.nil?
|
72
|
+
amt = string.gsub(/\s/, '')
|
73
|
+
if (decimal == ',') # E.g. "1.000.030,99"
|
74
|
+
amt.gsub!(/\./, '') # strip out . 1000s separator
|
75
|
+
amt.gsub!(/,/, '.') # replace decimal , with .
|
76
|
+
elsif (decimal == '.')
|
77
|
+
amt.gsub!(/,/, '') # strip out comma 1000s separator
|
78
|
+
end
|
79
|
+
return amt.to_f
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def self.wesabe_upload(wesabe_args, ofx_doc, logger)
|
84
|
+
if (wesabe_args.nil? or (wesabe_args.length < 2 and wesabe_args.length > 3))
|
85
|
+
raise "Incorrect number of args for Wesabe (#{wesabe_args}), should be 2 or 3."
|
86
|
+
else
|
87
|
+
load_wesabe
|
88
|
+
wuser, wpass, windex = *wesabe_args
|
89
|
+
wesabe = Wesabe.new(wuser, wpass)
|
90
|
+
num_accounts = wesabe.accounts.length
|
91
|
+
if num_accounts == 0
|
92
|
+
raise "The user \"#{wuser}\" has no Wesabe accounts. Create one at www.wesabe.com before attempting to upload a statement."
|
93
|
+
elsif (not windex.nil? and (num_accounts < windex.to_i))
|
94
|
+
raise "The user \"#{wuser}\" has only #{num_accounts} Wesabe accounts, but the account index #{windex} was specified."
|
95
|
+
elsif windex.nil?
|
96
|
+
if num_accounts > 1
|
97
|
+
raise "The user \"#{wuser}\" has #{num_accounts} Wesabe accounts, so the account index must be specified in the WESABE_ARGS."
|
98
|
+
else
|
99
|
+
# we have only one account, no need to specify the index
|
100
|
+
windex = 1
|
101
|
+
end
|
102
|
+
elsif windex.to_i == 0
|
103
|
+
raise "The Wesabe account index must be between 1 and #{num_accounts}. #{windex} is not acceptable"
|
104
|
+
end
|
105
|
+
logger.debug("Attempting to upload statement to the ##{windex} Wesabe account for user #{wuser}...")
|
106
|
+
# Get the account at the index (which is not necessarily the index in the array
|
107
|
+
# so we use the account(index) method to get it
|
108
|
+
account = wesabe.account(windex.to_i)
|
109
|
+
uploader = account.new_upload
|
110
|
+
uploader.statement = ofx_doc
|
111
|
+
uploader.upload!
|
112
|
+
logger.info("Uploaded statement to Wesabe account #{account.name}, the ##{windex} account for user #{wuser}, with the result: #{uploader.status}")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.wesabe_help(wesabe_args)
|
117
|
+
if (wesabe_args.nil? or wesabe_args.length != 2)
|
118
|
+
puts <<-EOF
|
119
|
+
Wesabe (http://www.wesabe.com) is an online bank account management tool (like Mint)
|
120
|
+
that allows you to upload (in some cases automatically) your bank statements and
|
121
|
+
automatically convert them into a more readable format to allow you to track
|
122
|
+
your spending and much more. Wesabe comes with its own community attached.
|
123
|
+
|
124
|
+
Bankjob has no affiliation with Wesabe, but allows you to upload the statements it
|
125
|
+
generates to your Wesabe account automatically.
|
126
|
+
|
127
|
+
To use Wesabe you need the Wesabe Ruby gem installed:
|
128
|
+
See the gem at http://github.com/wesabe/wesabe-rubygem
|
129
|
+
Install the gem with:
|
130
|
+
$ sudo gem install -r --source http://gems.github.com/ wesabe-wesabe
|
131
|
+
(on Windows, omit the "sudo")
|
132
|
+
|
133
|
+
You also need your Wesabe login name and password, and, if you have
|
134
|
+
more than one account on Wesabe, the id number of the account.
|
135
|
+
This is not a real account number - it's simply a counter that Wesabe uses.
|
136
|
+
If you have a single account it will be '1', if you have two accounts the
|
137
|
+
second account will be '2', etc.
|
138
|
+
|
139
|
+
Bankjob will help you find this number by listing your Wesabe accounts for you.
|
140
|
+
Simply use:
|
141
|
+
bankjob -wesabe_help "username password"
|
142
|
+
(The quotes are important - this is a single argument to Bankjob with two words)
|
143
|
+
|
144
|
+
If you already know the number of the account and you want to start uploading use:
|
145
|
+
|
146
|
+
bankjob [other bankjob args] --wesabe "username password id"
|
147
|
+
|
148
|
+
E.g.
|
149
|
+
bankjob --scraper bpi_scraper.rb --wesabe "bloggsy pw123 2"
|
150
|
+
|
151
|
+
If you only have a single account, you don't need to specify the id number
|
152
|
+
(but Bankjob will check and will fail with an error if you have more than one account)
|
153
|
+
|
154
|
+
bankjob [other bankjob args] --wesabe "username password"
|
155
|
+
|
156
|
+
If in any doubt --wesabe-help "username password" will set you straight.
|
157
|
+
|
158
|
+
Troubleshooting:
|
159
|
+
- If you see an error like Wesabe::Request::Unauthorized, then chances
|
160
|
+
are your username or password for Wesabe is incorrect.
|
161
|
+
|
162
|
+
- If you see an error "end of file reached" then it may be that you are logged
|
163
|
+
into the Wesabe account to which you are trying to upload - perhaps in a browser.
|
164
|
+
In this case, log out from Wesabe in the browser, _wait a minute_, then try again.
|
165
|
+
EOF
|
166
|
+
else
|
167
|
+
load_wesabe
|
168
|
+
begin
|
169
|
+
puts "Connecting to Wesabe...\n"
|
170
|
+
wuser, wpass = *wesabe_args
|
171
|
+
wesabe = Wesabe.new(wuser, wpass)
|
172
|
+
puts "You have #{wesabe.accounts.length} Wesabe accounts:"
|
173
|
+
wesabe.accounts.each do |account|
|
174
|
+
puts " Account Name: #{account.name}"
|
175
|
+
puts " wesabe id: #{account.id}"
|
176
|
+
puts " account no: #{account.number}"
|
177
|
+
puts " type: #{account.type}"
|
178
|
+
puts " balance: #{account.balance}"
|
179
|
+
puts " bank: #{account.financial_institution.name}"
|
180
|
+
puts "To upload to this account use:"
|
181
|
+
puts " bankjob [other bankjob args] --wesabe \"#{wuser} password #{account.id}\""
|
182
|
+
puts ""
|
183
|
+
if wesabe.accounts.length == 1
|
184
|
+
puts "Since you have one account you do not need to specify the id number, use:"
|
185
|
+
puts " bankjob [other bankjob args] --wesabe \"#{wuser} password\""
|
186
|
+
end
|
187
|
+
end
|
188
|
+
rescue Exception => e
|
189
|
+
raise <<-EOF
|
190
|
+
Failed to get Wesabe account information due to: #{e.message}.
|
191
|
+
Check your username and password or use:
|
192
|
+
bankjob --wesabe-help
|
193
|
+
with no arguments for more details.
|
194
|
+
EOF
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end # wesabe_help
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def self.load_wesabe(logger = nil)
|
202
|
+
begin
|
203
|
+
require 'wesabe'
|
204
|
+
rescue LoadError => error
|
205
|
+
msg = <<-EOF
|
206
|
+
Failed to load the Wesabe gem due to #{error.module}
|
207
|
+
See the gem at http://github.com/wesabe/wesabe-rubygem
|
208
|
+
Install the gem with:
|
209
|
+
$ sudo gem install -r --source http://gems.github.com/ wesabe-wesabe
|
210
|
+
EOF
|
211
|
+
logger.fatal(msg) unless logger.nil?
|
212
|
+
raise msg
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end # module Bankjob
|
216
|
+
|
217
|
+
|