Turn into mbank-compatible script
This commit is contained in:
parent
e1defce133
commit
9fa7da54dc
43
main.py
43
main.py
|
@ -1,39 +1,44 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from revolut import RevolutCsvReader
|
from mbank import MbankCsvReader
|
||||||
from mt940 import Mt940Writer
|
from mt940 import Mt940Writer
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='oddity-revolut-to-mt940',
|
prog="oddity-mbank-to-mt940",
|
||||||
description='Convert Revolut CSV-files to MT940 format.')
|
description="Convert mBank CSV-files to MT940 format.",
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument('--in',
|
parser.add_argument(
|
||||||
dest='input_file',
|
"--in", dest="input_file", help="path to Revolut csv-file", required=True
|
||||||
help='path to Revolut csv-file',
|
)
|
||||||
required=True)
|
|
||||||
|
|
||||||
parser.add_argument('--account-iban',
|
parser.add_argument(
|
||||||
dest='account_iban',
|
"--out", dest="output_file", help="path to MT940 output path", required=True
|
||||||
help='Revolut account IBAN',
|
)
|
||||||
required=True)
|
|
||||||
|
|
||||||
parser.add_argument('--out',
|
|
||||||
dest='output_file',
|
|
||||||
help='path to MT940 output path',
|
|
||||||
required=True)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
reader = RevolutCsvReader(args.input_file)
|
reader = MbankCsvReader(args.input_file)
|
||||||
|
|
||||||
with Mt940Writer(args.output_file, args.account_iban) as writer:
|
print(reader.range)
|
||||||
|
with Mt940Writer(
|
||||||
|
args.output_file,
|
||||||
|
reader.iban,
|
||||||
|
reader.range,
|
||||||
|
reader.starting_balance,
|
||||||
|
reader.date_start,
|
||||||
|
) as writer:
|
||||||
transactions = reader.get_all_transactions()
|
transactions = reader.get_all_transactions()
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
writer.write_transaction(transaction)
|
writer.write_transaction(transaction)
|
||||||
|
|
||||||
print('Wrote {} transactions to file: {}.'.format(len(transactions), args.output_file))
|
print(
|
||||||
|
"Wrote {} transactions to file: {}.".format(
|
||||||
|
len(transactions), args.output_file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
155
mbank.py
Normal file
155
mbank.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
import csv
|
||||||
|
import math
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from data import Transaction
|
||||||
|
|
||||||
|
EXPECT_HEADERS = [
|
||||||
|
"#Data księgowania",
|
||||||
|
"#Data operacji",
|
||||||
|
"#Opis operacji",
|
||||||
|
"#Tytuł",
|
||||||
|
"#Nadawca/Odbiorca",
|
||||||
|
"#Numer konta",
|
||||||
|
"#Kwota",
|
||||||
|
"#Saldo po operacji",
|
||||||
|
]
|
||||||
|
|
||||||
|
NAME_REMOVE_PREFIXES = ["Payment from ", "To "]
|
||||||
|
|
||||||
|
DATE_FORMAT = "%Y-%m-%d"
|
||||||
|
TIME_FORMAT = "%H:%M:%S"
|
||||||
|
DATETIME_FORMAT = DATE_FORMAT + TIME_FORMAT
|
||||||
|
|
||||||
|
FEE_NAME = "Revolut"
|
||||||
|
FEE_IBAN = ""
|
||||||
|
FEE_DESCRIPTION_FORMAT = "Bank transaction fee {}"
|
||||||
|
FEE_DATETIME_DELTA = timedelta(seconds=1)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_float(s: string):
|
||||||
|
return float(s.replace(",", ".").replace(" ", ""))
|
||||||
|
|
||||||
|
|
||||||
|
class MbankCsvReader:
|
||||||
|
def __init__(self, file_path):
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
raise ValueError("File does not exist: {}".format(file_path))
|
||||||
|
|
||||||
|
temp_file_path = file_path + ".tmp"
|
||||||
|
|
||||||
|
# trimming metadata added my mbank export
|
||||||
|
with open(file_path, encoding="cp1250") as old, open(
|
||||||
|
temp_file_path, "w", encoding="utf-8"
|
||||||
|
) as new:
|
||||||
|
lines = old.readlines()
|
||||||
|
(date_start, date_end, _3) = lines[14].split(";")
|
||||||
|
iban = "PL" + lines[20].split(";")[0].replace(" ", "")
|
||||||
|
starting_balance = parse_float(lines[35].split(";")[1].split(" ")[0])
|
||||||
|
self.range = date_start + "-" + date_end
|
||||||
|
self.date_start = datetime.fromisoformat(
|
||||||
|
"-".join(list(reversed(date_start.split("."))))
|
||||||
|
)
|
||||||
|
self.date_end = datetime.fromisoformat(
|
||||||
|
"-".join(list(reversed(date_end.split("."))))
|
||||||
|
)
|
||||||
|
self.iban = iban
|
||||||
|
self.starting_balance = starting_balance
|
||||||
|
new.writelines(lines[37:-1])
|
||||||
|
|
||||||
|
self.filename = temp_file_path
|
||||||
|
|
||||||
|
self.file = open(self.filename, "r", encoding="utf-8")
|
||||||
|
self.reader = csv.reader(self.file, delimiter=";")
|
||||||
|
|
||||||
|
self._validate()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if not self.file.closed:
|
||||||
|
self.file.close()
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
headers = [h for h in next(self.reader) if h != ""]
|
||||||
|
print(headers)
|
||||||
|
print(EXPECT_HEADERS)
|
||||||
|
if headers != EXPECT_HEADERS:
|
||||||
|
raise ValueError("Headers do not match expected Revolut CSV format.")
|
||||||
|
|
||||||
|
def get_all_transactions(self):
|
||||||
|
transactions = []
|
||||||
|
for row in self.reader:
|
||||||
|
transactions = self._parse_transaction(row) + transactions
|
||||||
|
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
def _parse_transaction(self, row):
|
||||||
|
def _santize_name(name_):
|
||||||
|
for remove_prefix in NAME_REMOVE_PREFIXES:
|
||||||
|
if name_.startswith(remove_prefix):
|
||||||
|
name_ = name_[len(remove_prefix) :]
|
||||||
|
|
||||||
|
return name_
|
||||||
|
|
||||||
|
def _parse_datetime(date_str):
|
||||||
|
return datetime.strptime(date_str + "7:00:00", DATETIME_FORMAT)
|
||||||
|
|
||||||
|
print(row)
|
||||||
|
if len(row) < 2 or row[2] == "OTWARCIE RACHUNKU" or row[0] == "":
|
||||||
|
print("skipping")
|
||||||
|
return []
|
||||||
|
|
||||||
|
(
|
||||||
|
_0,
|
||||||
|
completed_date_str,
|
||||||
|
_1,
|
||||||
|
description,
|
||||||
|
name,
|
||||||
|
iban,
|
||||||
|
amount_str,
|
||||||
|
balance_str,
|
||||||
|
_9,
|
||||||
|
) = row
|
||||||
|
|
||||||
|
completed_datetime = _parse_datetime(completed_date_str)
|
||||||
|
amount, balance = parse_float(amount_str), parse_float(balance_str)
|
||||||
|
|
||||||
|
print(amount, balance)
|
||||||
|
|
||||||
|
transaction_without_fee = Transaction(
|
||||||
|
amount=amount,
|
||||||
|
name=_santize_name(name),
|
||||||
|
iban=iban,
|
||||||
|
description=description,
|
||||||
|
datetime=completed_datetime,
|
||||||
|
before_balance=balance - amount,
|
||||||
|
after_balance=balance,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch = [transaction_without_fee]
|
||||||
|
|
||||||
|
# no support for transaction fees
|
||||||
|
# if not math.isclose(fee, 0.00):
|
||||||
|
# fee_transaction = self._make_fee_transaction(
|
||||||
|
# completed_datetime, balance, fee
|
||||||
|
# )
|
||||||
|
# batch.append(fee_transaction)
|
||||||
|
|
||||||
|
return batch
|
||||||
|
|
||||||
|
def _make_fee_transaction(self, completed_datetime, balance, fee):
|
||||||
|
return Transaction(
|
||||||
|
amount=fee,
|
||||||
|
name=FEE_NAME,
|
||||||
|
iban=FEE_IBAN,
|
||||||
|
# include timestamp of transaction to make sure that SnelStart
|
||||||
|
# does not detect similar transactions as the same one
|
||||||
|
description=FEE_DESCRIPTION_FORMAT.format(
|
||||||
|
int(completed_datetime.timestamp())
|
||||||
|
),
|
||||||
|
datetime=completed_datetime + FEE_DATETIME_DELTA,
|
||||||
|
before_balance=balance - fee,
|
||||||
|
after_balance=balance,
|
||||||
|
)
|
132
mt940.py
132
mt940.py
|
@ -1,16 +1,18 @@
|
||||||
from data import Transaction
|
from data import Transaction
|
||||||
|
|
||||||
BANK_NAME = 'Revolut'
|
BANK_NAME = "mBank"
|
||||||
BANK_BIC = 'REVOLT21'
|
BANK_BIC = "BREXPLPWMBK"
|
||||||
|
|
||||||
DEFAULT_SEQUENCE_NO = 1
|
DEFAULT_SEQUENCE_NO = 1
|
||||||
|
|
||||||
|
|
||||||
class Mt940Writer:
|
class Mt940Writer:
|
||||||
|
def __init__(self, filename, account_iban, range, starting_balance, date_start):
|
||||||
def __init__(self, filename, account_iban):
|
self.file = open(filename, "w")
|
||||||
self.file = open(filename, 'w')
|
|
||||||
self.account_iban = account_iban
|
self.account_iban = account_iban
|
||||||
|
self.range = range
|
||||||
|
self.starting_balance = starting_balance
|
||||||
|
self.date_start = date_start
|
||||||
|
|
||||||
self._write_header()
|
self._write_header()
|
||||||
self._written_starting_balance = False
|
self._written_starting_balance = False
|
||||||
|
@ -19,124 +21,106 @@ class Mt940Writer:
|
||||||
self._balance = None
|
self._balance = None
|
||||||
self._date = None
|
self._date = None
|
||||||
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
self.release()
|
self.release()
|
||||||
|
|
||||||
|
def write_transaction(self, transaction: Transaction):
|
||||||
def write_transaction(self, transaction : Transaction):
|
|
||||||
if not self._written_starting_balance:
|
if not self._written_starting_balance:
|
||||||
self._write_starting_balance(transaction.datetime,
|
self._write_starting_balance(self.starting_balance)
|
||||||
transaction.before_balance)
|
|
||||||
|
|
||||||
self.file.writelines([
|
self.file.writelines(
|
||||||
Mt940.make_61(
|
[
|
||||||
transaction.datetime,
|
Mt940.make_61(transaction.datetime, transaction.amount),
|
||||||
transaction.amount),
|
|
||||||
Mt940.make_86(
|
Mt940.make_86(
|
||||||
transaction.iban,
|
transaction.iban, transaction.name, transaction.description
|
||||||
transaction.name,
|
),
|
||||||
transaction.description)
|
]
|
||||||
])
|
)
|
||||||
|
|
||||||
self._balance = transaction.after_balance
|
self._balance = transaction.after_balance
|
||||||
self._date = transaction.datetime
|
self._date = transaction.datetime
|
||||||
|
|
||||||
|
|
||||||
def release(self):
|
def release(self):
|
||||||
if not self.file.closed \
|
if (
|
||||||
and self._written_starting_balance \
|
not self.file.closed
|
||||||
and not self._written_ending_balance:
|
and self._written_starting_balance
|
||||||
|
and not self._written_ending_balance
|
||||||
|
):
|
||||||
self._write_ending_balance()
|
self._write_ending_balance()
|
||||||
|
|
||||||
if not self.file.closed:
|
if not self.file.closed:
|
||||||
self.file.close()
|
self.file.close()
|
||||||
|
|
||||||
|
|
||||||
def _write_header(self):
|
def _write_header(self):
|
||||||
self.file.write(
|
self.file.write(Mt940.make_header(BANK_BIC))
|
||||||
Mt940.make_header(BANK_BIC))
|
self.file.writelines(
|
||||||
self.file.writelines([
|
[
|
||||||
Mt940.make_20(BANK_NAME),
|
Mt940.make_20(BANK_NAME, self.range),
|
||||||
Mt940.make_25(self.account_iban, CURRENCY),
|
Mt940.make_25(self.account_iban, CURRENCY),
|
||||||
Mt940.make_28(DEFAULT_SEQUENCE_NO)
|
Mt940.make_28(self.range),
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_starting_balance(self, balance):
|
||||||
def _write_starting_balance(self, date, balance):
|
self.file.write(Mt940.make_60f(self.date_start, balance, CURRENCY))
|
||||||
self.file.write(
|
|
||||||
Mt940.make_60f(date, balance, CURRENCY))
|
|
||||||
self._written_starting_balance = True
|
self._written_starting_balance = True
|
||||||
|
|
||||||
|
|
||||||
def _write_ending_balance(self):
|
def _write_ending_balance(self):
|
||||||
self.file.write(
|
self.file.write(Mt940.make_62f(self._date, self._balance, CURRENCY))
|
||||||
Mt940.make_62f(self._date, self._balance, CURRENCY))
|
|
||||||
self._written_ending_balance = True
|
self._written_ending_balance = True
|
||||||
|
|
||||||
|
|
||||||
|
CURRENCY = "PLN"
|
||||||
CURRENCY = 'EUR'
|
|
||||||
|
|
||||||
# format identifier
|
# format identifier
|
||||||
TAG_940 = '940'
|
TAG_940 = "940"
|
||||||
|
|
||||||
# header
|
# header
|
||||||
FORMAT_HEADER = \
|
FORMAT_HEADER = "{bic}\n" + TAG_940 + "\n" + "{bic}\n"
|
||||||
'{bic}\n' + \
|
|
||||||
TAG_940 + '\n' + \
|
|
||||||
'{bic}\n'
|
|
||||||
|
|
||||||
# transaction ref
|
# transaction ref
|
||||||
FORMAT_20 = ':20:{bank}\n'
|
FORMAT_20 = ":20:{bank}{range}\n"
|
||||||
|
|
||||||
# account id
|
# account id
|
||||||
FORMAT_25 = ':25:{iban} {currency}\n'
|
FORMAT_25 = ":25:{iban} {currency}\n"
|
||||||
|
|
||||||
# sequence no
|
# sequence no
|
||||||
FORMAT_28 = ':28:{seqno}\n'
|
FORMAT_28 = ":28:{seqno}\n"
|
||||||
|
|
||||||
# opening balance
|
# opening balance
|
||||||
FORMAT_60F = ':60F:{sign}{date}{currency}{amount}\n'
|
FORMAT_60F = ":60F:{sign}{date}{currency}{amount}\n"
|
||||||
|
|
||||||
# closing balance
|
# closing balance
|
||||||
FORMAT_62F = ':62F:{sign}{date}{currency}{amount}\n'
|
FORMAT_62F = ":62F:{sign}{date}{currency}{amount}\n"
|
||||||
|
|
||||||
# transaction
|
# transaction
|
||||||
FORMAT_61 = ':61:{date}{date2}{amount}{magic}\n'
|
FORMAT_61 = ":61:{date}{date2}{amount}{magic}\n"
|
||||||
|
|
||||||
# transaction 2
|
# transaction 2
|
||||||
FORMAT_86 = ':86:/IBAN/{iban}/NAME/{name}/REMI/{description}\n'
|
FORMAT_86 = ":86:/IBAN/{iban}/NAME/{name}/REMI/{description}\n"
|
||||||
|
|
||||||
MAGIC = 'NTRFNONREF'
|
MAGIC = "NTRFNONREF"
|
||||||
|
|
||||||
|
|
||||||
class Mt940:
|
class Mt940:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_header(bic):
|
def make_header(bic):
|
||||||
return FORMAT_HEADER.format(
|
return FORMAT_HEADER.format(bic=bic)
|
||||||
bic=bic)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_20(bank):
|
def make_20(bank, range):
|
||||||
return FORMAT_20.format(
|
return FORMAT_20.format(bank=bank, range=range)
|
||||||
bank=bank)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_25(iban, currency):
|
def make_25(iban, currency):
|
||||||
return FORMAT_25.format(
|
return FORMAT_25.format(iban=iban, currency=currency)
|
||||||
iban=iban,
|
|
||||||
currency=currency)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_28(seqno):
|
def make_28(seqno):
|
||||||
return FORMAT_28.format(
|
return FORMAT_28.format(seqno=Mt940.pad_5(seqno))
|
||||||
seqno=Mt940.pad_5(seqno))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_60f(datetime, balance, currency):
|
def make_60f(datetime, balance, currency):
|
||||||
|
@ -144,7 +128,8 @@ class Mt940:
|
||||||
sign=Mt940.amount_sign(balance),
|
sign=Mt940.amount_sign(balance),
|
||||||
date=Mt940.date(datetime),
|
date=Mt940.date(datetime),
|
||||||
currency=currency,
|
currency=currency,
|
||||||
amount= Mt940.amount_val(balance))
|
amount=Mt940.amount_val(balance),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_62f(datetime, balance, currency):
|
def make_62f(datetime, balance, currency):
|
||||||
|
@ -152,7 +137,8 @@ class Mt940:
|
||||||
sign=Mt940.amount_sign(balance),
|
sign=Mt940.amount_sign(balance),
|
||||||
date=Mt940.date(datetime),
|
date=Mt940.date(datetime),
|
||||||
currency=currency,
|
currency=currency,
|
||||||
amount= Mt940.amount_val(balance))
|
amount=Mt940.amount_val(balance),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_61(datetime, amount):
|
def make_61(datetime, amount):
|
||||||
|
@ -160,14 +146,12 @@ class Mt940:
|
||||||
date=Mt940.date(datetime),
|
date=Mt940.date(datetime),
|
||||||
date2=Mt940.date(datetime, with_year=False),
|
date2=Mt940.date(datetime, with_year=False),
|
||||||
amount=Mt940.amount(amount),
|
amount=Mt940.amount(amount),
|
||||||
magic=MAGIC)
|
magic=MAGIC,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_86(iban, name, description):
|
def make_86(iban, name, description):
|
||||||
return FORMAT_86.format(
|
return FORMAT_86.format(iban=iban, name=name, description=description)
|
||||||
iban=iban,
|
|
||||||
name=name,
|
|
||||||
description=description)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pad_5(val):
|
def pad_5(val):
|
||||||
|
@ -175,11 +159,11 @@ class Mt940:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def amount_sign(val):
|
def amount_sign(val):
|
||||||
return 'C' if val > 0 else 'D'
|
return "C" if val > 0 else "D"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def amount_val(val):
|
def amount_val(val):
|
||||||
return '{0:.2f}'.format(abs(val)).replace('.', ',')
|
return "{0:.2f}".format(abs(val)).replace(".", ",")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def amount(val):
|
def amount(val):
|
||||||
|
@ -188,6 +172,6 @@ class Mt940:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def date(val, with_year=True):
|
def date(val, with_year=True):
|
||||||
if with_year:
|
if with_year:
|
||||||
return val.strftime('%y%m%d')
|
return val.strftime("%y%m%d")
|
||||||
else:
|
else:
|
||||||
return val.strftime('%m%d')
|
return val.strftime("%m%d")
|
||||||
|
|
127
revolut.py
127
revolut.py
|
@ -1,127 +0,0 @@
|
||||||
import os
|
|
||||||
import string
|
|
||||||
import csv
|
|
||||||
import math
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from data import Transaction
|
|
||||||
|
|
||||||
EXCPECT_HEADERS = [
|
|
||||||
'Date started (UTC)', 'Time started (UTC)', 'Date completed (UTC)',
|
|
||||||
'Time completed (UTC)', 'State', 'Type', 'Description', 'Reference',
|
|
||||||
'Payer', 'Card name', 'Card number', 'Orig currency', 'Orig amount',
|
|
||||||
'Payment currency', 'Amount', 'Fee', 'Balance', 'Account',
|
|
||||||
'Beneficiary account number', 'Beneficiary sort code or routing number',
|
|
||||||
'Beneficiary IBAN', 'Beneficiary BIC'
|
|
||||||
]
|
|
||||||
|
|
||||||
NAME_REMOVE_PREFIXES = [
|
|
||||||
'Payment from ',
|
|
||||||
'To '
|
|
||||||
]
|
|
||||||
|
|
||||||
DATE_FORMAT = '%Y-%m-%d'
|
|
||||||
TIME_FORMAT = '%H:%M:%S'
|
|
||||||
DATETIME_FORMAT = DATE_FORMAT + TIME_FORMAT
|
|
||||||
|
|
||||||
FEE_NAME = 'Revolut'
|
|
||||||
FEE_IBAN = ''
|
|
||||||
FEE_DESCRIPTION_FORMAT = 'Bank transaction fee {}'
|
|
||||||
FEE_DATETIME_DELTA = timedelta(seconds=1)
|
|
||||||
|
|
||||||
|
|
||||||
class RevolutCsvReader:
|
|
||||||
|
|
||||||
def __init__(self, filename):
|
|
||||||
if not os.path.isfile(filename):
|
|
||||||
raise ValueError('File does not exist: {}'.format(filename))
|
|
||||||
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
self.file = open(self.filename, 'r')
|
|
||||||
self.reader = csv.reader(self.file)
|
|
||||||
|
|
||||||
self._validate()
|
|
||||||
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
if not self.file.closed:
|
|
||||||
self.file.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _validate(self):
|
|
||||||
def _santize_header(header):
|
|
||||||
header = ''.join([c for c in header
|
|
||||||
if c in string.printable])
|
|
||||||
header = header.strip()
|
|
||||||
return header
|
|
||||||
|
|
||||||
headers = [_santize_header(h) for h in next(self.reader)]
|
|
||||||
if headers != EXCPECT_HEADERS:
|
|
||||||
raise ValueError('Headers do not match expected Revolut CSV format.')
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_transactions(self):
|
|
||||||
transactions = []
|
|
||||||
for row in self.reader:
|
|
||||||
transactions = self._parse_transaction(row) + transactions
|
|
||||||
|
|
||||||
return transactions
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_transaction(self, row):
|
|
||||||
|
|
||||||
def _santize_name(name_):
|
|
||||||
for remove_prefix in NAME_REMOVE_PREFIXES:
|
|
||||||
if name_.startswith(remove_prefix):
|
|
||||||
name_ = name_[len(remove_prefix):]
|
|
||||||
|
|
||||||
return name_
|
|
||||||
|
|
||||||
def _parse_datetime(date_str, time_str):
|
|
||||||
return datetime.strptime(date_str + time_str, DATETIME_FORMAT)
|
|
||||||
|
|
||||||
|
|
||||||
_0, _1, completed_date_str, completed_time_str, _4, _5, name, description, _8, _9, _10, \
|
|
||||||
_11, _12, _13, amount_str, fee_str, balance_str, _17, _18, _19, iban, _21 \
|
|
||||||
= row
|
|
||||||
|
|
||||||
completed_datetime = _parse_datetime(completed_date_str, completed_time_str)
|
|
||||||
amount, fee, balance = \
|
|
||||||
float(amount_str), float(fee_str), float(balance_str)
|
|
||||||
|
|
||||||
transaction_without_fee = Transaction(
|
|
||||||
amount=amount,
|
|
||||||
name=_santize_name(name),
|
|
||||||
iban=iban,
|
|
||||||
description=description,
|
|
||||||
datetime=completed_datetime,
|
|
||||||
before_balance=balance - amount - fee,
|
|
||||||
after_balance=balance - fee)
|
|
||||||
|
|
||||||
batch = [transaction_without_fee]
|
|
||||||
|
|
||||||
if not math.isclose(fee, 0.00):
|
|
||||||
fee_transaction = self._make_fee_transaction(
|
|
||||||
completed_datetime,
|
|
||||||
balance,
|
|
||||||
fee)
|
|
||||||
|
|
||||||
batch.append(fee_transaction)
|
|
||||||
|
|
||||||
return batch
|
|
||||||
|
|
||||||
|
|
||||||
def _make_fee_transaction(self, completed_datetime, balance, fee):
|
|
||||||
return Transaction(
|
|
||||||
amount=fee,
|
|
||||||
name=FEE_NAME,
|
|
||||||
iban=FEE_IBAN,
|
|
||||||
# include timestamp of transaction to make sure that SnelStart
|
|
||||||
# does not detect similar transactions as the same one
|
|
||||||
description=FEE_DESCRIPTION_FORMAT.format(int(completed_datetime.timestamp())),
|
|
||||||
datetime=completed_datetime + FEE_DATETIME_DELTA,
|
|
||||||
before_balance=balance - fee,
|
|
||||||
after_balance=balance)
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user