ynab_convert 0.1.0.pre → 1.0.3

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: '08086730d5b1a483d48378a5f2a10937accd45b695a4b2f07c94dd0c19e27daf'
4
- data.tar.gz: 9586296b18ee416fd083b9477d58570da2559d1d3b68b3385ce0bd0bb1f6ded2
3
+ metadata.gz: 2af241f007b8495197a8971ffbf3b3efe0dda41411c56a3543452dfea50fd03e
4
+ data.tar.gz: ac7c2614c064e005524c808dd62bddbbaaa8e8395bea6a02b0bf4ef45960133f
5
5
  SHA512:
6
- metadata.gz: 0c0579fda3f49407a92345875fcbb95bb30cd65d4ffe0e59dd1bddecc6e5ee72aeafe592ef566cd756d4a6307b417e85dd85dd9df68a0375c5b410e08a1ed614
7
- data.tar.gz: 5dc8cfa674d17766f7dec08e1b8652f5aa891158355062c5ac75bd33a21bc1ac980401cd5c8eb49fa077081295cc9079de87c8c6cbfbde91a1dc3af66d896bf9
6
+ metadata.gz: 3f96f0a1839d1bc4111f8f53aeac6f7b503155f65b03e2ccd113e0847baa8637beeef7aeae182538b3f6f4cd5caf5a462b568b0ffae5a42f24062722c4a51cf7
7
+ data.tar.gz: 4f17efacff3b4b5cb62df38997863bad691b41a4f26d5a27ef3b7bfaa93072f217f4a824f11dd6e462ba422f862991e5ae14990e7f5f8c8879f49a7deba91274
data/.rubocop.yml CHANGED
@@ -1,6 +1,11 @@
1
1
  ---
2
+ # See https://github.com/rubocop/rubocop/blob/master/config/default.yml for all
3
+ # options
2
4
  require: rubocop-rake
3
5
 
6
+ AllCops:
7
+ DisplayCopNames: true
8
+
4
9
  Metrics/LineLength:
5
10
  Exclude:
6
11
  - ynab_convert.gemspec
data/.travis.yml CHANGED
@@ -4,10 +4,12 @@ cache: bundler
4
4
  rvm:
5
5
  - 2.6
6
6
  before_install: gem install bundler
7
- script: echo "Running tests for $(ruby -v)..." && bundle exec rake ci
8
7
 
9
8
  jobs:
10
9
  include:
10
+ - stage: test
11
+ rvm: 2.6
12
+ script: echo "Running tests for $(ruby -v)..." && bundle exec rake ci
11
13
  - stage: gem release
12
14
  rvm: 2.6
13
15
  script: echo "Publishing to rubygems.org..."
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ynab_convert (0.1.0.pre)
4
+ ynab_convert (1.0.3)
5
5
  i18n
6
6
  slop
7
7
 
@@ -12,7 +12,7 @@ GEM
12
12
  backport (1.1.2)
13
13
  byebug (11.0.1)
14
14
  coderay (1.1.2)
15
- concurrent-ruby (1.1.8)
15
+ concurrent-ruby (1.1.9)
16
16
  diff-lcs (1.3)
17
17
  docile (1.3.2)
18
18
  ffi (1.11.2)
@@ -35,19 +35,20 @@ GEM
35
35
  guard (~> 2.0)
36
36
  rubocop (~> 0.20)
37
37
  htmlentities (4.3.4)
38
- i18n (1.8.9)
38
+ i18n (1.10.0)
39
39
  concurrent-ruby (~> 1.0)
40
40
  jaro_winkler (1.5.4)
41
- json (2.2.0)
41
+ json (2.5.1)
42
42
  listen (3.2.0)
43
43
  rb-fsevent (~> 0.10, >= 0.10.3)
44
44
  rb-inotify (~> 0.9, >= 0.9.10)
45
45
  lumberjack (1.0.13)
46
46
  method_source (0.9.2)
47
- mini_portile2 (2.4.0)
47
+ mini_portile2 (2.8.0)
48
48
  nenv (0.3.0)
49
- nokogiri (1.10.9)
50
- mini_portile2 (~> 2.4.0)
49
+ nokogiri (1.13.3)
50
+ mini_portile2 (~> 2.8.0)
51
+ racc (~> 1.4)
51
52
  notiffany (0.1.3)
52
53
  nenv (~> 0.1)
53
54
  shellany (~> 0.0)
@@ -60,6 +61,7 @@ GEM
60
61
  pry-byebug (3.7.0)
