reckon 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b58a745c730c0dfaf022a98c19dfaf8568c0b3091548d70c9202f8f7c0d78ada
4
- data.tar.gz: 030d2036524a19c5eff981608133e8aac15f185b57a891e91a6f1fdabd3796c7
3
+ metadata.gz: f25c7df49774d944ebc40a73374ac593ebeac2e0b0a201e0d29f9c5ace2642e9
4
+ data.tar.gz: 27acd960b5210e7e0fb88c50acf35def784411d0b8112f0f0b8e6c4e2f5ab621
5
5
  SHA512:
6
- metadata.gz: 1daf76a19078f1707e847a7ac54aad5836ee3d49ce9c2f612bfefeb86f8c1bf9c9d1f97b7bbecb309f9f44f8097f8f2dbe0d6b6062bc376a62fc88f45bab1dfc
7
- data.tar.gz: 4784ac871dbdb6160dc53ef8a7f5d69f31ccc7f7f7ea876b97bf60941abb2d13a968f5229747dd74d0b574ecc12326485e1eaba7ea368a91162d36d3ef4f9f86
6
+ metadata.gz: ddd5fe0ef08b2718e8083cdbd9695e936737b9ff6b258808787192496675f9a371c302e9936069019821f1c865a3280488aa4396185f1e0bf6e7d84b78dc5c0a
7
+ data.tar.gz: f611a48dad98a6a4764518dcb2e72969898d6dfbdaf5e06a77f36ab071429ac6f3bd0b0f51200dc8fbaaca913a95519a6bb38e6ec203dc0af61d861802d101f6
data/.gitignore CHANGED
@@ -25,3 +25,6 @@ pkg
25
25
  .idea
26
26
  reckon_local
27
27
  private_tests
