ynab_convert 1.0.1 → 1.0.4

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: 9d57290b817fcadeb182c71a128179ca028608242651ea5e4fc6d9ba80ab034a
4
- data.tar.gz: 137491a1cb0d063cbd2ee4fc21dde6843121a9ebeaedeb32614300a058d09e1a
3
+ metadata.gz: 7ad8f2a7b8c6b00d895032525413d35d4879da80be21e0b9fc852e1d413dbaa0
4
+ data.tar.gz: 99b9168cb23390fe15deec8f0c9c3c27cda183f95867b2849d79fa4e4a704c4d
5
5
  SHA512:
6
- metadata.gz: 407012e06fcf3f00e5d888181076568530c7d524b7425d8d1068dde4458d9c5dd1c88364e66ca33d6692a4c8ae5351e33ec5b896fdb25c9b894a5bcfd5faab28
7
- data.tar.gz: e1b8b00d2ff99dd452ba7d308a800e418aa813f18d5340a0ec787b1f1e9cee35f5dad7858f97405d40fb682cc09052e198c367b023e90ab925a82e9eae6ba0d1
6
+ metadata.gz: 630775dd15ab4f65a32c6dbe9617bea36c0721ff9d5cca49ee986f4e04831fa9335b41b996b2b3f69bbce630e452ae9045d3d98ec9b3d6862c84f6fb0077a1a6
7
+ data.tar.gz: 3bdf9b06d5327815fc31b8b3b9a9f6fb52e606ae6dc900501ff8fdf0671c110effe02dd5ca625fed0c3bbeb884b9572ff18ce7b9e46b8c21dd61d0a4b685956c
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
@@ -3,12 +3,12 @@ language: ruby
3
3
  cache: bundler
4
4
  rvm:
5
5
  - 2.6
6
+ - 2.7
6
7
  before_install: gem install bundler
7
8
 
8
9
  jobs:
9
10
  include:
10
11
  - stage: test
11
- rvm: 2.6
12
12
  script: echo "Running tests for $(ruby -v)..." && bundle exec rake ci
13
13
  - stage: gem release
14
14
  rvm: 2.6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ynab_convert (1.0.1)
4
+ ynab_convert (1.0.4)
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
 
data/bin/ynab_convert CHANGED
@@ -9,7 +9,6 @@ require 'ynab_convert/processors'
9
9
  require 'ynab_convert/logger'
10
10
  require 'ynab_convert/error'
11
11
  require 'slop/symbol'
12
- require 'pry'
13
12
 
14
13
  # Add mixins and run converter
15
14
  class Convert
@@ -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
@@ -5,7 +5,8 @@ require 'csv'
5
5
  require 'ynab_convert/logger'
6
6
 
7
7
  module Processor
8
- # Base class for a Processor, all processors must inherit from it
8
+ # Base class for a Processor, all processors must inherit from it.
9
+
9
10
  # rubocop:disable Metrics/ClassLength
10
11
  class Base
11
12
  include YnabLogger
@@ -13,13 +14,30 @@ module Processor
13
14
 
14
15
  attr_reader :loader_options
15
16
 
16
- # @option opts [String] :file Path to the CSV file to process
17
- def initialize(opts)
17
+ # @option options [String] :file Path to the CSV file to process
18
+ # @option options [Symbol] :format YNAB4 format to use, one of :flows or
19
+ # :amounts. :flows is useful for CSVs with separate debit and credit
20
+ # columns, :amounts is for CSVs with only one amount columns and +/-
21
+ # numbers. See
22
+ # https://docs.youneedabudget.com/article/921-formatting-csv-file
23
+ def initialize(options)
24
+ default_options = { file: '', format: :flows }
25
+ opts = default_options.merge(options)
26
+
18
27
  logger.debug "Initializing processor with options: `#{opts.to_h}'"
19
28
  raise ::Errno::ENOENT unless File.exist? opts[:file]
20
29
 
21
30
  @file = opts[:file]