61
62
  byebug (~> 11.0)
62
63
  pry (~> 0.10)
64
+ racc (1.6.0)
63
65
  rainbow (3.0.0)
64
66
  rake (13.0.1)
65
67
  rb-fsevent (0.10.3)
@@ -96,7 +98,7 @@ GEM
96
98
  json (>= 1.8, < 3)
97
99
  simplecov-html (~> 0.10.0)
98
100
  simplecov-html (0.10.2)
99
- slop (4.8.2)
101
+ slop (4.9.1)
100
102
  solargraph (0.37.2)
101
103
  backport (~> 1.1)
102
104
  bundler (>= 1.17.2)
@@ -132,4 +134,4 @@ DEPENDENCIES
132
134
  ynab_convert!
133
135
 
134
136
  BUNDLED WITH
135
- 2.2.11
137
+ 2.3.8
data/Guardfile CHANGED
@@ -74,7 +74,7 @@ group :red_green_refactor, halt_on_fail: true do
74
74
  end
75
75
  end
76
76
 
77
- guard :rubocop, cli: ['--auto-correct'] do
77
+ guard :rubocop, cli: ['--auto-correct', '--display-cop-names'] do
78
78
  watch('Gemfile')
79
79
  watch('Rakefile')
80
80
  watch('bin/convert')
data/README.md CHANGED
@@ -27,6 +27,7 @@ latest one on 2019-12-01.
27
27
  `-i` argument | Institution's full name | Institution's website | Remarks
28
28
  ---|---|---|---
29
29
  `example` | Example Bank | N/A | Reference processor implementation, not a real institution
