reckon 0.5.4 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +50 -0
- data/.gitignore +3 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +73 -4
- data/Gemfile.lock +1 -45
- data/README.md +84 -33
- data/Rakefile +17 -1
- data/bin/build-new-version.sh +26 -0
- data/bin/reckon +6 -1
- data/lib/reckon.rb +2 -2
- data/lib/reckon/app.rb +140 -194
- data/lib/reckon/csv_parser.rb +2 -7
- data/lib/reckon/date_column.rb +10 -0
- data/lib/reckon/money.rb +48 -48
- data/lib/reckon/options.rb +149 -0
- data/lib/reckon/version.rb +1 -1
- data/reckon.gemspec +1 -3
- data/spec/integration/another_bank_example/input.csv +9 -0
- data/spec/integration/another_bank_example/output.ledger +36 -0
- data/spec/integration/another_bank_example/test_args +1 -0
- data/spec/integration/austrian_example/input.csv +13 -0
- data/spec/integration/austrian_example/output.ledger +52 -0
- data/spec/integration/austrian_example/test_args +2 -0
- data/spec/integration/bom_utf8_file/input.csv +3 -0
- data/spec/integration/bom_utf8_file/output.ledger +4 -0
- data/spec/integration/bom_utf8_file/test_args +3 -0
- data/spec/integration/broker_canada_example/input.csv +12 -0
- data/spec/integration/broker_canada_example/output.ledger +48 -0
- data/spec/integration/broker_canada_example/test_args +1 -0
- data/spec/integration/chase/account_tokens_and_regex/output.ledger +36 -0
- data/spec/integration/chase/account_tokens_and_regex/test_args +2 -0
- data/spec/integration/chase/account_tokens_and_regex/tokens.yml +16 -0
- data/spec/integration/chase/default_account_names/output.ledger +36 -0
- data/spec/integration/chase/default_account_names/test_args +3 -0
- data/spec/integration/chase/input.csv +9 -0
- data/spec/integration/chase/learn_from_existing/learn.ledger +7 -0
- data/spec/integration/chase/learn_from_existing/output.ledger +36 -0
- data/spec/integration/chase/learn_from_existing/test_args +1 -0
- data/spec/integration/chase/simple/output.ledger +36 -0
- data/spec/integration/chase/simple/test_args +1 -0
- data/spec/integration/danish_kroner_nordea_example/input.csv +6 -0
- data/spec/integration/danish_kroner_nordea_example/output.ledger +24 -0
- data/spec/integration/danish_kroner_nordea_example/test_args +1 -0
- data/spec/integration/english_date_example/input.csv +3 -0
- data/spec/integration/english_date_example/output.ledger +12 -0
- data/spec/integration/english_date_example/test_args +1 -0
- data/spec/integration/extratofake/input.csv +24 -0
- data/spec/integration/extratofake/output.ledger +92 -0
- data/spec/integration/extratofake/test_args +1 -0
- data/spec/integration/french_example/input.csv +9 -0
- data/spec/integration/french_example/output.ledger +36 -0
- data/spec/integration/french_example/test_args +2 -0
- data/spec/integration/german_date_example/input.csv +3 -0
- data/spec/integration/german_date_example/output.ledger +12 -0
- data/spec/integration/german_date_example/test_args +1 -0
- data/spec/integration/harder_date_example/input.csv +5 -0
- data/spec/integration/harder_date_example/output.ledger +20 -0
- data/spec/integration/harder_date_example/test_args +1 -0
- data/spec/integration/ing/input.csv +3 -0
- data/spec/integration/ing/output.ledger +12 -0
- data/spec/integration/ing/test_args +1 -0
- data/spec/integration/intuit_mint_example/input.csv +7 -0
- data/spec/integration/intuit_mint_example/output.ledger +28 -0
- data/spec/integration/intuit_mint_example/test_args +1 -0
- data/spec/integration/invalid_header_example/input.csv +6 -0
- data/spec/integration/invalid_header_example/output.ledger +8 -0
- data/spec/integration/invalid_header_example/test_args +1 -0
- data/spec/integration/inversed_credit_card/input.csv +16 -0
- data/spec/integration/inversed_credit_card/output.ledger +64 -0
- data/spec/integration/inversed_credit_card/test_args +1 -0
- data/spec/integration/nationwide/input.csv +4 -0
- data/spec/integration/nationwide/output.ledger +16 -0
- data/spec/integration/nationwide/test_args +1 -0
- data/spec/integration/regression/issue_51_account_tokens/input.csv +8 -0
- data/spec/integration/regression/issue_51_account_tokens/output.ledger +32 -0
- data/spec/integration/regression/issue_51_account_tokens/test_args +4 -0
- data/spec/integration/regression/issue_51_account_tokens/tokens.yml +9 -0
- data/spec/integration/regression/issue_64_date_column/input.csv +3 -0
- data/spec/integration/regression/issue_64_date_column/output.ledger +8 -0
- data/spec/integration/regression/issue_64_date_column/test_args +1 -0
- data/spec/integration/regression/issue_73_account_token_matching/input.csv +2 -0
- data/spec/integration/regression/issue_73_account_token_matching/output.ledger +4 -0
- data/spec/integration/regression/issue_73_account_token_matching/test_args +6 -0
- data/spec/integration/regression/issue_73_account_token_matching/tokens.yml +8 -0
- data/spec/integration/regression/issue_85_date_example/input.csv +2 -0
- data/spec/integration/regression/issue_85_date_example/output.ledger +8 -0
- data/spec/integration/regression/issue_85_date_example/test_args +1 -0
- data/spec/integration/spanish_date_example/input.csv +3 -0
- data/spec/integration/spanish_date_example/output.ledger +12 -0
- data/spec/integration/spanish_date_example/test_args +1 -0
- data/spec/integration/suntrust/input.csv +7 -0
- data/spec/integration/suntrust/output.ledger +28 -0
- data/spec/integration/suntrust/test_args +1 -0
- data/spec/integration/test.sh +83 -0
- data/spec/integration/test_money_column/input.csv +3 -0
- data/spec/integration/test_money_column/output.ledger +8 -0
- data/spec/integration/test_money_column/test_args +1 -0
- data/spec/integration/two_money_columns/input.csv +5 -0
- data/spec/integration/two_money_columns/output.ledger +20 -0
- data/spec/integration/two_money_columns/test_args +1 -0
- data/spec/integration/yyyymmdd_date_example/input.csv +1 -0
- data/spec/integration/yyyymmdd_date_example/output.ledger +4 -0
- data/spec/integration/yyyymmdd_date_example/test_args +1 -0
- data/spec/reckon/app_spec.rb +25 -7
- data/spec/reckon/ledger_parser_spec.rb +2 -2
- data/spec/reckon/money_column_spec.rb +24 -24
- data/spec/reckon/money_spec.rb +13 -32
- data/spec/reckon/options_spec.rb +17 -0
- data/spec/spec_helper.rb +6 -1
- metadata +97 -35
- data/.travis.yml +0 -13
data/bin/reckon
CHANGED
data/lib/reckon.rb
CHANGED
|
@@ -4,9 +4,8 @@ require 'rubygems'
|
|
|
4
4
|
require 'rchardet'
|
|
5
5
|
require 'chronic'
|
|
6
6
|
require 'csv'
|
|
7
|
-
require 'highline
|
|
7
|
+
require 'highline'
|
|
8
8
|
require 'optparse'
|
|
9
|
-
require 'terminal-table'
|
|
10
9
|
require 'time'
|
|
11
10
|
require 'logger'
|
|
12
11
|
|
|
@@ -17,4 +16,5 @@ require_relative 'reckon/date_column'
|
|
|
17
16
|
require_relative 'reckon/money'
|
|
18
17
|
require_relative 'reckon/ledger_parser'
|
|
19
18
|
require_relative 'reckon/csv_parser'
|
|
19
|
+
require_relative 'reckon/options'
|
|
20
20
|
require_relative 'reckon/app'
|
data/lib/reckon/app.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
|
|
2
3
|
require 'pp'
|
|
3
4
|
require 'yaml'
|
|
4
5
|
|
|
5
6
|
module Reckon
|
|
6
7
|
class App
|
|
7
8
|
attr_accessor :options, :seen, :csv_parser, :regexps, :matcher
|
|
9
|
+
@@cli = HighLine.new
|
|
8
10
|
|
|
9
|
-
def initialize(
|
|
11
|
+
def initialize(opts = {})
|
|
12
|
+
self.options = opts
|
|
10
13
|
LOGGER.level = Logger::INFO if options[:verbose]
|
|
11
|
-
|
|
14
|
+
|
|
12
15
|
self.regexps = {}
|
|
13
|
-
self.seen =
|
|
16
|
+
self.seen = Set.new
|
|
14
17
|
self.options[:currency] ||= '$'
|
|
15
|
-
options[:string] = File.read(options[:file]) unless options[:string]
|
|
16
18
|
@csv_parser = CSVParser.new( options )
|
|
17
19
|
@matcher = CosineSimilarity.new(options)
|
|
18
20
|
learn!
|
|
@@ -20,22 +22,19 @@ module Reckon
|
|
|
20
22
|
|
|
21
23
|
def interactive_output(str)
|
|
22
24
|
return if options[:unattended]
|
|
25
|
+
|
|
23
26
|
puts str
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def learn!
|
|
27
30
|
learn_from_account_tokens(options[:account_tokens_file])
|
|
28
|
-
|
|
29
|
-
ledger_file = options[:existing_ledger_file]
|
|
30
|
-
return unless ledger_file
|
|
31
|
-
fail "#{ledger_file} doesn't exist!" unless File.exists?(ledger_file)
|
|
32
|
-
learn_from(File.read(ledger_file))
|
|
31
|
+
learn_from_ledger_file(options[:existing_ledger_file])
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def learn_from_account_tokens(filename)
|
|
36
35
|
return unless filename
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
raise "#{filename} doesn't exist!" unless File.exist?(filename)
|
|
39
38
|
|
|
40
39
|
extract_account_tokens(YAML.load_file(filename)).each do |account, tokens|
|
|
41
40
|
tokens.each do |t|
|
|
@@ -48,14 +47,27 @@ module Reckon
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
def
|
|
50
|
+
def learn_from_ledger_file(ledger_file)
|
|
51
|
+
return unless ledger_file
|
|
52
|
+
|
|
53
|
+
raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
|
|
54
|
+
|
|
55
|
+
learn_from_ledger(File.read(ledger_file))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def learn_from_ledger(ledger)
|
|
59
|
+
LOGGER.info "learning from #{ledger}"
|
|
52
60
|
LedgerParser.new(ledger).entries.each do |entry|
|
|
53
61
|
entry[:accounts].each do |account|
|
|
54
62
|
str = [entry[:desc], account[:amount]].join(" ")
|
|
55
|
-
|
|
63
|
+
if account[:name] != options[:bank_account]
|
|
64
|
+
LOGGER.info "adding document #{account[:name]} #{str}"
|
|
65
|
+
@matcher.add_document(account[:name], str)
|
|
66
|
+
end
|
|
56
67
|
pretty_date = entry[:date].iso8601
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if account[:name] == options[:bank_account]
|
|
69
|
+
seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount]))
|
|
70
|
+
end
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
73
|
end
|
|
@@ -91,9 +103,10 @@ module Reckon
|
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
def walk_backwards
|
|
106
|
+
cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription"
|
|
94
107
|
seen_anything_new = false
|
|
95
108
|
each_row_backwards do |row|
|
|
96
|
-
|
|
109
|
+
print_transaction([row])
|
|
97
110
|
|
|
98
111
|
if already_seen?(row)
|
|
99
112
|
interactive_output "NOTE: This row is very similar to a previous one!"
|
|
@@ -105,50 +118,28 @@ module Reckon
|
|
|
105
118
|
seen_anything_new = true
|
|
106
119
|
end
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
else
|
|
114
|
-
out_of_account = ask("Which account provided this income? ([account]/[q]uit/[s]kip) ") { |q|
|
|
115
|
-
q.completion = possible_answers
|
|
116
|
-
q.readline = true
|
|
117
|
-
q.default = possible_answers.first
|
|
118
|
-
}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
finish if out_of_account == "quit" || out_of_account == "q"
|
|
122
|
-
if out_of_account == "skip" || out_of_account == "s"
|
|
123
|
-
interactive_output "Skipping"
|
|
124
|
-
next
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
ledger_format( row,
|
|
128
|
-
[options[:bank_account], row[:pretty_money]],
|
|
129
|
-
[out_of_account, row[:pretty_money_negated]] )
|
|
121
|
+
if row[:money] > 0
|
|
122
|
+
# out_of_account
|
|
123
|
+
answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
|
|
124
|
+
line1 = [options[:bank_account], row[:pretty_money]]
|
|
125
|
+
line2 = [answer, ""]
|
|
130
126
|
else
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
q.default = possible_answers.first
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
finish if into_account == "quit" || into_account == 'q'
|
|
141
|
-
if into_account == "skip" || into_account == 's'
|
|
142
|
-
interactive_output "Skipping"
|
|
143
|
-
next
|
|
144
|
-
end
|
|
127
|
+
# into_account
|
|
128
|
+
answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
|
|
129
|
+
# line1 = [answer, row[:pretty_money_negated]]
|
|
130
|
+
line1 = [answer, ""]
|
|
131
|
+
line2 = [options[:bank_account], row[:pretty_money]]
|
|
132
|
+
end
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
finish if %w[quit q].include?(answer)
|
|
135
|
+
if %w[skip s].include?(answer)
|
|
136
|
+
interactive_output "Skipping"
|
|
137
|
+
next
|
|
149
138
|
end
|
|
150
139
|
|
|
151
|
-
|
|
140
|
+
ledger = ledger_format(row, line1, line2)
|
|
141
|
+
LOGGER.info "ledger line: #{ledger}"
|
|
142
|
+
learn_from_ledger(ledger) unless options[:account_tokens_file]
|
|
152
143
|
output(ledger)
|
|
153
144
|
end
|
|
154
145
|
end
|
|
@@ -167,16 +158,95 @@ module Reckon
|
|
|
167
158
|
:money => @csv_parser.money_for(index),
|
|
168
159
|
:description => @csv_parser.description_for(index) }
|
|
169
160
|
end
|
|
170
|
-
rows.sort_by { |n| n[:date] }.each {|row| yield row }
|
|
161
|
+
rows.sort_by { |n| [n[:date], -n[:money], n[:description]] }.each { |row| yield row }
|
|
171
162
|
end
|
|
172
163
|
|
|
173
|
-
def
|
|
164
|
+
def print_transaction(rows)
|
|
165
|
+
str = "\n"
|
|
166
|
+
header = %w[Date Amount Description Note]
|
|
167
|
+
maxes = header.map(&:length)
|
|
168
|
+
|
|
169
|
+
rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
|
|
170
|
+
|
|
171
|
+
rows.each do |r|
|
|
172
|
+
r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
header.each_with_index do |n, i|
|
|
176
|
+
str += " #{n.center(maxes[i])} |"
|
|
177
|
+
end
|
|
178
|
+
str += "\n"
|
|
179
|
+
|
|
180
|
+
rows.each do |row|
|
|
181
|
+
row.each_with_index do |_, i|
|
|
182
|
+
just = maxes[i]
|
|
183
|
+
str += sprintf(" %#{just}s |", row[i])
|
|
184
|
+
end
|
|
185
|
+
str += "\n"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
interactive_output str
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def ask_account_question(msg, row)
|
|
192
|
+
possible_answers = suggest(row)
|
|
193
|
+
LOGGER.info "possible_answers===> #{possible_answers.inspect}"
|
|
194
|
+
|
|
195
|
+
if options[:unattended]
|
|
196
|
+
if options[:fail_on_unknown_account] && possible_answers.empty?
|
|
197
|
+
raise %(Couldn't find any matches for '#{row[:description]}'
|
|
198
|
+
Try adding an account token with --account-tokens)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
default = options[:default_outof_account]
|
|
202
|
+
default = options[:default_into_account] if row[:pretty_money][0] == '-'
|
|
203
|
+
return possible_answers[0] || default
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
answer = @@cli.ask(msg) do |q|
|
|
207
|
+
q.completion = possible_answers
|
|
208
|
+
q.readline = true
|
|
209
|
+
q.default = possible_answers.first
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# if answer isn't n/note/d/description, must be an account name, or skip, or quit
|
|
213
|
+
return answer unless %w[n note d description].include?(answer)
|
|
214
|
+
|
|
215
|
+
add_description(row) if %w[d description].include?(answer)
|
|
216
|
+
add_note(row) if %w[n note].include?(answer)
|
|
217
|
+
|
|
218
|
+
print_transaction([row])
|
|
219
|
+
# give user a chance to set account name or retry description
|
|
220
|
+
return ask_account_question(msg, row)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def add_description(row)
|
|
224
|
+
desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
|
|
225
|
+
q.overwrite = true
|
|
226
|
+
q.readline = true
|
|
227
|
+
q.default = row[:description]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
row[:description] = desc_answer unless desc_answer.empty?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def add_note(row)
|
|
234
|
+
desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
|
|
235
|
+
q.overwrite = true
|
|
236
|
+
q.readline = true
|
|
237
|
+
q.default = row[:note]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
row[:note] = desc_answer unless desc_answer.empty?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def most_specific_regexp_match(row)
|
|
174
244
|
matches = regexps.map { |regexp, account|
|
|
175
245
|
if match = regexp.match(row[:description])
|
|
176
246
|
[account, match[0]]
|
|
177
247
|
end
|
|
178
248
|
}.compact
|
|
179
|
-
matches.sort_by! { |
|
|
249
|
+
matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
|
|
180
250
|
end
|
|
181
251
|
|
|
182
252
|
def suggest(row)
|
|
@@ -185,9 +255,9 @@ module Reckon
|
|
|
185
255
|
end
|
|
186
256
|
|
|
187
257
|
def ledger_format(row, line1, line2)
|
|
188
|
-
out = "#{row[:pretty_date]}\t#{row[:description]}\n"
|
|
189
|
-
out += "\t#{line1.first}\t\t\t
|
|
190
|
-
out += "\t#{line2.first}\t\t\t
|
|
258
|
+
out = "#{row[:pretty_date]}\t#{row[:description]}#{row[:note] ? "\t; " + row[:note]: ""}\n"
|
|
259
|
+
out += "\t#{line1.first}\t\t\t#{line1.last}\n"
|
|
260
|
+
out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
|
|
191
261
|
out
|
|
192
262
|
end
|
|
193
263
|
|
|
@@ -196,8 +266,12 @@ module Reckon
|
|
|
196
266
|
options[:output_file].flush
|
|
197
267
|
end
|
|
198
268
|
|
|
269
|
+
def seen_key(date, amount)
|
|
270
|
+
return [date, amount].join("|")
|
|
271
|
+
end
|
|
272
|
+
|
|
199
273
|
def already_seen?(row)
|
|
200
|
-
seen
|
|
274
|
+
seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
|
|
201
275
|
end
|
|
202
276
|
|
|
203
277
|
def finish
|
|
@@ -207,139 +281,11 @@ module Reckon
|
|
|
207
281
|
end
|
|
208
282
|
|
|
209
283
|
def output_table
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
interactive_output output
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def self.parse_opts(args = ARGV)
|
|
220
|
-
options = { :output_file => STDOUT }
|
|
221
|
-
parser = OptionParser.new do |opts|
|
|
222
|
-
opts.banner = "Usage: Reckon.rb [options]"
|
|
223
|
-
opts.separator ""
|
|
224
|
-
|
|
225
|
-
opts.on("-f", "--file FILE", "The CSV file to parse") do |file|
|
|
226
|
-
options[:file] = file
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
opts.on("-a", "--account NAME", "The Ledger Account this file is for") do |a|
|
|
230
|
-
options[:bank_account] = a
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
|
234
|
-
options[:verbose] = v
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
opts.on("-i", "--inverse", "Use the negative of each amount") do |v|
|
|
238
|
-
options[:inverse] = v
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
opts.on("-p", "--print-table", "Print out the parsed CSV in table form") do |p|
|
|
242
|
-
options[:print_table] = p
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
opts.on("-o", "--output-file FILE", "The ledger file to append to") do |o|
|
|
246
|
-
options[:output_file] = File.open(o, 'a')
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
opts.on("-l", "--learn-from FILE", "An existing ledger file to learn accounts from") do |l|
|
|
250
|
-
options[:existing_ledger_file] = l
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
opts.on("", "--ignore-columns 1,2,5", "Columns to ignore in the CSV file - the first column is column 1") do |ignore|
|
|
254
|
-
options[:ignore_columns] = ignore.split(",").map { |i| i.to_i }
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
opts.on("", "--money-column 2", Integer, "Specify the money column instead of letting Reckon guess - the first column is column 1") do |column_number|
|
|
258
|
-
options[:money_column] = column_number
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
opts.on("", "--date-column 3", Integer, "Specify the date column instead of letting Reckon guess - the first column is column 1") do |column_number|
|
|
262
|
-
options[:date_column] = column_number
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
opts.on("", "--contains-header [N]", "The first row of the CSV is a header and should be skipped. Optionally add the number of rows to skip.") do |contains_header|
|
|
266
|
-
options[:contains_header] = 1
|
|
267
|
-
options[:contains_header] = contains_header.to_i if contains_header
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
opts.on("", "--csv-separator ','", "Separator for parsing the CSV - default is comma.") do |csv_separator|
|
|
271
|
-
options[:csv_separator] = csv_separator
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
opts.on("", "--comma-separates-cents", "Use comma instead of period to deliminate dollars from cents when parsing ($100,50 instead of $100.50)") do |c|
|
|
275
|
-
options[:comma_separates_cents] = c
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
opts.on("", "--encoding 'UTF-8'", "Specify an encoding for the CSV file; not usually needed") do |e|
|
|
279
|
-
options[:encoding] = e
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
opts.on("-c", "--currency '$'", "Currency symbol to use, defaults to $ (£, EUR)") do |e|
|
|
283
|
-
options[:currency] = e
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
opts.on("", "--date-format '%d/%m/%Y'", "Force the date format (see Ruby DateTime strftime)") do |d|
|
|
287
|
-
options[:date_format] = d
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
opts.on("-u", "--unattended", "Don't ask questions and guess all the accounts automatically. Used with --learn-from or --account-tokens options.") do |n|
|
|
291
|
-
options[:unattended] = n
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
opts.on("-t", "--account-tokens FILE", "YAML file with manually-assigned tokens for each account (see README)") do |a|
|
|
295
|
-
options[:account_tokens_file] = a
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
opts.on("", "--default-into-account NAME", "Default into account") do |a|
|
|
299
|
-
options[:default_into_account] = a
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
opts.on("", "--default-outof-account NAME", "Default 'out of' account") do |a|
|
|
303
|
-
options[:default_outof_account] = a
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
opts.on("", "--suffixed", "If --currency should be used as a suffix. Defaults to false.") do |e|
|
|
307
|
-
options[:suffixed] = e
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
|
311
|
-
puts opts
|
|
312
|
-
exit
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
opts.on_tail("--version", "Show version") do
|
|
316
|
-
puts VERSION
|
|
317
|
-
exit
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
opts.parse!(args)
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
unless options[:file]
|
|
324
|
-
options[:file] = ask("What CSV file should I parse? ")
|
|
325
|
-
unless options[:file].length > 0
|
|
326
|
-
puts "\nYou must provide a CSV file to parse.\n"
|
|
327
|
-
puts parser
|
|
328
|
-
exit
|
|
329
|
-
end
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
unless options[:bank_account]
|
|
333
|
-
fail "Please specify --account for the unattended mode" if options[:unattended]
|
|
334
|
-
|
|
335
|
-
options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
|
|
336
|
-
q.readline = true
|
|
337
|
-
q.validate = /^.{2,}$/
|
|
338
|
-
q.default = "Assets:Bank:Checking"
|
|
339
|
-
end
|
|
284
|
+
rows = []
|
|
285
|
+
each_row_backwards do |row|
|
|
286
|
+
rows << row
|
|
340
287
|
end
|
|
341
|
-
|
|
342
|
-
options
|
|
288
|
+
print_transaction(rows)
|
|
343
289
|
end
|
|
344
290
|
end
|
|
345
291
|
end
|