22
- @headers = { transaction_date: nil, payee: nil, debit: nil, credit: nil }
31
+ @headers = { transaction_date: nil, payee: nil }
32
+ @format = opts[:format]
33
+
34
+ if @format == :amounts
35
+ amounts_columns = { amount: nil }
36
+ @headers.merge!(amounts_columns)
37
+ else
38
+ flows_columns = { inflow: nil, outflow: nil }
39
+ @headers.merge!(flows_columns)
40
+ end
23
41
  end
24
42
 
25
43
  def to_ynab!
@@ -38,15 +56,41 @@ module Processor
38
56
 
39
57
  attr_accessor :statement_from, :statement_to, :headers
40
58
 
41
- def inflow_or_outflow_missing?(row)
59
+ def amount_invalid?(row)
60
+ amount_index = 3
61
+
62
+ # If there is no amount,
63
+ # then the row is invalid.
64
+ row[amount_index].nil? || row[amount_index].empty?
65
+ end
66
+
67
+ def inflow_outflow_invalid?(row)
42
68
  inflow_index = 3
43
69
  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')
70
+
71
+ # If there is neither inflow and outflow values,
72
+ # or both the inflow and outflow amounts are 0,
73
+ # then the row is invalid.
74
+ (
75
+ row[inflow_index].nil? ||
76
+ row[inflow_index].empty? ||
77
+ row[inflow_index] == '0.00'
78
+ ) && (
79
+ row[outflow_index].nil? ||
80
+ row[outflow_index].empty? ||
81
+ row[outflow_index] == '0.00'
82
+ )
83
+ end
84
+
85
+ def amounts_missing?(row)
86
+ logger.debug "Checking for missing amount in `#{row}`"
87
+ if @format == :amounts
88
+ logger.debug 'Using `:amounts`'
89
+ amount_invalid?(row)
90
+ else
91
+ logger.debug 'Using `:flows`'
92
+ inflow_outflow_invalid?(row)
93
+ end
50
94
  end
51
95
 
52
96
  def skip_row(row)
@@ -96,14 +140,15 @@ module Processor
96
140
  def convert!
97
141
  logger.debug "Will write to `#{temp_filename}'"
98
142
 
99
- CSV.open(temp_filename, 'wb', output_options) do |converted|
100
- CSV.foreach(@file, 'rb', loader_options) do |row|
143
+ logger.debug(loader_options)
144
+ CSV.open(temp_filename, 'wb', **output_options) do |converted|
145
+ CSV.foreach(@file, 'rb', **loader_options) do |row|
101
146
  logger.debug "Parsing row: `#{row.to_h}'"
102
147
  # Some rows don't contain valid or useful data
103
148
  catch :skip_row do
104
149
  extract_header_names(row)
105
150
  ynab_row = transformers(row)
106
- if inflow_or_outflow_missing?(ynab_row) ||
151
+ if amounts_missing?(ynab_row) ||
107
152
  transaction_date_missing?(ynab_row)
108
153
  logger.debug 'Empty row, skipping it'
109
154
  skip_row(row)
@@ -151,7 +196,17 @@ module Processor
151
196
  end
152
197
 
153
198
  def ynab_headers
154
- %w[Date Payee Memo Outflow Inflow]
199
+ common_headers = %w[Date Payee Memo]
200
+
201
+ if @format == :amounts
202
+ amounts_headers = %w[Amount]
203
+ common_headers.concat(amounts_headers)
204
+ else
205
+ flows_headers = %w[Outflow Inflow]
206
+ common_headers.concat(flows_headers)
207
+ end
208
+
209
+ common_headers
155
210
  end
156
211
 
157
212
  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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Processor
