1# vim: fileencoding=utf-8:

2'''

3

4 The MT940 format is a standard for bank account statements. It is used by

5 many banks in Europe and is based on the SWIFT MT940 format.

6

7 The MT940 tags are:

8

9+---------+-----------------------------------------------------------------+

10| Tag | Description |

11+=========+=================================================================+

12| `:13:` | Date/Time indication at which the report was created |

13+---------+-----------------------------------------------------------------+

14| `:20:` | Transaction Reference Number |

15+---------+-----------------------------------------------------------------+

16| `:21:` | Related Reference Number |

17+---------+-----------------------------------------------------------------+

18| `:25:` | Account Identification |

19+---------+-----------------------------------------------------------------+

20| `:28:` | Statement Number |

21+---------+-----------------------------------------------------------------+

22| `:34:` | The floor limit for debit and credit |

23+---------+-----------------------------------------------------------------+

24| `:60F:` | Opening Balance |

25+---------+-----------------------------------------------------------------+

26| `:60M:` | Intermediate Balance |

27+---------+-----------------------------------------------------------------+

28| `:60E:` | Closing Balance |

29+---------+-----------------------------------------------------------------+

30| `:61:` | Statement Line |

31+---------+-----------------------------------------------------------------+

32| `:62:` | Closing Balance |

33+---------+-----------------------------------------------------------------+

34| `:62M:` | Intermediate Closing Balance |

35+---------+-----------------------------------------------------------------+

36| `:62F:` | Final Closing Balance |

37+---------+-----------------------------------------------------------------+

38| `:64:` | Available Balance |

39+---------+-----------------------------------------------------------------+

40| `:65:` | Forward Available Balance |

41+---------+-----------------------------------------------------------------+

42| `:86:` | Transaction Information |

43+---------+-----------------------------------------------------------------+

44| `:90:` | Total number and amount of debit entries |

45+---------+-----------------------------------------------------------------+

46| `:NS:` | Bank specific Non-swift extensions containing extra information |

47+---------+-----------------------------------------------------------------+

48

49 Format

50---------------------

51

52 Sources:

53

54.. _Swift for corporates: http://www.sepaforcorporates.com/\

55 swift-for-corporates/account-statement-mt940-file-format-overview/

56.. _Rabobank MT940: https://www.rabobank.nl/images/\

57 formaatbeschrijving_swift_bt940s_1_0_nl_rib_29539296.pdf

58

59 - `Swift for corporates`_

60 - `Rabobank MT940`_

61

62 The pattern for the tags use the following syntax:

63

64::

65

66 [] = optional

67 ! = fixed length

68 a = Text

69 x = Alphanumeric, seems more like text actually. Can include special

70 characters (slashes) and whitespace as well as letters and numbers

71 d = Numeric separated by decimal (usually comma)

72 c = Code list value

73 n = Numeric

74'''

75 from __future__ import print_function

76

77 import logging

78 import re

79

80 try:

81 import enum

82 except ImportError: # pragma: no cover

83 import sys

84

85 print('MT940 requires the `enum34` package', file=sys.stderr)

86

87 class enum(object):

88 @staticmethod

89 def unique(*args, **kwargs):

90 return []

91

92 Enum = object

93

94 from . import models

95

96 logger = logging.getLogger(__name__)

97

98

99 class Tag(object):

100 id = 0

101 RE_FLAGS = re.IGNORECASE | re.VERBOSE | re.UNICODE

102 scope = models.Transactions

103

104 def __init__(self):

105 self.re = re.compile(self.pattern, self.RE_FLAGS)

106

107 def parse(self, transactions, value):

108 match = self.re.match(value)

109 if match: # pragma: no branch

110 self.logger.debug(

111 'matched (%d) %r against "%s", got: %s',

112 len(value), value, self.pattern,

113 match.groupdict()

114 )

115 else: # pragma: no cover

116 self.logger.error(

117 'matching id=%s (len=%d) "%s" against\n %s',

118 self.id,

119 len(value),

120 value,

121 self.pattern

122 )

123

124 part_value = value

125 for pattern in self.pattern.split('\n'):

126 match = re.match(pattern, part_value, self.RE_FLAGS)

127 if match:

128 self.logger.info(

129 'matched %r against %r, got: %s',

130 pattern, match.group(0),

131 match.groupdict()

132 )

133 part_value = part_value[len(match.group(0)):]

134 else:

135 self.logger.error(

136 'no match for %r against %r',

137 pattern, part_value

138 )

139

140 raise RuntimeError(

141 'Unable to parse %r from %r' % (self, value),

142 self, value

143 )

144 return match.groupdict()

145

