Skip to content

Commit

Permalink
* mt940 first throw
Browse files Browse the repository at this point in the history
* show reference on postings page

* added cheque model for tests in acts_as_account

* MT940 bank statements can now get imported into betterplace

* add mt940 gem

* add field 21

added two regressions:
* missing \n\r at end of file
* empty lines an beginning of file

* encode bank statement strings in utf8

* added currency in field #25 (Account)

* new gem version (acts_as_account)

* cleanup
* funds_code is now kept in bank_statement_lines
* added valuta to postings

* add bank_statement_lines to accounting controller
* refactored dorting

* 100 bank_lines per page

* all booking is now done in the same transaction context as the frontend. this mean, that when a donation is committed, the Bookkeeper will book it. This simplifies things greatly.
* moved bookkeeper code into the domain-objects

* tiny html cleanup

* fix recently_confirmed in Donation

* all features now explicitly tell whet transfers they expect
* Bookkeeper can now destroy donations (used in API)
* external donations are now booked
* If a feature does not either read the generated postings or ignores them an exception will be thrown in the global After block.
  • Loading branch information
Thies C. Arntzen committed Mar 29, 2010
0 parents commit fdaea44
Show file tree
Hide file tree
Showing 24 changed files with 4,140 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .document
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
README.rdoc
lib/**/*.rb
bin/*
features/**/*.feature
LICENSE
56 changes: 56 additions & 0 deletions .specification
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--- !ruby/object:Gem::Specification
name: mt940
version: !ruby/object:Gem::Version
version: 1.0.0
platform: ruby
authors: []

autorequire:
bindir: bin
cert_chain: []

date: 2010-03-24 00:00:00 +01:00
default_executable:
dependencies: []

description:
email:
executables: []

extensions: []

extra_rdoc_files: []

files:
- lib
- lib/mt940.rb
has_rdoc: true
homepage:
licenses: []

post_install_message:
rdoc_options: []

require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: "0"
version:
required_rubygems_version: !ruby/object:Gem::Requirement
requirements:
- - ">="
- !ruby/object:Gem::Version
version: "0"
version:
requirements: []

rubyforge_project:
rubygems_version: 1.3.5
signing_key:
specification_version: 3
summary:
test_files: []

20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2010 betterplace

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17 changes: 17 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
= mt940

Description goes here.

== Note on Patches/Pull Requests

* Fork the project.
* Make your feature addition or bug fix.
* Add tests for it. This is important so I don't break it in a
future version unintentionally.
* Commit, do not mess with rakefile, version, or history.
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
* Send me a pull request. Bonus points for topic branches.

== Copyright

Copyright (c) 2010 Thies C. Arntzen. See LICENSE for details.
53 changes: 53 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'rubygems'
require 'rake'

begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "mt940"
gem.summary = %Q{TODO: one-line summary of your gem}
gem.description = %Q{TODO: longer description of your gem}
gem.email = "[email protected]"
gem.homepage = "http://github.com/thieso2/mt940"
gem.authors = ["Thies C. Arntzen"]
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end

require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.libs << 'lib' << 'test'
test.pattern = 'test/**/test_*.rb'
test.verbose = true
end

begin
require 'rcov/rcovtask'
Rcov::RcovTask.new do |test|
test.libs << 'test'
test.pattern = 'test/**/test_*.rb'
test.verbose = true
end
rescue LoadError
task :rcov do
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
end
end

task :test => :check_dependencies

task :default => :test

require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""

rdoc.rdoc_dir = 'rdoc'
rdoc.title = "mt940 #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
Binary file added docs/0E0Y00DNY.pdf
Binary file not shown.
Binary file added docs/FinTS_4.0_Formals.pdf
Binary file not shown.
Binary file added docs/FinTS_4.0_Messages_Finanzdatenformate.pdf
Binary file not shown.
Binary file added docs/MT940_Deutschland_Structure2002.pdf
Binary file not shown.
Binary file added docs/SEPA_20MT940__Schnittstellenbeschreibung.pdf
Binary file not shown.
Binary file added docs/mt940.pdf
Binary file not shown.
Binary file added docs/swift_mt940_942.pdf
Binary file not shown.
Binary file not shown.
226 changes: 226 additions & 0 deletions lib/mt940.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
class MT940
class Field
attr_reader :modifier, :content

DATE = /(\d{2})(\d{2})(\d{2})/
SHORT_DATE = /(\d{2})(\d{2})/

class << self

def for(line)
if line.match(/^:(\d{2,2})(\w)?:(.*)$/)
number, modifier, content = $1, $2, $3
klass = {
'20' => Job,
'21' => Reference,
'25' => Account,
'28' => Statement,
'60' => AccountBalance,
'61' => StatementLine,
'62' => ClosingBalance,
'64' => ValutaBalance,
'65' => FutureValutaBalance,
'86' => StatementLineInformation
}[number]

raise StandardError, "Field #{number} is not implemented" unless klass