28
+
29
+ ## Bundler
30
+ vendor
@@ -1 +1 @@
1
- 2.5
1
+ 2.0.0-p648
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/cantino/reckon/tree/0.6.0) (2020-09-03)
4
+
5
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.4...0.6.0)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - \[BUG\] Reckon appears not to be parsing ISO standard date yyyy-mm-dd? [\#85](https://github.com/cantino/reckon/issues/85)
10
+
11
+ **Closed issues:**
12
+
13
+ - duplicate detection [\#16](https://github.com/cantino/reckon/issues/16)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Add ability to add note to transaction when entering it [\#89](https://github.com/cantino/reckon/pull/89) ([benprew](https://github.com/benprew))
18
+
19
+ ## [v0.5.4](https://github.com/cantino/reckon/tree/v0.5.4) (2020-06-05)
20
+
21
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.3...v0.5.4)
22
+
23
+ **Fixed bugs:**
24
+
25
+ - order of transactions [\#88](https://github.com/cantino/reckon/issues/88)
26
+ - Is reckon failing to handle comments when learning? [\#87](https://github.com/cantino/reckon/issues/87)
27
+
28
+ ## [v0.5.3](https://github.com/cantino/reckon/tree/v0.5.3) (2020-05-02)
29
+
30
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.2...v0.5.3)
31
+
32
+ **Closed issues:**
33
+
34
+ - \[FEATURE REQUEST\] Ask for currency of Account and output in output file in standard format of xxxx TLA for currency [\#84](https://github.com/cantino/reckon/issues/84)
35
+
36
+ ## [v0.5.2](https://github.com/cantino/reckon/tree/v0.5.2) (2020-03-07)
37
+
38
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.1...v0.5.2)
39
+
40
+ **Closed issues:**
41
+
42
+ - \[Bug\]? Reckon fails to run on ruby 2.7.0 on Catalina [\#83](https://github.com/cantino/reckon/issues/83)
43
+ - --account-tokens issue [\#51](https://github.com/cantino/reckon/issues/51)
44
+
45
+ ## [v0.5.1](https://github.com/cantino/reckon/tree/v0.5.1) (2020-02-25)
46
+
47
+ [Full Changelog](https://github.com/cantino/reckon/compare/v0.5.0...v0.5.1)
48
+
49
+ **Closed issues:**
50
+
51
+ - Error Importing [\#64](https://github.com/cantino/reckon/issues/64)
52
+
53
+ **Merged pull requests:**
54
+
55
+ - guard against rows that don't parse dates [\#82](https://github.com/cantino/reckon/pull/82) ([benprew](https://github.com/benprew))
56
+
3
57
  ## [v0.5.0](https://github.com/cantino/reckon/tree/v0.5.0) (2020-02-19)
4
58
 
5
59
  [Full Changelog](https://github.com/cantino/reckon/compare/v0.4.4...v0.5.0)
@@ -144,7 +198,6 @@
144
198
 
145
199
  **Merged pull requests:**
146
200
 
147
- - Updated the sources to allow for custom curreny [\#11](https://github.com/cantino/reckon/pull/11) ([ghost](https://github.com/ghost))
148
201
  - Add --account option on the commandline [\#10](https://github.com/cantino/reckon/pull/10) ([copiousfreetime](https://github.com/copiousfreetime))
149
202
 
150
203
  ## [v0.3.6](https://github.com/cantino/reckon/tree/v0.3.6) (2013-04-30)
@@ -178,6 +231,7 @@
178
231
 
179
232
  **Merged pull requests:**
180
233
 
234
+ - Updated the sources to allow for custom curreny [\#11](https://github.com/cantino/reckon/pull/11) ([ghost](https://github.com/ghost))
181
235
  - adds support for Nordea csv files [\#1](https://github.com/cantino/reckon/pull/1) ([x2q](https://github.com/x2q))
182
236
 
183
237
  ## [v0.3.3](https://github.com/cantino/reckon/tree/v0.3.3) (2013-01-13)
@@ -1,11 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reckon (0.5.0)
4
+ reckon (0.6.0)
5
5
  chronic (>= 0.3.0)
6
6
  highline (>= 1.5.2)
7
7
  rchardet (>= 1.8.0)
8
- terminal-table (>= 1.4.2)
9
8
 
10
9
  GEM
11
10
  remote: http://rubygems.org/
@@ -34,9 +33,6 @@ GEM
34
33
  diff-lcs (>= 1.2.0, < 2.0)
35
34
  rspec-support (~> 3.9.0)
36
35
  rspec-support (3.9.2)
37
- terminal-table (1.8.0)
38
- unicode-display_width (~> 1.1, >= 1.1.1)
39
- unicode-display_width (1.6.1)
40
36
 
41
37
  PLATFORMS
42
38
  ruby
data/README.md CHANGED
@@ -8,7 +8,7 @@ Reckon automagically converts CSV files for use with the command-line accounting
8
8
 
9
9
  Assuming you have Ruby and [Rubygems](http://rubygems.org/pages/download) installed on your system, simply run
10
10
 
11
- (sudo) gem install reckon
11
+ gem install --user reckon
12
12
 
13
13
  ## Example Usage
14
14
 
@@ -4,18 +4,16 @@ require 'rubygems'
4
4
  require 'rchardet'
5
5
  require 'chronic'
6
6
  require 'csv'
7
- require 'highline/import'
7
+ require 'highline'
8
8
  require 'optparse'
9
- require 'terminal-table'
10
9
  require 'time'
11
10
  require 'logger'
12
11
 
13
- LOGGER = Logger.new(STDOUT)
14
- LOGGER.level = Logger::ERROR
15
-
16
12
  require_relative 'reckon/version'
13
+ require_relative 'reckon/logger'
17
14
  require_relative 'reckon/cosine_similarity'
18
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "app"))
19
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "ledger_parser"))
20
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "csv_parser"))
21
- require File.expand_path(File.join(File.dirname(__FILE__), "reckon", "money"))
15
+ require_relative 'reckon/date_column'
16
+ require_relative 'reckon/money'
17
+ require_relative 'reckon/ledger_parser'
18
+ require_relative 'reckon/csv_parser'
19
+ require_relative 'reckon/app'
@@ -1,18 +1,19 @@
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
11
  def initialize(options = {})
10
12
  LOGGER.level = Logger::INFO if options[:verbose]
11
13
  self.options = options
12
14
  self.regexps = {}
13
- self.seen = {}
15
+ self.seen = Set.new
14
16
  self.options[:currency] ||= '$'
15
- options[:string] = File.read(options[:file]) unless options[:string]
16
17
  @csv_parser = CSVParser.new( options )
17
18
  @matcher = CosineSimilarity.new(options)
18
19
  learn!
@@ -20,22 +21,19 @@ module Reckon
20
21
 
21
22
  def interactive_output(str)
22
23
  return if options[:unattended]
24
+
23
25
  puts str
24
26
  end
25
27
 
26
28
  def learn!
27
29
  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))
30
+ learn_from_ledger_file(options[:existing_ledger_file])
33
31
  end
34
32
 
35
33
  def learn_from_account_tokens(filename)
36
34
  return unless filename
37
35
 
38
- fail "#{filename} doesn't exist!" unless File.exists?(filename)
36
+ raise "#{filename} doesn't exist!" unless File.exist?(filename)
39
37
 
40
38
  extract_account_tokens(YAML.load_file(filename)).each do |account, tokens|
41
39
  tokens.each do |t|
@@ -48,14 +46,27 @@ module Reckon
48
46
  end
49
47
  end
50
48
 
51
- def learn_from(ledger)
49
+ def learn_from_ledger_file(ledger_file)
50
+ return unless ledger_file
51
+
52
+ raise "#{ledger_file} doesn't exist!" unless File.exist?(ledger_file)
53
+
54
+ learn_from_ledger(File.read(ledger_file))
55
+ end
56
+
57
+ def learn_from_ledger(ledger)
58
+ LOGGER.info "learning from #{ledger}"
52
59
  LedgerParser.new(ledger).entries.each do |entry|
53
60
  entry[:accounts].each do |account|
54
61
  str = [entry[:desc], account[:amount]].join(" ")
55
- @matcher.add_document(account[:name], str) unless account[:name] == options[:bank_account]
62
+ if account[:name] != options[:bank_account]
63
+ LOGGER.info "adding document #{account[:name]} #{str}"
64
+ @matcher.add_document(account[:name], str)
65
+ end
56
66
  pretty_date = entry[:date].iso8601
57
- seen[pretty_date] ||= {}
58
- seen[pretty_date][@csv_parser.pretty_money(account[:amount])] = true
67
+ if account[:name] == options[:bank_account]
68
+ seen << seen_key(pretty_date, @csv_parser.pretty_money(account[:amount]))
69
+ end
59
70
  end
60
71
  end
61
72
  end
@@ -91,9 +102,10 @@ module Reckon
91
102
  end
92
103
 
93
104
  def walk_backwards
105
+ cmd_options = "[account]/[q]uit/[s]kip/[n]ote/[d]escription"
94
106
  seen_anything_new = false
95
107
  each_row_backwards do |row|
96
- interactive_output Terminal::Table.new(:rows => [ [ row[:pretty_date], row[:pretty_money], row[:description] ] ])
108
+ print_transaction([row])
97
109
 
98
110
  if already_seen?(row)
99
111
  interactive_output "NOTE: This row is very similar to a previous one!"
@@ -105,50 +117,28 @@ module Reckon
105
117
  seen_anything_new = true
106
118
  end
107
119
 
108
- possible_answers = suggest(row)
109
-
110
- ledger = if row[:money] > 0
111
- if options[:unattended]
112
- out_of_account = possible_answers.first || options[:default_outof_account] || 'Income:Unknown'
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]] )
120
+ if row[:money] > 0
121
+ # out_of_account
122
+ answer = ask_account_question("Which account provided this income? (#{cmd_options})", row)
123
+ line1 = [options[:bank_account], row[:pretty_money]]
124
+ line2 = [answer, ""]
130
125
  else
131
- if options[:unattended]
132
- into_account = possible_answers.first || options[:default_into_account] || 'Expenses:Unknown'
133
- else
134
- into_account = ask("To which account did this money go? ([account]/[q]uit/[s]kip) ") { |q|
135
- q.completion = possible_answers
136
- q.readline = true
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
126
+ # into_account
127
+ answer = ask_account_question("To which account did this money go? (#{cmd_options})", row)
128
+ # line1 = [answer, row[:pretty_money_negated]]
129
+ line1 = [answer, ""]
130
+ line2 = [options[:bank_account], row[:pretty_money]]
131
+ end
145
132
 
146
- ledger_format( row,
147
- [into_account, row[:pretty_money_negated]],
148
- [options[:bank_account], row[:pretty_money]] )
133
+ finish if %w[quit q].include?(answer)
134
+ if %w[skip s].include?(answer)
135
+ interactive_output "Skipping"
136
+ next
149
137
  end
150
138
 
151
- learn_from(ledger) unless options[:account_tokens_file]
139
+ ledger = ledger_format(row, line1, line2)
140
+ LOGGER.info "ledger line: #{ledger}"
141
+ learn_from_ledger(ledger) unless options[:account_tokens_file]
152
142
  output(ledger)
153
143
  end
154
144
  end
@@ -156,6 +146,10 @@ module Reckon
156
146
  def each_row_backwards
157
147
  rows = []
158
148
  (0...@csv_parser.columns.first.length).to_a.each do |index|
149
+ if @csv_parser.date_for(index).nil?
150
+ LOGGER.warn("Skipping row: '#{@csv_parser.row(index)}' that doesn't have a valid date")
151
+ next
152
+ end
159
153
  rows << { :date => @csv_parser.date_for(index),
160
154
  :pretty_date => @csv_parser.pretty_date_for(index),
161
155
  :pretty_money => @csv_parser.pretty_money_for(index),
@@ -163,18 +157,93 @@ module Reckon
163
157
  :money => @csv_parser.money_for(index),
164
158
  :description => @csv_parser.description_for(index) }
165
159
  end
166
- rows.sort { |a, b| a[:date] <=> b[:date] }.each do |row|
167
- yield row
160
+ rows.sort_by { |n| n[:date] }.each { |row| yield row }
161
+ end
162
+
163
+ def print_transaction(rows)
164
+ str = "\n"
165
+ header = %w[Date Amount Description Note]
166
+ maxes = header.map(&:length)
167
+
168
+ rows = rows.map { |r| [r[:pretty_date], r[:pretty_money], r[:description], r[:note]] }
169
+
170
+ rows.each do |r|
171
+ r.length.times { |i| l = r[i] ? r[i].length : 0; maxes[i] = l if maxes[i] < l }
168
172
  end
173
+
174
+ header.each_with_index do |n, i|
175
+ str += " #{n.center(maxes[i])} |"
176
+ end
177
+ str += "\n"
178
+
179
+ rows.each do |row|
180
+ row.each_with_index do |_, i|
181
+ just = maxes[i]
182
+ str += sprintf(" %#{just}s |", row[i])
183
+ end
184
+ str += "\n"
185
+ end
186
+
187
+ interactive_output str
188
+ end
189
+
190
+ def ask_account_question(msg, row)
191
+ possible_answers = suggest(row)
192
+ LOGGER.info "possible_answers===> #{possible_answers.inspect}"
193
+
194
+ if options[:unattended]
195
+ default = if row[:pretty_money][0] == '-'
196
+ options[:default_into_account] || 'Expenses:Unknown'
197
+ else
198
+ options[:default_outof_account] || 'Income:Unknown'
199
+ end
200
+ return possible_answers[0] || default
201
+ end
202
+
203
+ answer = @@cli.ask(msg) do |q|
204
+ q.completion = possible_answers
205
+ q.readline = true
206
+ q.default = possible_answers.first
207
+ end
208
+
209
+ # if answer isn't n/note/d/description, must be an account name, or skip, or quit
210
+ return answer unless %w[n note d description].include?(answer)
211
+
212
+ add_description(row) if %w[d description].include?(answer)
213
+ add_note(row) if %w[n note].include?(answer)
214
+
215
+ print_transaction([row])
216
+ # give user a chance to set account name or retry description
217
+ return ask_account_question(msg, row)
169
218
  end
170
219
 
171
- def most_specific_regexp_match( row )
220
+ def add_description(row)
221
+ desc_answer = @@cli.ask("Enter a new description for this transaction (empty line aborts)\n") do |q|
222
+ q.overwrite = true
223
+ q.readline = true
224
+ q.default = row[:description]
225
+ end
226
+
227
+ row[:description] = desc_answer unless desc_answer.empty?
228
+ end
229
+
230
+ def add_note(row)
231
+ desc_answer = @@cli.ask("Enter a new note for this transaction (empty line aborts)\n") do |q|
232
+ q.overwrite = true
233
+ q.readline = true
234
+ q.default = row[:note]
235
+ end
236
+
237
+ row[:note] = desc_answer unless desc_answer.empty?
238
+ end
239
+
240
+ def most_specific_regexp_match(row)
172
241
  matches = regexps.map { |regexp, account|
173
242
  if match = regexp.match(row[:description])
174
243
  [account, match[0]]
175
244
  end
176
245
  }.compact
177
- matches.sort_by! { |account, matched_text| matched_text.length }.map(&:first)
246
+ matches.sort_by! { |_account, matched_text| matched_text.length }.map(&:first)
178
247
  end
179
248
 
180
249
  def suggest(row)
@@ -183,9 +252,9 @@ module Reckon
183
252
  end
184
253
 
185
254
  def ledger_format(row, line1, line2)
186
- out = "#{row[:pretty_date]}\t#{row[:description]}\n"
187
- out += "\t#{line1.first}\t\t\t\t\t#{line1.last}\n"
188
- out += "\t#{line2.first}\t\t\t\t\t#{line2.last}\n\n"
255
+ out = "#{row[:pretty_date]}\t#{row[:description]}\t; #{row[:note]}\n"
256
+ out += "\t#{line1.first}\t\t\t#{line1.last}\n"
257
+ out += "\t#{line2.first}\t\t\t#{line2.last}\n\n"
189
258
  out
190
259
  end
191
260
 
@@ -194,8 +263,12 @@ module Reckon
194
263
  options[:output_file].flush
195
264
  end
196
265
 
266
+ def seen_key(date, amount)
267
+ return [date, amount].join("|")
268
+ end
269
+
197
270
  def already_seen?(row)
198
- seen[row[:pretty_date]] && seen[row[:pretty_date]][row[:pretty_money]]
271
+ seen.include?(seen_key(row[:pretty_date], row[:pretty_money]))
199
272
  end
200
273
 
201
274
  def finish
@@ -205,13 +278,11 @@ module Reckon
205
278
  end
206
279
 
207
280
  def output_table
208
- output = Terminal::Table.new do |t|
209
- t.headings = 'Date', 'Amount', 'Description'
210
- each_row_backwards do |row|
211
- t << [ row[:pretty_date], row[:pretty_money], row[:description] ]
212
- end
281
+ rows = []
282
+ each_row_backwards do |row|
283
+ rows << row
213
284
  end
214
- interactive_output output
285
+ print_transaction(rows)
215
286
  end
216
287
 
217
288
  def self.parse_opts(args = ARGV)
@@ -319,7 +390,7 @@ module Reckon
319
390
  end
320
391
 
321
392
  unless options[:file]
322
- options[:file] = ask("What CSV file should I parse? ")
393
+ options[:file] = @@cli.ask("What CSV file should I parse? ")
323
394
  unless options[:file].length > 0
324
395
  puts "\nYou must provide a CSV file to parse.\n"
325
396
  puts parser
@@ -330,7 +401,7 @@ module Reckon
330
401
  unless options[:bank_account]
331
402
  fail "Please specify --account for the unattended mode" if options[:unattended]
332
403
 
333
- options[:bank_account] = ask("What is the account name of this bank account in Ledger? ") do |q|
404
+ options[:bank_account] = @@cli.ask("What is the account name of this bank account in Ledger? ") do |q|
334
405
  q.readline = true
335
406
  q.validate = /^.{2,}$/
336
407
  q.default = "Assets:Bank:Checking"