-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 84f966f
Showing
6 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*~ | ||
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Extensible python parser for MT940 and MT942 files. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#!/usr/bin/env python | ||
|
||
from swift import SwiftReader | ||
from swift.TabaSK import TabaParser940 | ||
|
||
if __name__ == '__main__': | ||
import os, glob | ||
pattern = "download/*.STA" | ||
|
||
parser = SwiftReader(TabaParser940) | ||
for filename in glob.glob(pattern): | ||
print "Parsing %s ..." % filename | ||
statements = parser.parse_file(filename) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
|
||
from datetime import date | ||
from decimal import Decimal | ||
import re | ||
|
||
class RunawayField(Exception): | ||
pass | ||
|
||
class NoTransaction(Exception): | ||
pass | ||
|
||
class InvalidFieldValue(Exception): | ||
pass | ||
|
||
class MissingFieldParser(Exception): | ||
pass | ||
|
||
|
||
class MT940Transaction(object): | ||
|
||
__slots__ = ['value_date', 'entry_date', 'amount', 'type_code', 'cust_ref', 'bank_ref'] | ||
|
||
def __init__(self): | ||
for attr in self.__slots__: | ||
setattr(self, attr, None) | ||
|
||
def update(self, **kwargs): | ||
for key, val in kwargs.items(): | ||
setattr(self, key, val) | ||
|
||
|
||
class MT940Statement(object): | ||
|
||
__slots__ = ['transaction_ref', 'number', 'account', 'opening_balance', 'closing_balance', | ||
'closing_available_balance', 'transaction' ] | ||
|
||
_transaction_class = MT940Transaction | ||
|
||
def __init__(self, tref): | ||
self.transaction_ref = tref | ||
self.number = None | ||
self.account = None | ||
self.opening_balance = (None, None, None) | ||
self.closing_balance = (None, None, None) | ||
self.closing_available_balance = (None, None, None) | ||
self.transactions = [] | ||
|
||
def add_transaction(self, **kwargs): | ||
self._current_transaction = self._transaction_class() | ||
self._current_transaction.update(**kwargs) | ||
self.transactions.append(self._current_transaction) | ||
|
||
def update_transaction(self, **kwargs): | ||
if self._current_transaction: | ||
self._current_transaction.update(**kwargs) | ||
else: | ||
raise NoTransaction("No current transaction defined") | ||
|
||
|
||
class MT940Parser(object): | ||
|
||
_statement_class = MT940Statement | ||
|
||
RE20 = re.compile("^(.{1:16})$") | ||
RE25 = re.compile("^(.{1:35})$") | ||
RE28C = re.compile("^(\d{1,4})(\/(\d{1,5}))?$") | ||
RE61 = re.compile("^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})" | ||
"([CD])([0-9,]+)([A-Z]{4})([\w\/\. -]{0,16})" | ||
"(\/\/([\w\/\. -]{0,16}))?$") | ||
# matches 60F, 60M, 62F, 62M, 64, 65 | ||
RE6XX = re.compile("^([CD])(\d{2})(\d{2})(\d{2})([A-Z]{3})([0-9,]+)$") | ||
|
||
def __init__(self): | ||
self._name = 'MT940Parser' | ||
self.current_statement = None | ||
self.statements = [] | ||
|
||
def parse(self, lines=[], parse_transactions = True): | ||
|
||
statement_lines = [] | ||
|
||
for line in lines: | ||
if line.startswith(':20:'): | ||
statement_lines.append(line) | ||
elif line.startswith('-'): | ||
self._parse_statement(statement_lines) | ||
statement_lines = [] | ||
else: | ||
if statement_lines: | ||
statement_lines.append(line) | ||
|
||
return self.statements | ||
|
||
def _parse_statement(self, lines=[]): | ||
|
||
field_re = re.compile("^\:(\w+)\:(.*)$") | ||
field = (None, None, []) | ||
|
||
for line in lines: | ||
line = line.rstrip() | ||
match = field_re.match(line) | ||
if match: | ||
if field[0]: | ||
try: | ||
field_parser = getattr(self, '_field_%s' % field[0].lower()) | ||
except AttributeError: | ||
raise MissingFieldParser("Field parser %s not implemented" % field[0]) | ||
else: | ||
field_parser(field[1], field[2]) | ||
|
||
field = (match.group(1), match.group(2), []) | ||
else: | ||
field[2].append(line) | ||
|
||
if field[0]: | ||
try: | ||
field_parser = getattr(self, '_field_%s' % field[0].lower()) | ||
except AttributeError: | ||
raise NotImplementedError("Field parser %s not implemented" % field[0]) | ||
else: | ||
field_parser(field[1], field[2]) | ||
|
||
|
||
def _field_20(self, value, subfields=[]): | ||
m = self.RE20.match(value) | ||
if m: | ||
self.current_statement = self._statement_class(m.group(1)) | ||
self.statements.append(self.current_statement) | ||
else: | ||
raise InvalidFieldValue("Invalid field 20 value `%s`" % value) | ||
|
||
return m | ||
|
||
|
||
def _field_25(self, value, subfields=[]): | ||
m = self.RE25.match(value) | ||
if m: | ||
if self.current_statement: | ||
self.current_statement.account = m.group(1) | ||
else: | ||
raise RunawayField("Runaway field 25 `%s`" % value) | ||
else: | ||
raise InvalidFieldValue("Invalid field 25 value `%s`" % value) | ||
|
||
return m | ||
|
||
|
||
def _field_28c(self, value, subfields=[]): | ||
m = self.RE28C.match(value) | ||
if m: | ||
if self.current_statement: | ||
self.current_statement.number = m.group(1) | ||
else: | ||
raise RunawayField("Runaway field 25 `%s`" % value) | ||
else: | ||
raise InvalidFieldValue("Invalid field 28C value `%s`" % value) | ||
|
||
return m | ||
|
||
|
||
def _field_61(self, value, subfields=[]): | ||
m = self.RE61.match(value) | ||
if m: | ||
value_date = date(2000 + int(m.group(1)), int(m.group(2)), int(m.group(3))) | ||
entry_date = date(value_date.year, int(m.group(4)), int(m.group(5))) | ||
amount = Decimal(m.group(7).replace(',','.')) | ||
if m.group(6) == 'D': | ||
amount = -amount | ||
type_code = m.group(8) | ||
cust_ref = m.group(9) | ||
bank_ref = m.group(10) | ||
self.current_statement.add_transaction(value_date = value_date, entry_date = entry_date, | ||
amount= amount, type_code = type_code, cust_ref = cust_ref, bank_ref = bank_ref) | ||
else: | ||
raise InvalidFieldValue("Invalid field 61 value `%s`" % value) | ||
|
||
return m | ||
|
||
|
||
def _field_6xx_balance(self, value, field): | ||
m = self.RE6XX.match(value) | ||
if m: | ||
value_date = date(2000 + int(m.group(2)), int(m.group(3)), int(m.group(4))) | ||
currency = m.group(5) | ||
amount = Decimal(m.group(6).replace(',','.')) | ||
if m.group(1) == 'D': | ||
amount = -amount | ||
if self.current_statement: | ||
if field.startswith('60'): | ||
self.current_statement.opening_balance= (amount, currency, value_date) | ||
elif field.startswith('62'): | ||
self.current_statement.closing_balance = (amount, currency, value_date) | ||
elif field.startswith('64'): | ||
self.current_statement.closing_available_balance = (amount, currency, value_date) | ||
else: | ||
raise InvalidFieldValue("Invalid field %s value `%s`" % (field, value)) | ||
else: | ||
raise RunawayField("Runaway field %s `%s`" % (field, value)) | ||
|
||
else: | ||
raise InvalidFieldValue("Invalid field %s value `%s`" % (field, value)) | ||
|
||
return m | ||
|
||
|
||
def _field_60f(self, value, subfields=[]): | ||
return self._field_6xx_balance(value, '60f') | ||
|
||
|
||
def _field_62f(self, value, subfields=[]): | ||
return self._field_6xx_balance(value, '62f') | ||
|
||
|
||
def _field_64(self, value, subfields=[]): | ||
return self._field_6xx_balance(value, '64') | ||
|
||
|
||
def _field_86(self, value, subfields=[]): | ||
raise MissingFieldParser("Please define parser for field 86") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
|
||
import re | ||
import MT940 | ||
|
||
class MT940TransactionSK(MT940.MT940Transaction): | ||
|
||
__slots__ = ['vs', 'ks', 'ss', 'message', 'other_account', 'other_name'] | ||
|
||
|
||
class MT940StatementSK(MT940.MT940Statement): | ||
|
||
_transaction_class = MT940TransactionSK | ||
|
||
|
||
class TabaParser940(MT940.MT940Parser): | ||
|
||
_statement_class = MT940StatementSK | ||
|
||
RE20 = re.compile("^MC940(\d{6})00000$") | ||
RE25 = re.compile("^SK(\d{2})(\d{4})(\d{6})(\d{10})$") | ||
|
||
def __init__(self): | ||
super(TabaParser940, self).__init__() | ||
self.name = 'Tatrabanka SK' | ||
|
||
def _field_25(self, value, subfields=[]): | ||
m = super(TabaParser940, self)._field_25(value, subfields) | ||
self.current_statement.account = "%s-%s/%s" % (m.group(2), m.group(3), m.group(4)) | ||
|
||
def _field_86(self, value, subfields=[]): | ||
for line in subfields: | ||
if line.startswith('?24'): | ||
self.current_statement.update_transaction(message=line[3:]) | ||
elif line.startswith('?31'): | ||
m = self.RE25.match(line[3:]) | ||
if m: | ||
self.current_statement.update_transaction(other_account = "%s-%s/%s" % | ||
(m.group(2), m.group(3), m.group(4))) | ||
else: | ||
pass | ||
#raise InvalidFieldValue("Invalid field 86:31 value `%s`" % line[3:]) | ||
elif line.startswith('?32'): | ||
self.current_statement.update_transaction(other_name=line[3:]) | ||
elif line.startswith('?60'): | ||
(vs, ss, ks) = (None, None, None) | ||
symbols = filter(None, line[3:].split('/')) | ||
for symbol in symbols: | ||
if symbol.startswith('VS'): | ||
vs = symbol[3:] or None | ||
elif symbol.startswith('SS'): | ||
ss = symbol[3:] or None | ||
elif symbol.startswith('KS'): | ||
ks = symbol[3:] or None | ||
else: | ||
pass | ||
#raise InvalidFieldValue("Invalid field 86:60 value `%s`" % line[3:]) | ||
|
||
self.current_statement.update_transaction(vs = vs, ss = ss, ks = ks) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
|
||
class SwiftReader(object): | ||
|
||
def __init__(self, parser): | ||
self._parser_class = parser | ||
self._parser = self._parser_class() | ||
|
||
def parse_file(self, filename): | ||
with open(filename) as file: | ||
return self.parse(file.read().splitlines()) | ||
|
||
def parse(self, lines): | ||
return self._parser.parse(lines) |