146 def __call__(self, transactions, value):

147 return value

148

149 def __new__(cls, *args, **kwargs):

150 cls.name = cls.__name__

151

152 words = re.findall('([A-Z][a-z]+)', cls.__name__)

153 cls.slug = '_'.join(w.lower() for w in words)

154 cls.logger = logger.getChild(cls.name)

155

156 return object.__new__(cls, *args, **kwargs)

157

158 def __hash__(self):

159 return self.id

160

161

162 class DateTimeIndication(Tag):

163 '''Date/Time indication at which the report was created

164

165 Pattern: 6!n4!n1! x4!n

166 '''

167 id = 13

168 pattern = r'''^

169 (?P<year>\d{2})

170 (?P<month>\d{2})

171 (?P<day>\d{2})

172 (?P<hour>\d{2})

173 (?P<minute>\d{2})

174 (\+(?P<offset>\d{4})|)

175 '''

176

177 def __call__(self, transactions, value):

178 data = super(DateTimeIndication, self).__call__(transactions, value)

179 return {

180 'date': models.DateTime(**data)

181 }

182

183

184 class TransactionReferenceNumber(Tag):

185

186 '''Transaction reference number

187

188 Pattern: 16x

189 '''

190 id = 20

191 pattern = r'(?P<transaction_reference>.{0,16})'

192

193

194 class RelatedReference(Tag):

195

196 '''Related reference

197

198 Pattern: 16x

199 '''

200 id = 21

201 pattern = r'(?P<related_reference>.{0,16})'

202

203

204 class AccountIdentification(Tag):

205

206 '''Account identification

207

208 Pattern: 35x

209 '''

210 id = 25

211 pattern = r'(?P<account_identification>.{0,35})'

212

213

214 class StatementNumber(Tag):

215

216 '''Statement number / sequence number

217

218 Pattern: 5n[/5n]

219 '''

220 id = 28

221 pattern = r'''

222 (?P<statement_number>\d{1,5}) # 5n

223 (?:/?(?P<sequence_number>\d{1,5}))? # [/5n]

224 $'''

225

226

227 class FloorLimitIndicator(Tag):

228 '''Floor limit indicator

229 indicates the minimum value reported for debit and credit amounts

230

231 Pattern: :34F:GHSC0,00

232 '''

233 id = 34

234 pattern = r'''^

235 (?P<currency>[A-Z]{3}) # 3!a Currency

236 (?P<status>[DC ]?) # 2a Debit/Credit Mark

237 (?P<amount>[0-9,]{0,16}) # 15d Amount (includes decimal sign, so 16)

238 $'''

239

240 def __call__(self, transactions, value):

241 data = super(FloorLimitIndicator, self).__call__(transactions, value)

242 if data['status']:

243 return {

244 data['status'].lower() + '_floor_limit': models.Amount(**data)

245 }

246

247 data_d = data.copy()

248 data_c = data.copy()

249 data_d.update({'status': 'D'})

250 data_c.update({'status': 'C'})

251 return {

252 'd_floor_limit': models.Amount(**data_d),

253 'c_floor_limit': models.Amount(**data_c)

254 }

255

256

257 class NonSwift(Tag):

258

259 '''Non-swift extension for MT940 containing extra information. The

260 actual definition is not consistent between banks so the current

261 implementation is a tad limited. Feel free to extend the implementation

262 and create a pull request with a better version :)

263

264 It seems this could be anything so we'll have to be flexible about it.

265

266 Pattern: `2!n35x | *x`

267 '''

268

269 class scope(models.Transaction, models.Transactions):

270 pass

271

272 id = 'NS'

273

274 pattern = r'''

275 (?P<non_swift>

276 (

277 (\d{2}.{0,})

278 (\n\d{2}.{0,})*

279 )|(

280 [^\n]*

281 )

282 )

283 $'''

284 sub_pattern = r'''

285 (?P<ns_id>\d{2})(?P<ns_data>.{0,})

286 '''

287 sub_pattern_m = re.compile(

288 sub_pattern,

289 re.IGNORECASE | re.VERBOSE | re.UNICODE

290 )

291

292 def __call__(self, transactions, value):

293 text = []

294 data = value['non_swift']

295 for line in data.split('\n'):

296 frag = self.sub_pattern_m.match(line)

297 if frag and frag.group(2):

298 ns = frag.groupdict()

299 value['non_swift_' + ns['ns_id']] = ns['ns_data']

300 text.append(ns['ns_data'])

301 elif len(text) and text[-1]:

302 text.append('')

303 elif line.strip():

304 text.append(line.strip())

305 value['non_swift_text'] = '\n'.join(text)

306 value['non_swift'] = data

