zilverline-mt940 1.0 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|