Implement OFX 2.x import

This commit is contained in:
RunasSudo 2023-01-04 18:37:05 +11:00
parent 89791a8ba0
commit 13123deebe
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 87 additions and 3 deletions

View File

@ -24,11 +24,11 @@ from io import StringIO
def import_ofx1(file): def import_ofx1(file):
raw_ofx = file.read().decode('utf-8') raw_ofx = file.read().decode('utf-8')
# Convert OFX header to XML and parse # Convert OFX header to SGML and parse
raw_payload = raw_ofx[raw_ofx.index('<OFX>'):] raw_payload = raw_ofx[raw_ofx.index('<OFX>'):]
xml_input = StringIO(raw_payload.replace('&', '&amp;')) sgml_input = StringIO(raw_payload.replace('&', '&amp;'))
try: try:
tree = ET.parse(xml_input, ET.HTMLParser()) tree = ET.parse(sgml_input, ET.HTMLParser())
except Exception as ex: except Exception as ex:
raise ex raise ex
root = tree.getroot() root = tree.getroot()

View File

@ -0,0 +1,61 @@
# DrCr: Web-based double-entry bookkeeping framework
# Copyright (C) 2022–2023 Lee Yingtong Li (RunasSudo)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from ..models import StatementLine
from datetime import datetime
from io import StringIO
import xml.etree.ElementTree as ET
def import_ofx2(file):
raw_ofx = file.read().decode('utf-8')
# Convert OFX header to XML and parse
xml_header = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
raw_payload = raw_ofx[raw_ofx.index('?>')+2:]
xml_input = StringIO(xml_header + raw_payload.replace('&', '&amp;'))
try:
tree = ET.parse(xml_input)
except Exception as ex:
raise ex
root = tree.getroot()
# Read transactions
lines = [] # Do first pass to catch "extra description lines"
for transaction in root.find('BANKMSGSRSV1').find('STMTTRNRS').find('STMTRS').find('BANKTRANLIST').findall('STMTTRN'):
date = transaction.find('DTPOSTED').text
date = date[0:4] + '-' + date[4:6] + '-' + date[6:8]
description = transaction.find('NAME').text
amount = transaction.find('TRNAMT').text
if amount == '0':
lines[-1][3].append(description)
continue
lines.append([date, description, amount, []])
imported_statement_lines = []
# Import
for date, description, amount, notes in lines:
imported_statement_lines.append(StatementLine(
dt=datetime.strptime(date, '%Y-%m-%d'),
description=description,
quantity=round(float(amount)*100),
commodity='$'
))
return imported_statement_lines

View File

@ -102,6 +102,9 @@ def statement_lines_import():
if request.form['format'] == 'ofx1': if request.form['format'] == 'ofx1':
from .importers.ofx1 import import_ofx1 from .importers.ofx1 import import_ofx1
statement_lines = import_ofx1(request.files['file']) statement_lines = import_ofx1(request.files['file'])
elif request.form['format'] == 'ofx2':
from .importers.ofx2 import import_ofx2
statement_lines = import_ofx2(request.files['file'])
else: else:
abort(400) abort(400)

View File

@ -40,4 +40,24 @@
</div> </div>
</div> </div>
</form> </form>
<h2 class="h3 mt-4">OFX 2.x</h2>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="format" value="ofx2">
<div class="d-flex">
<div class="flex-grow-1 me-2">
<input class="form-control" name="source-account" placeholder="Source account">
</div>
<div class="flex-grow-1">
<input class="form-control" type="file" name="file" accept=".ofx">
</div>
<div>
<button type="submit" name="action" value="preview" class="btn btn-secondary ms-2">Preview</button>
</div>
<div>
<button type="submit" name="action" value="import" class="btn btn-primary ms-2">Import</button>
</div>
</div>
</form>
{% endblock %} {% endblock %}