From eb479b8fe19be8dd13447522892cebab18c41196 Mon Sep 17 00:00:00 2001 From: Miku Laitinen Date: Mon, 30 Apr 2018 12:46:56 +0300 Subject: [PATCH 1/2] [FEAT] Support the new CSV format introduced in Apr 2018 --- src/ofxstatement/plugins/revolut.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/ofxstatement/plugins/revolut.py b/src/ofxstatement/plugins/revolut.py index db6d2ca..becdd9b 100644 --- a/src/ofxstatement/plugins/revolut.py +++ b/src/ofxstatement/plugins/revolut.py @@ -4,8 +4,10 @@ from ofxstatement.parser import CsvStatementParser from ofxstatement.statement import StatementLine, BankAccount -SIGNATURE = "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; " \ - "Exchange Out; Exchange In; Balance (EUR); Category" +SIGNATURES = [ + "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category", # Pre Apr-2018 + "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category; Notes", # Apr-2018 +] TRANSACTION_TYPES = { "To ": "XFER", @@ -38,6 +40,12 @@ def parse_payee_memo(self, value): else: return self.parse_value(value, 'payee'), '' + def parse_amount(self, value): + if not value or not value.strip(): + return 0 + + return self.parse_float(value.strip().replace(',', '')) + def parse_record(self, line): # Free Headerline if self.cur_record <= 1: @@ -47,8 +55,8 @@ def parse_record(self, line): stmt_line.date = self.parse_datetime(line[0].strip()) # Amount - paid_out = -self.parse_float(line[2].strip() or '0') - paid_in = self.parse_float(line[3].strip() or '0') + paid_out = -self.parse_amount(line[2]) + paid_in = self.parse_amount(line[3]) stmt_line.amount = paid_out or paid_in reference = line[1].strip() @@ -76,6 +84,14 @@ def parse_record(self, line): else: stmt_line.memo = self.parse_value(reference, 'memo') + # Notes (from Apr-2018) + if len(line) > 8 and line[8].strip(): + if not stmt_line.memo: + stmt_line.memo = u'' + elif len(stmt_line.memo.strip()) > 0: + stmt_line.memo += u' ' + stmt_line.memo += u'({})'.format(line[8].strip()) + return stmt_line @@ -86,7 +102,7 @@ def get_parser(self, fin): f = open(fin, "r", encoding='utf-8') signature = f.readline().strip() f.seek(0) - if signature == SIGNATURE: + if signature in SIGNATURES: parser = RevolutCSVStatementParser(f) if 'account' in self.settings: parser.statement.account_id = self.settings['account'] From e6f27b3e103aa5e7c293a6b6cfc0589018378237 Mon Sep 17 00:00:00 2001 From: Miku Laitinen Date: Mon, 30 Apr 2018 12:47:32 +0300 Subject: [PATCH 2/2] Changed line feed from CRLF to LF in the plugin. Version 1.1.0. --- setup.py | 2 +- src/ofxstatement/plugins/revolut.py | 232 ++++++++++++++-------------- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/setup.py b/setup.py index ad9b90d..9fdd14c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages from distutils.core import setup -version = "1.0.0" +version = "1.1.0" setup(name='ofxstatement-revolut', version=version, diff --git a/src/ofxstatement/plugins/revolut.py b/src/ofxstatement/plugins/revolut.py index becdd9b..3dc21eb 100644 --- a/src/ofxstatement/plugins/revolut.py +++ b/src/ofxstatement/plugins/revolut.py @@ -1,116 +1,116 @@ -import csv - -from ofxstatement.plugin import Plugin -from ofxstatement.parser import CsvStatementParser -from ofxstatement.statement import StatementLine, BankAccount - -SIGNATURES = [ - "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category", # Pre Apr-2018 - "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category; Notes", # Apr-2018 -] - -TRANSACTION_TYPES = { - "To ": "XFER", - "From ": "XFER", - "Cash at ": "ATM", - "Top-Up by": "DEP", -} - - -class RevolutCSVStatementParser(CsvStatementParser): - - date_format = "%b %d, %Y" - - def split_records(self): - return csv.reader(self.fin, delimiter=';', quotechar='"') - - def parse_value(self, value, field): - value = value.strip() if value else value - if field == "bank_account_to": - return BankAccount("", value) - else: - return super().parse_value(value, field) - - def parse_payee_memo(self, value): - if 'FX Rate' in value: - limit = value.find('FX Rate') - payee = self.parse_value(value[0:limit], 'payee') - memo = self.parse_value(value[limit:], 'memo') - return payee, memo - else: - return self.parse_value(value, 'payee'), '' - - def parse_amount(self, value): - if not value or not value.strip(): - return 0 - - return self.parse_float(value.strip().replace(',', '')) - - def parse_record(self, line): - # Free Headerline - if self.cur_record <= 1: - return None - - stmt_line = StatementLine() - stmt_line.date = self.parse_datetime(line[0].strip()) - - # Amount - paid_out = -self.parse_amount(line[2]) - paid_in = self.parse_amount(line[3]) - stmt_line.amount = paid_out or paid_in - - reference = line[1].strip() - trntype = False - for prefix, transaction_type in TRANSACTION_TYPES.items(): - if reference.startswith(prefix): - trntype = transaction_type - break - - if not trntype: - trntype = 'POS' # Default: Debit card payment - - # It's ... pretty ugly, but I see no other way to do this than parse - # the reference string because that's all the data we have. - stmt_line.trntype = trntype - if trntype == 'POS': - stmt_line.payee, stmt_line.memo = self.parse_payee_memo(reference) - elif reference.startswith('Cash at '): - stmt_line.payee, stmt_line.memo = self.parse_payee_memo( - reference[8:]) - elif reference.startswith('To ') or reference.startswith('From '): - stmt_line.payee = self.parse_value( - reference[reference.find(' '):], 'payee' - ) - else: - stmt_line.memo = self.parse_value(reference, 'memo') - - # Notes (from Apr-2018) - if len(line) > 8 and line[8].strip(): - if not stmt_line.memo: - stmt_line.memo = u'' - elif len(stmt_line.memo.strip()) > 0: - stmt_line.memo += u' ' - stmt_line.memo += u'({})'.format(line[8].strip()) - - return stmt_line - - -class RevolutPlugin(Plugin): - """Revolut""" - - def get_parser(self, fin): - f = open(fin, "r", encoding='utf-8') - signature = f.readline().strip() - f.seek(0) - if signature in SIGNATURES: - parser = RevolutCSVStatementParser(f) - if 'account' in self.settings: - parser.statement.account_id = self.settings['account'] - if 'currency' in self.settings: - parser.statement.currency = self.settings['currency'] - parser.statement.bank_id = self.settings.get('bank', 'Revolut') - return parser - - # no plugin with matching signature was found - raise Exception("No suitable Revolut parser " - "found for this statement file.") +import csv + +from ofxstatement.plugin import Plugin +from ofxstatement.parser import CsvStatementParser +from ofxstatement.statement import StatementLine, BankAccount + +SIGNATURES = [ + "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category", # Pre Apr-2018 + "Completed Date ; Reference ; Paid Out (EUR) ; Paid In (EUR) ; Exchange Out; Exchange In; Balance (EUR); Category; Notes", # Apr-2018 +] + +TRANSACTION_TYPES = { + "To ": "XFER", + "From ": "XFER", + "Cash at ": "ATM", + "Top-Up by": "DEP", +} + + +class RevolutCSVStatementParser(CsvStatementParser): + + date_format = "%b %d, %Y" + + def split_records(self): + return csv.reader(self.fin, delimiter=';', quotechar='"') + + def parse_value(self, value, field): + value = value.strip() if value else value + if field == "bank_account_to": + return BankAccount("", value) + else: + return super().parse_value(value, field) + + def parse_payee_memo(self, value): + if 'FX Rate' in value: + limit = value.find('FX Rate') + payee = self.parse_value(value[0:limit], 'payee') + memo = self.parse_value(value[limit:], 'memo') + return payee, memo + else: + return self.parse_value(value, 'payee'), '' + + def parse_amount(self, value): + if not value or not value.strip(): + return 0 + + return self.parse_float(value.strip().replace(',', '')) + + def parse_record(self, line): + # Free Headerline + if self.cur_record <= 1: + return None + + stmt_line = StatementLine() + stmt_line.date = self.parse_datetime(line[0].strip()) + + # Amount + paid_out = -self.parse_amount(line[2]) + paid_in = self.parse_amount(line[3]) + stmt_line.amount = paid_out or paid_in + + reference = line[1].strip() + trntype = False + for prefix, transaction_type in TRANSACTION_TYPES.items(): + if reference.startswith(prefix): + trntype = transaction_type + break + + if not trntype: + trntype = 'POS' # Default: Debit card payment + + # It's ... pretty ugly, but I see no other way to do this than parse + # the reference string because that's all the data we have. + stmt_line.trntype = trntype + if trntype == 'POS': + stmt_line.payee, stmt_line.memo = self.parse_payee_memo(reference) + elif reference.startswith('Cash at '): + stmt_line.payee, stmt_line.memo = self.parse_payee_memo( + reference[8:]) + elif reference.startswith('To ') or reference.startswith('From '): + stmt_line.payee = self.parse_value( + reference[reference.find(' '):], 'payee' + ) + else: + stmt_line.memo = self.parse_value(reference, 'memo') + + # Notes (from Apr-2018) + if len(line) > 8 and line[8].strip(): + if not stmt_line.memo: + stmt_line.memo = u'' + elif len(stmt_line.memo.strip()) > 0: + stmt_line.memo += u' ' + stmt_line.memo += u'({})'.format(line[8].strip()) + + return stmt_line + + +class RevolutPlugin(Plugin): + """Revolut""" + + def get_parser(self, fin): + f = open(fin, "r", encoding='utf-8') + signature = f.readline().strip() + f.seek(0) + if signature in SIGNATURES: + parser = RevolutCSVStatementParser(f) + if 'account' in self.settings: + parser.statement.account_id = self.settings['account'] + if 'currency' in self.settings: + parser.statement.currency = self.settings['currency'] + parser.statement.bank_id = self.settings.get('bank', 'Revolut') + return parser + + # no plugin with matching signature was found + raise Exception("No suitable Revolut parser " + "found for this statement file.")