307 return value

308

309

310 class BalanceBase(Tag):

311

312 '''Balance base

313

314 Pattern: 1!a6!n3!a15d

315 '''

316 pattern = r'''^

317 (?P<status>[DC]) # 1!a Debit/Credit

318 (?P<year>\d{2}) # 6!n Value Date (YYMMDD)

319 (?P<month>\d{2})

320 (?P<day>\d{2})

321 (?P<currency>.{3}) # 3!a Currency

322 (?P<amount>[0-9,]{0,16}) # 15d Amount (includes decimal sign, so 16)

323 '''

324

325 def __call__(self, transactions, value):

326 data = super(BalanceBase, self).__call__(transactions, value)

327 data['amount'] = models.Amount(**data)

328 data['date'] = models.Date(**data)

329 return {

330 self.slug: models.Balance(**data)

331 }

332

333

334 class OpeningBalance(BalanceBase):

335 id = 60

336

337

338 class FinalOpeningBalance(BalanceBase):

339 id = '60F'

340

341

342 class IntermediateOpeningBalance(BalanceBase):

343 id = '60M'

344

345

346 class Statement(Tag):

347

348 '''

349

350 The MT940 Tag 61 provides information about a single transaction that

351 has taken place on the account. Each transaction is identified by a

352 unique transaction reference number (Tag 20) and is described in the

353 Statement Line (Tag 61).

354

355 Pattern: 6!n[4!n]2a[1!a]15d1!a3!c23x[//16x]

356

357 The fields are:

358

359 - `value_date`: transaction date (YYMMDD)

360 - `entry_date`: Optional 4-digit month value and 2-digit day value of

361 the entry date (MMDD)

362 - `funds_code`: Optional 1-character code indicating the funds type (

363 the third character of the currency code if needed)

364 - `amount`: 15-digit value of the transaction amount, including commas

365 for decimal separation

366 - `transaction_type`: Optional 4-character transaction type

367 identification code starting with a letter followed by alphanumeric

368 characters and spaces

369 - `customer_reference`: Optional 16-character customer reference,

370 excluding any bank reference

371 - `bank_reference`: Optional 23-character bank reference starting with

372 "//"

373 - `supplementary_details`: Optional 34-character supplementary details

374 about the transaction.

375

376 The Tag 61 can occur multiple times within an MT940 file, with each

377 occurrence representing a different transaction.

378

379 '''

380 id = 61

381 scope = models.Transaction

382 pattern = r'''^

383 (?P<year>\d{2}) # 6!n Value Date (YYMMDD)

384 (?P<month>\d{2})

385 (?P<day>\d{2})

386 (?P<entry_month>\d{2})? # [4!n] Entry Date (MMDD)

387 (?P<entry_day>\d{2})?

388 (?P<status>R?[DC]) # 2a Debit/Credit Mark

389 (?P<funds_code>[A-Z])? # [1!a] Funds Code (3rd character of the currency

390 # code, if needed)

391 [\n ]? # apparently some banks (sparkassen) incorporate newlines here

392 # cuscal can also send a space here as well

393 (?P<amount>[\d,]{1,15}) # 15d Amount

394 (?P<id>[A-Z][A-Z0-9 ]{3})? # 1!a3!c Transaction Type Identification Code

395 # We need the (slow) repeating negative lookahead to search for // so we

396 # don't acciddntly include the bank reference in the customer reference.

397 (?P<customer_reference>((?!//)[^\n]){0,16}) # 16x Customer Reference

398 (//(?P<bank_reference>.{0,23}))? # [//23x] Bank Reference

399 (\n?(?P<extra_details>.{0,34}))? # [34x] Supplementary Details

400 $'''

401

402 def __call__(self, transactions, value):

403 data = super(Statement, self).__call__(transactions, value)

404 data.setdefault('currency', transactions.currency)

405

406 data['amount'] = models.Amount(**data)

407 date = data['date'] = models.Date(**data)

408

409 if data.get('entry_day') and data.get('entry_month'):

410 entry_date = data['entry_date'] = models.Date(

411 day=data.get('entry_day'),

412 month=data.get('entry_month'),

413 year=str(data['date'].year),

414 )

415

416 if date > entry_date and (date - entry_date).days >= 330:

417 year = 1

418 elif entry_date > date and (entry_date - date).days >= 330:

419 year = -1

420 else:

421 year = 0

422

423 data['guessed_entry_date'] = models.Date(

424 day=entry_date.day,

425 month=entry_date.month,

426 year=entry_date.year + year,

427 )

428

429 return data

430

431

432 class StatementASNB(Statement):