30
+ `n26` | N26 | [n26.com](n26.com) | N26 CSV statements
30
31
  `revolut` | Revolut Ltd | [revolut.com](https://www.revolut.com/) | The processor isn't aware of currencies. Make sure the statements processed with `revolut` are in the same currency that your YNAB is in
31
32
  `ubs_chequing` | UBS Switzerland (private banking) | [ubs.ch](https://ubs.ch) | Private chequing and joint accounts
32
33
  `ubs_credit` | UBS Switzerland (credit cards) | [ubs.ch](https://ubs.ch) | Both MasterCard and Visa
@@ -44,7 +45,8 @@ https://github.com/coaxial/ynab_convert.
44
45
 
45
46
  ### Enable debug output
46
47
 
47
- Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task `spec:debug`. Debug logging goes to STDERR.
48
+ Run `ynab_convert` with `YNAB_CONVERT_DEBUG=true`, or use the rake task
49
+ `spec:debug`. Debug logging goes to STDERR.
48
50
 
49
51
  ### Adding a new financial institution
50
52
 
@@ -6,7 +6,7 @@ module CoreExtensions
6
6
  # Adds convenience methods
7
7
  module Inflections
8
8
  def snake_case
9
- downcase.tr(' ', '_').gsub(/[^a-z_]/, '')
9
+ downcase.tr(' ', '_').gsub(/[^a-z_0-9]/, '')
10
10
  end
11
11
 
12
12
  def camel_case
@@ -14,12 +14,29 @@ module Processor
14
14
  attr_reader :loader_options
15
15
 
16
16
  # @option opts [String] :file Path to the CSV file to process
17
- def initialize(opts)
17
+ # @option opts [Symbol] :format YNAB4 format to use, one of :flows or
18
+ # :amounts. :flows is useful for CSVs with separate debit and credit
19
+ # columns, :amounts is for CSVs with only one amount columns and +/-
20
+ # numbers. See
21
+ # https://docs.youneedabudget.com/article/921-formatting-csv-file
22
+ def initialize(options)
23
+ default_options = { file: '', format: :flows }
24
+ opts = default_options.merge(options)
25
+
18
26
  logger.debug "Initializing processor with options: `#{opts.to_h}'"
19
27
  raise ::Errno::ENOENT unless File.exist? opts[:file]
20
28
 
21
29
  @file = opts[:file]
22
- @headers = { transaction_date: nil, payee: nil, debit: nil, credit: nil }
30
+ @headers = { transaction_date: nil, payee: nil }
31
+ @format = opts[:format]
32
+
33
+ if @format == :amounts
34
+ amounts_columns = { amount: nil }
35
+ @headers.merge!(amounts_columns)
36
+ else
37
+ flows_columns = { inflow: nil, outflow: nil }
38
+ @headers.merge!(flows_columns)
39
+ end
23
40
  end
24
41
 
25
42
  def to_ynab!
@@ -38,15 +55,41 @@ module Processor
38
55
 
39
56
  attr_accessor :statement_from, :statement_to, :headers
40
57
 
41
- def inflow_or_outflow_missing?(row)
58
+ def amount_invalid?(row)
59
+ amount_index = 3
60
+
61
+ # If there is no amount,
62
+ # then the row is invalid.
63
+ row[amount_index].nil? || row[amount_index].empty?
64
+ end
65
+
66
+ def inflow_outflow_invalid?(row)
42
67
  inflow_index = 3
43
68
  outflow_index = 4
44
- # If there is neither inflow and outflow values, or their value is 0,
45
- # then the row is not valid to YNAB4
46
- (row[inflow_index].nil? || row[inflow_index].empty? ||
47
- row[inflow_index] == '0.00') &&
48
- (row[outflow_index].nil? || row[outflow_index].empty? ||
49
- row[outflow_index] == '0.00')
69
+
70
+ # If there is neither inflow and outflow values,
71
+ # or both the inflow and outflow amounts are 0,
72
+ # then the row is invalid.
73
+ (
74
+ row[inflow_index].nil? ||
75
+ row[inflow_index].empty? ||
76
+ row[inflow_index] == '0.00'
77
+ ) && (
78
+ row[outflow_index].nil? ||
79
+ row[outflow_index].empty? ||
80
+ row[outflow_index] == '0.00'
81
+ )
82
+ end
83
+
84
+ def amounts_missing?(row)
85
+ logger.debug "Checking for missing amount in `#{row}`"
86
+ if @format == :amounts
87
+ logger.debug 'Using `:amounts`'
88
+ amount_invalid?(row)
89
+ else
90
+ logger.debug 'Using `:flows`'
91
+ inflow_outflow_invalid?(row)
92
+ end
50
93
  end
51
94
 
52
95
  def skip_row(row)
@@ -96,6 +139,7 @@ module Processor
96
139
  def convert!
97
140
  logger.debug "Will write to `#{temp_filename}'"
98
141
 
142
+ logger.debug(loader_options)
99
143
  CSV.open(temp_filename, 'wb', output_options) do |converted|
100
144
  CSV.foreach(@file, 'rb', loader_options) do |row|
101
145
  logger.debug "Parsing row: `#{row.to_h}'"
@@ -103,7 +147,7 @@ module Processor
103
147
  catch :skip_row do
104
148
  extract_header_names(row)
105
149
  ynab_row = transformers(row)
106
- if inflow_or_outflow_missing?(ynab_row) ||
150
+ if amounts_missing?(ynab_row) ||
107
151
  transaction_date_missing?(ynab_row)
108
152
  logger.debug 'Empty row, skipping it'
109
153
  skip_row(row)
@@ -151,7 +195,17 @@ module Processor
151
195
  end
152
196
 
153
197
  def ynab_headers
154
- %w[Date Payee Memo Outflow Inflow]
198
+ common_headers = %w[Date Payee Memo]
199
+
200
+ if @format == :amounts
201
+ amounts_headers = %w[Amount]
202
+ common_headers.concat(amounts_headers)
203
+ else
204
+ flows_headers = %w[Outflow Inflow]
205
+ common_headers.concat(flows_headers)
206
+ end
207
+
208
+ common_headers
155
209
  end
156
210
 
157
211
  def output_options
@@ -14,7 +14,7 @@ module Processor
14
14
  # if it's redundant. For instance, this parser is for "Example Bank" but it's
15
15
  # named "example.rb", its corresponding spec is
16
16
  # "spec/example_processor_spec.rb" and its fixture would be
17
- # "spec/fixtures/example.csv"
17
+ # "spec/fixtures/example/statement.csv"
18
18
  class Example < Processor::Base
19
19
  # @option options [String] :file Path to the CSV file to process
20
20
  def initialize(options)
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Processor
4
+ # Processes CSV files from N26
5
+ #
6
+ # An example of how to implement a custom processor
7
+ # Processes CSV files with this format:
8
+ # <<~ROWS
9
+ # "Date","Payee","Memo","Outflow","Inflow"
10
+ # "23/12/2019","coaxial","","1000000.00",""
11
+ # "30/12/2019","Santa","","50000.00",""
12
+ # "02/02/2020","Someone Else","","45.00",""
13
+ # ROWS
14
+ # The file name for the processor should be the institution name in
15
+ # camel case. It's ok to skip "Bank" or "Credit Union" when naming the file
16
+ # if it's redundant. For instance, this parser is for "Example Bank" but it's
17
+ # named "example.rb", its corresponding spec is
18
+ # "spec/example_processor_spec.rb" and its fixture would be
19
+ # "spec/fixtures/example.csv"
20
+ class N26 < Processor::Base
21
+ # @option options [String] :file Path to the CSV file to process
22
+ def initialize(options)
23
+ # Custom converters can be added so that the CSV data is parsed when
24
+ # loading the original file
25
+ register_custom_converters
26
+
27
+ # These are the options for the CSV module (see
28
+ # https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new)
29
+ # They should match the format for the CSV file that the financial
30
+ # institution generates.
31
+ @loader_options = {
32
+ col_sep: ',',
33
+ quote_char: '"',
34
+ # Use your converters, if any
35
+ # converters: %i[],
36
+ headers: true,
37
+ encoding: 'bom|utf-8'
38
+ }
39
+
40
+ # This is the financial institution's full name as it calls itself. This
41
+ # usually matches the institution's letterhead and/or commercial name.
42
+ # It can happen that the same institution needs different parsers because
43
+ # its credit card CSV files are in one format, and its chequing accounts
44
+ # in another. In that case, more details can be added in parens.
45
+ # For instance:
46
+ # 'Example Bank (credit cards)' and 'Example Bank (chequing)'
47
+ @institution_name = 'N26 Bank'
48
+ # N26's CSV only has one columns for all transactions instead of separate
49
+ # debit and credit columns
50
+ additional_processor_options = { format: :amounts }
51
+
52
+ # This is mandatory.
53
+ super(options.merge(additional_processor_options))
54
+ end
55
+
56
+ private
57
+
58
+ def register_custom_converters; end
59
+
60
+ protected
61
+
62
+ def transformers(row)
63
+ transaction_date = row[headers[:transaction_date]]
64
+ payee = row[headers[:payee]]
65
+ amount = row[headers[:amount]]
66
+
67
+ converted_row = [transaction_date, payee, nil, amount]
68
+ logger.debug "Converted row: #{converted_row}"
69
+ converted_row
70
+ end
71
+
72
+ private
73
+
74
+ # Institutions love translating the column names, apparently. Rather than
75
+ # hardcoding the column name as a string, use the headers array at the
76
+ # right index.
77
+ # These lookups aren't particularly expensive but they're done on each row
78
+ # so why not memoize them with ||=
79
+ def extract_header_names(row)
80
+ headers[:transaction_date] ||= row.headers[0]
81
+ headers[:payee] ||= row.headers[1]
82
+ headers[:amount] ||= row.headers[5]
83
+ end
84
+ end
85
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Processor
4
- # Processes CSV files from UBS Personal Banking Switzerland (French)
4
+ # Processes CSV files from UBS Personal Banking Switzerland
5
5
  class UbsChequing < Processor::Base
6
6
  # @option options [String] :file Path to the CSV file to process
7
7
  def initialize(options)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Processor
4
- # Processes CSV files from UBS Credit Cards Switzerland (French)
4
+ # Processes CSV files from UBS Credit Cards Switzerland
5
5
  class UbsCredit < Processor::Base
6
6
  # @option options [String] :file Path to the CSV file to process
7
7
  def initialize(options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YnabConvert
4
- VERSION = '0.1.0.pre'
4
+ VERSION = '1.0.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ynab_convert
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - coaxial
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-23 00:00:00.000000000 Z
11
+ date: 2022-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -225,6 +225,7 @@ files:
225
225
  - lib/ynab_convert/logger.rb
226
226
  - lib/ynab_convert/processor/base.rb
227
227
  - lib/ynab_convert/processor/example.rb
228
+ - lib/ynab_convert/processor/n26.rb
228
229
  - lib/ynab_convert/processor/revolut.rb
229
230
  - lib/ynab_convert/processor/ubs_chequing.rb
230
231
  - lib/ynab_convert/processor/ubs_credit.rb
@@ -249,11 +250,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
249
250
  version: '0'
250
251
  required_rubygems_version: !ruby/object:Gem::Requirement
251
252
  requirements:
252
- - - ">"
253
+ - - ">="
253
254
  - !ruby/object:Gem::Version
254
- version: 1.3.1
255
+ version: '0'
255
256
  requirements: []
256
- rubygems_version: 3.0.6
257
+ rubygems_version: 3.0.8
257
258
  signing_key:
258
259
  specification_version: 4
259
260
  summary: Convert online banking CSV files to YNAB 4 format.