You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

249 lines
9.3 KiB

  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. """Generic parser for MT940 files, base for customized versions per bank."""
  4. ##############################################################################
  5. #
  6. # OpenERP, Open Source Management Solution
  7. # This module copyright (C) 2014 Therp BV (<http://therp.nl>).
  8. #
  9. # This program is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU Affero General Public License as
  11. # published by the Free Software Foundation, either version 3 of the
  12. # License, or (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU Affero General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Affero General Public License
  20. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. #
  22. ##############################################################################
  23. import re
  24. import logging
  25. from datetime import datetime
  26. from openerp.addons.bank_statement_parse import parserlib
  27. def str2amount(sign, amount_str):
  28. """Convert sign (C or D) and amount in string to signed amount (float)."""
  29. factor = (1 if sign == 'C' else -1)
  30. return factor * float(amount_str.replace(',', '.'))
  31. def get_subfields(data, codewords):
  32. """Return dictionary with value array for each codeword in data.
  33. For instance:
  34. data =
  35. /BENM//NAME/Kosten/REMI/Periode 01-10-2013 t/m 31-12-2013/ISDT/20
  36. codewords = ['BENM', 'ADDR', 'NAME', 'CNTP', ISDT', 'REMI']
  37. Then return subfields = {
  38. 'BENM': [],
  39. 'NAME': ['Kosten'],
  40. 'REMI': ['Periode 01-10-2013 t', 'm 31-12-2013'],
  41. 'ISDT': ['20'],
  42. }
  43. """
  44. subfields = {}
  45. current_codeword = None
  46. for word in data.split('/'):
  47. if not word and not current_codeword:
  48. continue
  49. if word in codewords:
  50. current_codeword = word
  51. subfields[current_codeword] = []
  52. continue
  53. if current_codeword in subfields:
  54. subfields[current_codeword].append(word)
  55. return subfields
  56. def get_counterpart(transaction, subfield):
  57. """Get counterpart from transaction.
  58. Counterpart is often stored in subfield of tag 86. The subfield
  59. can be BENM, ORDP, CNTP"""
  60. if not subfield:
  61. return # subfield is empty
  62. if len(subfield) >= 1 and subfield[0]:
  63. transaction.remote_account = subfield[0]
  64. if len(subfield) >= 2 and subfield[1]:
  65. transaction.remote_bank_bic = subfield[1]
  66. if len(subfield) >= 3 and subfield[2]:
  67. transaction.remote_owner = subfield[2]
  68. if len(subfield) >= 4 and subfield[3]:
  69. transaction.remote_owner_city = subfield[3]
  70. def handle_common_subfields(transaction, subfields):
  71. """Deal with common functionality for tag 86 subfields."""
  72. # Get counterpart from CNTP, BENM or ORDP subfields:
  73. for counterpart_field in ['CNTP', 'BENM', 'ORDP']:
  74. if counterpart_field in subfields:
  75. get_counterpart(transaction, subfields[counterpart_field])
  76. # REMI: Remitter information (text entered by other party on trans.):
  77. if 'REMI' in subfields:
  78. transaction.message = (
  79. '/'.join(x for x in subfields['REMI'] if x))
  80. # Get transaction reference subfield (might vary):
  81. if transaction.eref in subfields:
  82. transaction.eref = ''.join(
  83. subfields[transaction.eref])
  84. class MT940(object):
  85. """Inherit this class in your account_banking.parsers.models.parser,
  86. define functions to handle the tags you need to handle and adjust static
  87. variables as needed.
  88. At least, you should override handle_tag_61 and handle_tag_86.
  89. """
  90. def __init__(self):
  91. """Initialize parser - override at least header_regex.
  92. This in fact uses the ING syntax, override in others."""
  93. self.header_lines = 3 # Number of lines to skip
  94. self.header_regex = '^{1:[0-9A-Z]{25,25}}' # Start of relevant data
  95. self.footer_regex = '^-}$|^-XXX$' # Stop processing on seeing this
  96. self.tag_regex = '^:[0-9]{2}[A-Z]*:' # Start of new tag
  97. self.current_statement = None
  98. self.current_transaction = None
  99. self.statements = []
  100. def create_transaction(self):
  101. """Create and return BankTransaction object."""
  102. transaction = parserlib.BankTransaction()
  103. return transaction
  104. def is_mt940(self, line):
  105. """determine if a line is the header of a statement"""
  106. if not bool(re.match(self.header_regex, line)):
  107. raise ValueError(
  108. 'This does not seem to be a MT940 format bank statement.')
  109. def parse(self, data):
  110. """Parse mt940 bank statement file contents."""
  111. self.is_mt940(data)
  112. iterator = data.replace('\r\n', '\n').split('\n').__iter__()
  113. line = None
  114. record_line = ''
  115. try:
  116. while True:
  117. if not self.current_statement:
  118. self.handle_header(line, iterator)
  119. line = iterator.next()
  120. if not self.is_tag(line) and not self.is_footer(line):
  121. record_line = self.append_continuation_line(
  122. record_line, line)
  123. continue
  124. if record_line:
  125. self.handle_record(record_line)
  126. if self.is_footer(line):
  127. self.handle_footer(line, iterator)
  128. record_line = ''
  129. continue
  130. record_line = line
  131. except StopIteration:
  132. pass
  133. if self.current_statement:
  134. if record_line:
  135. self.handle_record(record_line)
  136. record_line = ''
  137. self.statements.append(self.current_statement)
  138. self.current_statement = None
  139. return self.statements
  140. def append_continuation_line(self, line, continuation_line):
  141. """append a continuation line for a multiline record.
  142. Override and do data cleanups as necessary."""
  143. return line + continuation_line
  144. def create_statement(self):
  145. """create a BankStatement."""
  146. return parserlib.BankStatement()
  147. def is_footer(self, line):
  148. """determine if a line is the footer of a statement"""
  149. return line and bool(re.match(self.footer_regex, line))
  150. def is_tag(self, line):
  151. """determine if a line has a tag"""
  152. return line and bool(re.match(self.tag_regex, line))
  153. def handle_header(self, line, iterator):
  154. """skip header lines, create current statement"""
  155. for dummy_i in range(self.header_lines):
  156. iterator.next()
  157. self.current_statement = self.create_statement()
  158. def handle_footer(self, line, iterator):
  159. """add current statement to list, reset state"""
  160. self.statements.append(self.current_statement)
  161. self.current_statement = None
  162. def handle_record(self, line):
  163. """find a function to handle the record represented by line"""
  164. tag_match = re.match(self.tag_regex, line)
  165. tag = tag_match.group(0).strip(':')
  166. if not hasattr(self, 'handle_tag_%s' % tag):
  167. logging.error('Unknown tag %s', tag)
  168. logging.error(line)
  169. return
  170. handler = getattr(self, 'handle_tag_%s' % tag)
  171. handler(line[tag_match.end():])
  172. def handle_tag_20(self, data):
  173. """Contains unique ? message ID"""
  174. pass
  175. def handle_tag_25(self, data):
  176. """Handle tag 25: local bank account information."""
  177. data = data.replace('EUR', '').replace('.', '').strip()
  178. self.current_statement.local_account = data
  179. def handle_tag_28C(self, data):
  180. """get sequence number _within_this_batch_ - this alone
  181. doesn't provide a unique id!"""
  182. self.current_statement.statement_id = data
  183. def handle_tag_60F(self, data):
  184. """get start balance and currency"""
  185. self.current_statement.local_currency = data[7:10]
  186. self.current_statement.date = datetime.strptime(data[1:7], '%y%m%d')
  187. self.current_statement.start_balance = str2amount(data[0], data[10:])
  188. self.current_statement.statement_id = '%s/%s' % (
  189. self.current_statement.date.strftime('%Y-%m-%d'),
  190. self.current_statement.statement_id,
  191. )
  192. def handle_tag_62F(self, data):
  193. """get ending balance"""
  194. self.current_statement.end_balance = str2amount(data[0], data[10:])
  195. def handle_tag_64(self, data):
  196. """get current balance in currency"""
  197. pass
  198. def handle_tag_65(self, data):
  199. """get future balance in currency"""
  200. pass
  201. def handle_tag_61(self, data):
  202. """get transaction values"""
  203. transaction = self.create_transaction()
  204. self.current_statement.transactions.append(transaction)
  205. self.current_transaction = transaction
  206. transaction.execution_date = datetime.strptime(data[:6], '%y%m%d')
  207. transaction.value_date = datetime.strptime(data[:6], '%y%m%d')
  208. # ...and the rest already is highly bank dependent
  209. def handle_tag_86(self, data):
  210. """details for previous transaction, here most differences between
  211. banks occur"""
  212. pass
  213. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: