zilverline-mt940 1.0 → 2.0
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 +8 -8
- data/Gemfile.lock +1 -1
- data/README.md +5 -4
- data/lib/mt940.rb +1 -6
- data/lib/mt940/version.rb +2 -2
- data/lib/mt940_structured/file_content.rb +46 -0
- data/lib/mt940_structured/header.rb +30 -0
- data/lib/mt940_structured/mt940_structured.rb +8 -0
- data/lib/mt940_structured/parser.rb +18 -0
- data/lib/mt940_structured/parsers/abnamro/abnamro.rb +5 -0
- data/lib/mt940_structured/parsers/abnamro/parser.rb +15 -0
- data/lib/mt940_structured/parsers/abnamro/transaction_parser.rb +55 -0
- data/lib/mt940_structured/parsers/balance_parser.rb +12 -0
- data/lib/mt940_structured/parsers/bank_statement_parser.rb +59 -0
- data/lib/mt940_structured/parsers/base.rb +34 -0
- data/lib/mt940_structured/parsers/date_parser.rb +7 -0
- data/lib/mt940_structured/parsers/default_line61_parser.rb +25 -0
- data/lib/mt940_structured/parsers/iban_support.rb +15 -0
- data/lib/mt940_structured/parsers/ing/ing.rb +7 -0
- data/lib/mt940_structured/parsers/ing/parser.rb +18 -0
- data/lib/mt940_structured/parsers/ing/structured_transaction_parser.rb +50 -0
- data/lib/mt940_structured/parsers/ing/transaction_parser.rb +31 -0
- data/lib/mt940_structured/parsers/ing/types.rb +26 -0
- data/lib/mt940_structured/parsers/parsers.rb +16 -0
- data/lib/mt940_structured/parsers/rabobank/parser.rb +13 -0
- data/lib/mt940_structured/parsers/rabobank/rabobank.rb +8 -0
- data/lib/mt940_structured/parsers/rabobank/structured_transaction_parser.rb +41 -0
- data/lib/mt940_structured/parsers/rabobank/transaction_parser.rb +29 -0
- data/lib/mt940_structured/parsers/rabobank/types.rb +714 -0
- data/lib/mt940_structured/parsers/structured_description_parser.rb +12 -0
- data/lib/mt940_structured/parsers/tridios/parser.rb +14 -0
- data/lib/mt940_structured/parsers/tridios/transaction_parser.rb +23 -0
- data/lib/mt940_structured/parsers/tridios/triodos.rb +5 -0
- data/spec/fixtures/ing/eu_incasso.txt +17 -0
- data/spec/fixtures/ing/eu_incasso_foreign_transaction.txt +17 -0
- data/spec/fixtures/ing/failing.txt +18 -0
- data/spec/mt940_abnamro_spec.rb +18 -6
- data/spec/mt940_ing_spec.rb +78 -2
- data/spec/mt940_rabobank_spec.rb +11 -11
- data/spec/mt940_structured/file_content_spec.rb +77 -0
- data/spec/mt940_structured/header_spec.rb +32 -0
- data/spec/mt940_structured/parsers/rabobank/bank_statement_parser_spec.rb +32 -0
- data/spec/mt940_triodos_spec.rb +1 -1
- data/spec/mt940_two_accounts_spec.rb +1 -1
- metadata +41 -9
- data/lib/mt940/banks/abnamro.rb +0 -76
- data/lib/mt940/banks/ing.rb +0 -84
- data/lib/mt940/banks/rabobank.rb +0 -770
- data/lib/mt940/banks/triodos.rb +0 -20
- data/lib/mt940/base.rb +0 -165
- data/lib/mt940/structured_format.rb +0 -16
- data/spec/mt940_base_spec.rb +0 -48
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
MTJlMDQ3YTFiYWIxM2NkODRkMzY0Y2I3MzRmZTRiNDA0YzlhZThiNA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MjU0N2ZlY2NkYjU2YTFjMGVkZDU4MjhhMzkyYWM4ZGY4YjczNDRiNA==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZWVlNmQ0YmIyYmU0ZDc4M2E0ZWFlMWQzNzY1OTQ1YmI1NjVhNDFkNzJlMDA3
|
10
|
+
Y2NkMjAyZDkyNDdjYzM1N2ZjNDY5ZWJlZWU2MmQ1MmQzMjQ4Mjk3ODMwNDIw
|
11
|
+
YTQ0ZGNmMGM2YWY2YTVmY2I3MjY5NmVhYzY1MDcxYzRhMDEzOGY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
Y2RjMzExNTk2MDA1OTg4MWM0MDIyOTFkZDExNjg5ZmVlNjllZDdmNzQ5Mzc2
|
14
|
+
NTA4NTI4ZDExMTljZDI5MTVjYjNiNzgzMjRlMDZmOTJjYzMwMjRkYzI5YjY1
|
15
|
+
NTYwZGNkZGY3NDM4OTE0Y2IzZTRjMDdiNDU1NGMxNTNhZTMyYzk=
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
MT940
|
2
2
|
======
|
3
3
|
|
4
|
-
Full parser for MT940 files, see [MT940](http://nl.wikipedia.org/wiki/MT940).
|
5
|
-
the original gem of [Frank Oxener - Agile Dovadi BV](http://github.com/dovadi/mt940)
|
4
|
+
Full parser for MT940 files, see [MT940](http://nl.wikipedia.org/wiki/MT940).
|
5
|
+
Initially this is based on the original gem of [Frank Oxener - Agile Dovadi BV](http://github.com/dovadi/mt940)
|
6
|
+
but as of version 2.0 completely rewritten in order to support MT940-structured format introduced by SEPA.
|
6
7
|
|
7
8
|
The following Dutch banks are implemented:
|
8
9
|
|
@@ -18,7 +19,7 @@ With the file name as argument:
|
|
18
19
|
|
19
20
|
file_name = '~/Downloads/ing.940'
|
20
21
|
|
21
|
-
@parse_result =
|
22
|
+
@parse_result = MT940Structured::Parser.parse_mt940(file_name)
|
22
23
|
|
23
24
|
or with the file itself:
|
24
25
|
|
@@ -26,7 +27,7 @@ or with the file itself:
|
|
26
27
|
|
27
28
|
file = File.open(file_name)
|
28
29
|
|
29
|
-
@parse_result =
|
30
|
+
@parse_result = MT940Structured::Parser.parse_mt940(file)
|
30
31
|
|
31
32
|
after parsing:
|
32
33
|
|
data/lib/mt940.rb
CHANGED
@@ -2,9 +2,4 @@ require 'tempfile'
|
|
2
2
|
require 'date'
|
3
3
|
require_relative 'mt940/transaction'
|
4
4
|
require_relative 'mt940/bank_statement'
|
5
|
-
require_relative '
|
6
|
-
require_relative 'mt940/structured_format'
|
7
|
-
require_relative 'mt940/banks/ing'
|
8
|
-
require_relative 'mt940/banks/rabobank'
|
9
|
-
require_relative 'mt940/banks/abnamro'
|
10
|
-
require_relative 'mt940/banks/triodos'
|
5
|
+
require_relative 'mt940_structured/mt940_structured'
|
data/lib/mt940/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module MT940
|
2
|
-
VERSION = '
|
3
|
-
end
|
2
|
+
VERSION = '2.0'
|
3
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class MT940Structured::FileContent
|
2
|
+
R_EOF_ING = /^-XXX$/
|
3
|
+
R_EOF_ABN_AMRO = /^-$/
|
4
|
+
R_EOF_TRIODOS = /^-$/
|
5
|
+
|
6
|
+
def initialize(raw_lines, join_lines_by = ' ')
|
7
|
+
@raw_lines = raw_lines.map{|line|line.strip}
|
8
|
+
@join_lines_by = join_lines_by
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_header
|
12
|
+
MT940Structured::Header.new(@raw_lines)
|
13
|
+
end
|
14
|
+
|
15
|
+
def group_lines
|
16
|
+
body_lines = @raw_lines[start_index..(end_index-1)]
|
17
|
+
grouped_lines = []
|
18
|
+
previous_tag = nil
|
19
|
+
body_lines.each do |line|
|
20
|
+
mt940_line = line.match /^(:\d{2}[D|C|F|M]?:)/
|
21
|
+
if mt940_line && previous_tag != $1
|
22
|
+
previous_tag = $1
|
23
|
+
grouped_lines << line
|
24
|
+
else
|
25
|
+
next_line = if line.match /^(:\d{2}[D|C|F|M]?:)(.*)/
|
26
|
+
$2
|
27
|
+
else
|
28
|
+
line
|
29
|
+
end
|
30
|
+
grouped_lines[-1] = [grouped_lines.last, @join_lines_by, next_line].join
|
31
|
+
end
|
32
|
+
end
|
33
|
+
grouped_lines
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def start_index
|
39
|
+
@raw_lines.index { |line| line.match /^:20:/ }
|
40
|
+
end
|
41
|
+
|
42
|
+
def end_index
|
43
|
+
@raw_lines.rindex { |line| line.match(R_EOF_ING) || line.match(R_EOF_ABN_AMRO) ||line.match(R_EOF_TRIODOS) } || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module MT940Structured
|
2
|
+
class Header
|
3
|
+
R_RABOBANK = /^:940:/
|
4
|
+
R_ABN_AMRO = /ABNANL/
|
5
|
+
R_TRIODOS = /^:25:TRIODOSBANK/
|
6
|
+
R_ING = /INGBNL/
|
7
|
+
|
8
|
+
def initialize(raw_lines)
|
9
|
+
@raw_lines = raw_lines
|
10
|
+
end
|
11
|
+
|
12
|
+
def parser
|
13
|
+
if @raw_lines[0].match(R_RABOBANK)
|
14
|
+
MT940Structured::Parsers::Rabobank::Parser.new
|
15
|
+
elsif @raw_lines[0].match(R_ABN_AMRO)
|
16
|
+
MT940Structured::Parsers::Abnamro::Parser.new
|
17
|
+
elsif @raw_lines[1] && @raw_lines[1].match(R_TRIODOS)
|
18
|
+
MT940Structured::Parsers::Triodos::Parser.new
|
19
|
+
elsif @raw_lines[0].match(R_ING)
|
20
|
+
MT940Structured::Parsers::Ing::Parser.new
|
21
|
+
else
|
22
|
+
raise UnsupportedBankError.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
class UnsupportedBankError < StandardError
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MT940Structured
|
2
|
+
class Parser
|
3
|
+
def self.parse_mt940(path, join_lines_by = ' ')
|
4
|
+
file_content = FileContent.new(readfile(path), join_lines_by)
|
5
|
+
grouped_lines = file_content.group_lines
|
6
|
+
file_content.get_header.parser.transform(grouped_lines)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def self.readfile(path)
|
11
|
+
File.open(path).readlines.map do |line|
|
12
|
+
line
|
13
|
+
.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace) # remove other obscure chars. god knows what people upload.
|
14
|
+
.gsub(/\u001A/, '') # remove eof chars in the middle of the string... yes it happens :-(
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MT940Structured::Parsers::Abnamro
|
2
|
+
class Parser < MT940Structured::Parsers::Base
|
3
|
+
def initialize
|
4
|
+
super "Abnamro", TransactionParsers.new
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class TransactionParsers
|
9
|
+
|
10
|
+
def for_format(_)
|
11
|
+
TransactionParser.new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module MT940Structured::Parsers::Abnamro
|
2
|
+
class TransactionParser
|
3
|
+
include MT940Structured::Parsers::DateParser
|
4
|
+
include MT940Structured::Parsers::IbanSupport
|
5
|
+
include MT940Structured::Parsers::StructuredDescriptionParser
|
6
|
+
include MT940Structured::Parsers::DefaultLine61Parser
|
7
|
+
|
8
|
+
def get_regex_for_line_61
|
9
|
+
/^:61:(\d{6})\d{4}(C|D)(\d+),(\d{0,2})/
|
10
|
+
end
|
11
|
+
|
12
|
+
def enrich_transaction(transaction, line_86)
|
13
|
+
transaction.contra_account = "NONREF" #default
|
14
|
+
line_86 = line_86.gsub(/:86:/, '')
|
15
|
+
case line_86
|
16
|
+
when /^(GIRO)\s+(\d+)(.+)/
|
17
|
+
transaction.contra_account = $2.rjust(9, '000000000')
|
18
|
+
transaction.description = $3.strip
|
19
|
+
when /^(\d{2}.\d{2}.\d{2}.\d{3})(.+)/
|
20
|
+
transaction.description = $2.strip
|
21
|
+
transaction.contra_account = $1.gsub('.', '')
|
22
|
+
when /\/TRTP\/SEPA OVERBOEKING/
|
23
|
+
description_parts = line_86[4..-1].split('/')
|
24
|
+
transaction.contra_account_iban = parse_description_after_tag description_parts, "IBAN"
|
25
|
+
transaction.contra_account = iban_to_account transaction.contra_account_iban
|
26
|
+
transaction.contra_account_owner = parse_description_after_tag description_parts, "NAME"
|
27
|
+
transaction.description = parse_description_after_tag description_parts, "REMI"
|
28
|
+
when /SEPA IDEAL/
|
29
|
+
if line_86.match /OMSCHRIJVING\:(.+)?/
|
30
|
+
transaction.description = $1.strip
|
31
|
+
end
|
32
|
+
if line_86.match /IBAN\:(.+)?BIC\:/
|
33
|
+
transaction.contra_account_iban = $1.strip
|
34
|
+
transaction.contra_account = iban_to_account transaction.contra_account_iban
|
35
|
+
end
|
36
|
+
if line_86.match /NAAM\:(.+)?OMSCHRIJVING\:/
|
37
|
+
transaction.contra_account_owner = $1.strip
|
38
|
+
end
|
39
|
+
when /SEPA ACCEPTGIROBETALING/
|
40
|
+
if line_86.match /(BETALINGSKENM\.\:.+)/
|
41
|
+
transaction.description = $1.strip
|
42
|
+
end
|
43
|
+
if line_86.match /IBAN\:(.+)?BIC\:/
|
44
|
+
transaction.contra_account_iban = $1.strip
|
45
|
+
transaction.contra_account = iban_to_account transaction.contra_account_iban
|
46
|
+
end
|
47
|
+
if line_86.match /NAAM\:(.+)?BETALINGSKENM\.\:/
|
48
|
+
transaction.contra_account_owner = $1.strip
|
49
|
+
end
|
50
|
+
else
|
51
|
+
transaction.description = line_86
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module MT940Structured::Parsers
|
2
|
+
module BalanceParser
|
3
|
+
def parse_balance(line)
|
4
|
+
currency = line[12..14]
|
5
|
+
balance_date = parse_date(line[6..11])
|
6
|
+
type = line[5] == 'D' ? -1 : 1
|
7
|
+
amount = line[15..-1].gsub(",", ".").to_f * type
|
8
|
+
MT940::Balance.new(amount, balance_date, currency)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module MT940Structured::Parsers
|
2
|
+
class BankStatementParser
|
3
|
+
include DateParser, BalanceParser, IbanSupport
|
4
|
+
attr_reader :bank_statement
|
5
|
+
|
6
|
+
def initialize(bank, transaction_parsers, lines)
|
7
|
+
@bank = bank
|
8
|
+
@transaction_parsers = transaction_parsers
|
9
|
+
@bank_statement = MT940::BankStatement.new([])
|
10
|
+
lines.each do |line|
|
11
|
+
if line.match /^:(\d{2})(F|C|M)?:/
|
12
|
+
parse_method = "parse_line_#{$1}".to_sym
|
13
|
+
send(parse_method, line) if respond_to? parse_method
|
14
|
+
else
|
15
|
+
raise "nyi '#{$1}' - line #{line}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_line_25(line)
|
21
|
+
line.gsub!('.', '')
|
22
|
+
case line
|
23
|
+
when /^:\d{2}:NL/
|
24
|
+
@bank_statement.bank_account_iban = line[4, 18]
|
25
|
+
@bank_statement.bank_account = iban_to_account(@bank_statement.bank_account_iban)
|
26
|
+
@is_structured_format = true
|
27
|
+
when /^:\d{2}:\D*(\d*)/
|
28
|
+
@bank_statement.bank_account = $1.gsub(/\D/, '').gsub(/^0+/, '')
|
29
|
+
@is_structured_format = false
|
30
|
+
else
|
31
|
+
raise "Unknown format for tag 25: #{line}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_line_60(line)
|
36
|
+
@bank_statement.previous_balance = parse_balance(line)
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_line_61(line_61)
|
40
|
+
@is_structured_format = @transaction_parsers.structured?(line_61) if @transaction_parsers.respond_to?(:structured?)
|
41
|
+
@transaction_parser = @transaction_parsers.for_format @is_structured_format
|
42
|
+
transaction = @transaction_parser.parse_transaction(line_61)
|
43
|
+
transaction.bank_account = @bank_statement.bank_account
|
44
|
+
transaction.bank_account_iban = @bank_statement.bank_account_iban
|
45
|
+
transaction.currency = @bank_statement.previous_balance.currency
|
46
|
+
transaction.bank = @bank
|
47
|
+
@bank_statement.transactions << transaction
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_line_86(line)
|
51
|
+
@transaction_parser.enrich_transaction(@bank_statement.transactions.last, line)
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_line_62(line)
|
55
|
+
@bank_statement.new_balance = parse_balance(line)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module MT940Structured::Parsers
|
2
|
+
class Base
|
3
|
+
def initialize(bank, transaction_parsers)
|
4
|
+
@bank = bank
|
5
|
+
@transaction_parsers = transaction_parsers
|
6
|
+
end
|
7
|
+
|
8
|
+
def transform(lines)
|
9
|
+
bank_statements = Hash.new { |h, k| h[k] = [] }
|
10
|
+
result = group_lines_by_tag(lines)
|
11
|
+
result.each do |bank_statement_lines|
|
12
|
+
bank_statement = BankStatementParser.new(@bank, @transaction_parsers, bank_statement_lines).bank_statement
|
13
|
+
bank_statements[bank_statement.bank_account] << bank_statement
|
14
|
+
end
|
15
|
+
bank_statements
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def group_lines_by_tag(lines)
|
20
|
+
result = []
|
21
|
+
while !lines.empty? do
|
22
|
+
start_index = lines.index { |line| line.match(/^:20:/)}
|
23
|
+
end_index = lines.index { |line| line.match(/^:62F:/)}
|
24
|
+
if start_index && end_index > start_index
|
25
|
+
result << lines[start_index..end_index]
|
26
|
+
lines = lines.drop(end_index + 1)
|
27
|
+
else
|
28
|
+
lines = []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MT940Structured::Parsers
|
2
|
+
##
|
3
|
+
# Basic line 61 parser. Retrieves the date and amount from the line :61:.
|
4
|
+
# This module expects that a method get_regex_for_line_61 exists that returns
|
5
|
+
# a regex that will, if matched, produces the following groups:
|
6
|
+
# $1 - the transaction date
|
7
|
+
# $2 - D for Debit, C for Credit transactions
|
8
|
+
# $3 - The amount of the transaction before the cent mark.
|
9
|
+
# $4 - The cents of the transaction
|
10
|
+
#
|
11
|
+
module DefaultLine61Parser
|
12
|
+
def get_regex_for_line_61
|
13
|
+
raise 'Override this when using this module'
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_transaction(line_61)
|
17
|
+
if line_61.match(get_regex_for_line_61)
|
18
|
+
type = $2 == 'D' ? -1 : 1
|
19
|
+
transaction = MT940::Transaction.new(amount: type * ($3 + '.' + $4).to_f)
|
20
|
+
transaction.date = parse_date($1)
|
21
|
+
transaction
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MT940Structured::Parsers
|
2
|
+
module IbanSupport
|
3
|
+
IBAN_R = /[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{0,30}/
|
4
|
+
|
5
|
+
def iban?(string)
|
6
|
+
!string.nil? and string.match(IBAN_R)
|
7
|
+
end
|
8
|
+
|
9
|
+
def iban_to_account(iban)
|
10
|
+
!iban.nil? ? iban.split(//).last(10).join.gsub(/^0+/, '') : nil
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MT940Structured::Parsers::Ing
|
2
|
+
class Parser < MT940Structured::Parsers::Base
|
3
|
+
def initialize
|
4
|
+
super "Ing", TransactionParsers.new
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class TransactionParsers
|
9
|
+
def structured?(line_61)
|
10
|
+
line_61.match /EREF|PREF|MARF|\d{16}/
|
11
|
+
end
|
12
|
+
|
13
|
+
def for_format(is_structured)
|
14
|
+
is_structured ? StructuredTransactionParser.new : TransactionParser.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|