klass.new(modifier, content)
else
raise StandardError, "Wrong line format: #{line.dump}"
end
end
end

def initialize(modifier, content)
@modifier = modifier
parse_content(content)
end

private
def parse_amount_in_cents(amount)
Integer(amount.gsub(',', ''))
end

def parse_date(date)
date.match(DATE)
Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
end

def parse_short_date(year, date)
date.match(SHORT_DATE)
Date.new(year, $1.to_i, $2.to_i)
end
end

# 20
class Job < Field
attr_reader :reference

def parse_content(content)
@reference = content
end
end

# 21
class Reference < Job
end

# 25
class Account < Field
attr_reader :bank_code, :account_number, :account_currency

def parse_content(content)
content.match(/^(.{8,11})\/(\d{0,23})([A-Z]{3})?$/)
@bank_code, @account_number, @account_currency = $1, $2, $3
end
end

# 28
class Statement < Field
attr_reader :number, :sheet

def parse_content(content)
content.match(/^(0|(\d{5,5})\/(\d{2,5}))$/)
if $1 == '0'
@number = @sheet = 0
else
@number, @sheet = $2.to_i, $3.to_i
end
end
end

# 60
class AccountBalance < Field
attr_reader :balance_type, :sign, :currency, :amount, :date

def parse_content(content)
content.match(/^(C|D)(\w{6})(\w{3})(\d{1,12},\d{0,2})$/)

@balance_type = case @modifier
when 'F'
:start
when 'M'
:intermediate
end

@sign = case $1
when 'C'
:credit
when 'D'
:debit
end

raw_date = $2
@currency = $3
@amount = parse_amount_in_cents($4)

@date = case raw_date
when 'ALT', '0'
nil
when DATE
Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
end
end
end

# 61
class StatementLine < Field
attr_reader :date, :entry_date, :funds_code, :amount, :swift_code, :reference, :transaction_description

def parse_content(content)
content.match(/^(\d{6})(\d{4})?(C|D|RC|RD)\D?(\d{1,12},\d{0,2})((?:N|F).{3})(NONREF|.{0,16})(?:$|\/\/)(.*)/).to_a

raw_date = $1
raw_entry_date = $2
@funds_code = case $3
when 'C'
:credit
when 'D'
:debit
when 'RC'
:return_credit
when 'RD'
:return_debit
end

@amount = parse_amount_in_cents($4)
@swift_code = $5
@reference = $6
@transaction_description = $7

@date = parse_date(raw_date)
@entry_date = parse_short_date(@date.year, raw_entry_date)
end
end

# 62
class ClosingBalance < AccountBalance
end

# 64
class ValutaBalance < AccountBalance
end

# 65
class FutureValutaBalance < AccountBalance
end

# 86
class StatementLineInformation < Field
attr_reader :code, :transaction_description, :prima_nota, :details, :bank_code, :account_number,
:account_holder, :text_key_extension, :not_implemented_fields

def parse_content(content)
content.match(/^(\d{3})((.).*)$/)
@code = $1.to_i

seperator = $3
sub_fields = $2.scan(/#{Regexp.escape(seperator)}(\d{2})([^#{Regexp.escape(seperator)}]*)/)

details = []
account_holder = []

sub_fields.each do |(code, content)|
case code.to_i
when 0
@transaction_description = content
when 10
@prima_nota = content
when 20..29, 60..63
details << content
when 30
@bank_code = content
when 31
@account_number = content
when 32..33
account_holder << content
when 34
@text_key_extension = content
else
@not_implemented_fields ||= []
@not_implemented_fields << [code, content]
$stderr << "code not implemented: code:#{code} content:»#{content}«\n" if $DEBUG
end
end

@details = details.join("\n")
@account_holder = account_holder.join("\n")
end
end

class << self
def parse(text)
text << "\r\n" if text[-1,1] == '-'
raw_sheets = text.split(/^-\r\n/).map { |sheet| sheet.gsub(/\r\n(?!:)/, '') }
sheets = raw_sheets.map { |raw_sheet| parse_sheet(raw_sheet) }
end

private
def parse_sheet(sheet)
lines = sheet.split("\r\n")
fields = lines.reject { |line| line.empty? }.map { |line| Field.for(line) }
fields
end
end
end
1 change: 1 addition & 0 deletions test/fixtures/currency_in_25.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:25:51210600/9223382012EUR
6 changes: 6 additions & 0 deletions test/fixtures/currency_in_25.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- - !ruby/object:MT940::Account
account_currency: EUR
account_number: "9223382012"
bank_code: "51210600"
modifier:
2 changes: 2 additions & 0 deletions test/fixtures/empty_line.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

:20:TELEREPORTING
4 changes: 4 additions & 0 deletions test/fixtures/empty_line.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
- - !ruby/object:MT940::Job
modifier:
reference: TELEREPORTING
Loading

0 comments on commit fdaea44

Please sign in to comment.