ynab_convert 1.0.2 → 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: 2e707b36e18c8b57f7b517bf6f1f29d769e22089316448da062165c35e5fb713
4
- data.tar.gz: ee5dbf483c6252085860166c6371fd7d6ba75b91d90dc17a74a9a68ddff80026
3
+ metadata.gz: 2af241f007b8495197a8971ffbf3b3efe0dda41411c56a3543452dfea50fd03e
4
+ data.tar.gz: ac7c2614c064e005524c808dd62bddbbaaa8e8395bea6a02b0bf4ef45960133f
5
5
  SHA512:
6
- metadata.gz: 44f457015648d06a43c98da0622ae12de10eb84025636677b044020d98aa82dca85c2968f9b924541dd8260002a8e727bef22cf60515d6dc17e345d8e06cf0e5
7
- data.tar.gz: 0fadae67422faf27caa356890121600879416545043c240ef951a3bc2c9caf9a8af544f25978a50e3e479994eb020e008e99c32a99c76363d239e82d09037fcb
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/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ynab_convert (1.0.2)
4
+ ynab_convert (1.0.3)
5
5
  i18n
6
6
  slop
7
7
 
@@ -44,10 +44,10 @@ GEM
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.6.1)
47
+ mini_portile2 (2.8.0)
48
48
  nenv (0.3.0)
49
- nokogiri (1.12.5)
50
- mini_portile2 (~> 2.6.1)
49
+ nokogiri (1.13.3)
50
+ mini_portile2 (~> 2.8.0)
51
51
  racc (~> 1.4)
52
52
  notiffany (0.1.3)
53
53
  nenv (~> 0.1)
@@ -61,7 +61,7 @@ GEM
61
61
  pry-byebug (3.7.0)
62
62
  byebug (~> 11.0)
63
63
  pry (~> 0.10)
64
- racc (1.5.2)
64
+ racc (1.6.0)
65
65
  rainbow (3.0.0)
66
66
  rake (13.0.1)
67
67
  rb-fsevent (0.10.3)
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 = '1.0.2'
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: 1.0.2
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: 2022-02-24 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