4
+ # Processes CSV files from N26
5
+ class N26 < Processor::Base
6
+ # @option options [String] :file Path to the CSV file to process
7
+ def initialize(options)
8
+ # Custom converters can be added so that the CSV data is parsed when
9
+ # loading the original file
10
+ register_custom_converters
11
+
12
+ # These are the options for the CSV module (see
13
+ # https://ruby-doc.org/stdlib-2.6/libdoc/csv/rdoc/CSV.html#method-c-new)
14
+ # They should match the format for the CSV file that the financial
15
+ # institution generates.
16
+ @loader_options = {
17
+ col_sep: ',',
18
+ quote_char: '"',
19
+ # Use your converters, if any
20
+ # converters: %i[],
21
+ headers: true,
22
+ encoding: 'bom|utf-8'
23
+ }
24
+
25
+ # This is the financial institution's full name as it calls itself. This
26
+ # usually matches the institution's letterhead and/or commercial name.
27
+ # It can happen that the same institution needs different parsers because
28
+ # its credit card CSV files are in one format, and its chequing accounts
29
+ # in another. In that case, more details can be added in parens.
30
+ # For instance:
31
+ # 'Example Bank (credit cards)' and 'Example Bank (chequing)'
32
+ @institution_name = 'N26 Bank'
33
+ # N26's CSV only has one columns for all transactions instead of separate
34
+ # debit and credit columns
35
+ additional_processor_options = { format: :amounts }
36
+
37
+ # This is mandatory.
38
+ super(options.merge(additional_processor_options))
39
+ end
40
+
41
+ private
42
+
43
+ def register_custom_converters; end
44
+
45
+ protected
46
+
47
+ def transformers(row)
48
+ transaction_date = row[headers[:transaction_date]]
49
+ payee = row[headers[:payee]]
50
+ amount = row[headers[:amount]]
51
+
52
+ converted_row = [transaction_date, payee, nil, amount]
53
+ logger.debug "Converted row: #{converted_row}"
54
+ converted_row
55
+ end
56
+
57
+ private
58
+
59
+ # Institutions love translating the column names, apparently. Rather than
60
+ # hardcoding the column name as a string, use the headers array at the
61
+ # right index.
62
+ # These lookups aren't particularly expensive but they're done on each row
63
+ # so why not memoize them with ||=
64
+ def extract_header_names(row)
65
+ headers[:transaction_date] ||= row.headers[0]
66
+ headers[:payee] ||= row.headers[1]
67
+ headers[:amount] ||= row.headers[5]
68
+ end
69
+ end
70
+ 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.1'
4
+ VERSION = '1.0.4'
5
5
  end
data/lib/ynab_convert.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'ynab_convert/version'
4
4
  require 'slop'
5
- require 'pry'
6
5
  require 'ynab_convert/logger'
7
6
  require 'core_extensions/string.rb'
8
7
 
data/ynab_convert.gemspec CHANGED
@@ -14,6 +14,16 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/coaxial/ynab_convert'
15
15
  spec.license = 'MIT'
16
16
 
17
+ spec.required_ruby_version = '~> 2.6'
18
+
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/coaxial/ynab_convert/issues',
21
+ 'documentation_uri' => 'https://rubydoc.info/github/coaxial/ynab_convert/master',
22
+ 'homepage_uri' => 'https://github.com/coaxial/ynab_convert',
23
+ 'source_code_uri' => 'https://github.com/coaxial/ynab_convert'
24
+ }
25
+ spec.post_install_message = 'Happy budgetting!'
26
+
17
27
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
28
  # to allow pushing to a single host or delete this section to allow pushing to any host.
19
29
  if spec.respond_to?(:metadata)
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.1
4
+ version: 1.0.4
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
@@ -235,18 +236,20 @@ homepage: https://github.com/coaxial/ynab_convert
235
236
  licenses:
236
237
  - MIT
237
238
  metadata:
238
- allowed_push_host: https://rubygems.org
239
+ bug_tracker_uri: https://github.com/coaxial/ynab_convert/issues
240
+ documentation_uri: https://rubydoc.info/github/coaxial/ynab_convert/master
239
241
  homepage_uri: https://github.com/coaxial/ynab_convert
240
242
  source_code_uri: https://github.com/coaxial/ynab_convert
241
- post_install_message:
243
+ allowed_push_host: https://rubygems.org
244
+ post_install_message: Happy budgetting!
242
245
  rdoc_options: []
243
246
  require_paths:
244
247
  - lib
245
248
  required_ruby_version: !ruby/object:Gem::Requirement
246
249
  requirements:
247
- - - ">="
250
+ - - "~>"
248
251
  - !ruby/object:Gem::Version
249
- version: '0'
252
+ version: '2.6'
250
253
  required_rubygems_version: !ruby/object:Gem::Requirement
251
254
  requirements:
252
255
  - - ">="