ynab_convert 1.0.2 → 1.0.3

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 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