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 +4 -4
- data/.rubocop.yml +5 -0
- data/Gemfile.lock +5 -5
- data/Guardfile +1 -1
- data/README.md +3 -1
- data/lib/core_extensions/string.rb +1 -1
- data/lib/ynab_convert/processor/base.rb +65 -11
- data/lib/ynab_convert/processor/example.rb +1 -1
- data/lib/ynab_convert/processor/n26.rb +85 -0
- data/lib/ynab_convert/processor/ubs_chequing.rb +1 -1
- data/lib/ynab_convert/processor/ubs_credit.rb +1 -1
- data/lib/ynab_convert/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2af241f007b8495197a8971ffbf3b3efe0dda41411c56a3543452dfea50fd03e
|
4
|
+
data.tar.gz: ac7c2614c064e005524c808dd62bddbbaaa8e8395bea6a02b0bf4ef45960133f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f96f0a1839d1bc4111f8f53aeac6f7b503155f65b03e2ccd113e0847baa8637beeef7aeae182538b3f6f4cd5caf5a462b568b0ffae5a42f24062722c4a51cf7
|
7
|
+
data.tar.gz: 4f17efacff3b4b5cb62df38997863bad691b41a4f26d5a27ef3b7bfaa93072f217f4a824f11dd6e462ba422f862991e5ae14990e7f5f8c8879f49a7deba91274
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ynab_convert (1.0.
|
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.
|
47
|
+
mini_portile2 (2.8.0)
|
48
48
|
nenv (0.3.0)
|
49
|
-
nokogiri (1.
|
50
|
-
mini_portile2 (~> 2.
|
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.
|
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
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
|
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
|
|
@@ -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
|
-
|
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
|
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
|
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
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
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
|
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
|
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
|
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)
|
data/lib/ynab_convert/version.rb
CHANGED
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.
|
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-
|
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
|