433 '''StatementASNB

434

435 From: https://www.sepaforcorporates.com/swift-for-corporates

436

437 Pattern: 6!n[4!n]2a[1!a]15d1!a3!c16x[//16x]

438 [34x]

439

440 But ASN bank puts the IBAN in the customer reference, which is acording to

441 Wikipedia at most 34 characters.

442

443 So this is the new pattern:

444

445 Pattern: 6!n[4!n]2a[1!a]15d1!a3!c34x[//16x]

446 [34x]

447 '''

448 pattern = r'''^

449 (?P<year>\d{2}) # 6!n Value Date (YYMMDD)

450 (?P<month>\d{2})

451 (?P<day>\d{2})

452 (?P<entry_month>\d{2})? # [4!n] Entry Date (MMDD)

453 (?P<entry_day>\d{2})?

454 (?P<status>[A-Z]?[DC]) # 2a Debit/Credit Mark

455 (?P<funds_code>[A-Z])? # [1!a] Funds Code (3rd character of the currency

456 # code, if needed)

457 \n? # apparently some banks (sparkassen) incorporate newlines here

458 (?P<amount>[\d,]{1,15}) # 15d Amount

459 (?P<id>[A-Z][A-Z0-9 ]{3})? # 1!a3!c Transaction Type Identification Code

460 (?P<customer_reference>.{0,34}) # 34x Customer Reference

461 (//(?P<bank_reference>.{0,16}))? # [//16x] Bank Reference

462 (\n?(?P<extra_details>.{0,34}))? # [34x] Supplementary Details

463 $'''

464

465 def __call__(self, transactions, value):

466 return super(StatementASNB, self).__call__(transactions, value)

467

468

469 class ClosingBalance(BalanceBase):

470 id = 62

471

472

473 class IntermediateClosingBalance(ClosingBalance):

474 id = '62M'

475

476

477 class FinalClosingBalance(ClosingBalance):

478 id = '62F'

479

480

481 class AvailableBalance(BalanceBase):

482 id = 64

483

484

485 class ForwardAvailableBalance(BalanceBase):

486 id = 65

487

488

489 class TransactionDetails(Tag):

490

491 '''Transaction details

492

493 Pattern: 6x65x

494 '''

495 id = 86

496 scope = models.Transaction

497 pattern = r'''

498 (?P<transaction_details>(([\s\S]{0,65}\r?\n?){0,8}[\s\S]{0,65}))

499 '''

500

501

502 class SumEntries(Tag):

503 '''Number and Sum of debit Entries

504

505 '''

506

507 id = 90

508 pattern = r'''^

509 (?P<number>\d*)

510 (?P<currency>.{3}) # 3!a Currency

511 (?P<amount>[\d,]{1,15}) # 15d Amount

512 '''

513

514 def __call__(self, transactions, value):

515 data = super(SumEntries, self).__call__(transactions, value)

516

517 data['status'] = self.status

518 return {

519 self.slug: models.SumAmount(**data)

520 }

521

522

523 class SumDebitEntries(SumEntries):

524 status = 'D'

525 id = '90D'

526

527

528 class SumCreditEntries(SumEntries):

529 status = 'C'

530 id = '90C'

531

532

533@enum.unique

534 class Tags(enum.Enum):

535 DATE_TIME_INDICATION = DateTimeIndication()

536 TRANSACTION_REFERENCE_NUMBER = TransactionReferenceNumber()

537 RELATED_REFERENCE = RelatedReference()

538 ACCOUNT_IDENTIFICATION = AccountIdentification()

539 STATEMENT_NUMBER = StatementNumber()

540 OPENING_BALANCE = OpeningBalance()

541 INTERMEDIATE_OPENING_BALANCE = IntermediateOpeningBalance()

542 FINAL_OPENING_BALANCE = FinalOpeningBalance()

543 STATEMENT = Statement()

544 CLOSING_BALANCE = ClosingBalance()

545 INTERMEDIATE_CLOSING_BALANCE = IntermediateClosingBalance()

546 FINAL_CLOSING_BALANCE = FinalClosingBalance()

547 AVAILABLE_BALANCE = AvailableBalance()

548 FORWARD_AVAILABLE_BALANCE = ForwardAvailableBalance()

549 TRANSACTION_DETAILS = TransactionDetails()

550 FLOOR_LIMIT_INDICATOR = FloorLimitIndicator()

551 NON_SWIFT = NonSwift()

552 SUM_ENTRIES = SumEntries()

553 SUM_DEBIT_ENTRIES = SumDebitEntries()

554 SUM_CREDIT_ENTRIES = SumCreditEntries()

555

556

557 TAG_BY_ID = {t.value.id: t.value for t in Tags}