diff --git a/drcr/statements/__init__.py b/drcr/statements/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/drcr/statements/importers/__init__.py b/drcr/statements/importers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/drcr/statements/importers/ofx1.py b/drcr/statements/importers/ofx1.py
new file mode 100644
index 0000000..7c79d3a
--- /dev/null
+++ b/drcr/statements/importers/ofx1.py
@@ -0,0 +1,57 @@
+# 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 .
+
+import lxml.etree as ET
+
+from ..models import StatementLine
+
+from datetime import datetime
+from io import StringIO
+
+def import_ofx1(file):
+ raw_ofx = file.read().decode('utf-8')
+
+ # Convert OFX header to XML and parse
+ raw_payload = raw_ofx[raw_ofx.index(''):]
+ xml_input = StringIO(raw_payload.replace('&', '&'))
+ try:
+ tree = ET.parse(xml_input, ET.HTMLParser())
+ 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('.//banktranlist').findall('.//stmttrn'):
+ date = transaction.find('.//dtposted').text
+ date = date[0:4] + '-' + date[4:6] + '-' + date[6:8]
+ description = transaction.find('.//memo').text.strip()
+ amount = transaction.find('.//trnamt').text.strip()
+
+ 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
diff --git a/drcr/statements/views.py b/drcr/statements/views.py
index fbfce33..50d1a35 100644
--- a/drcr/statements/views.py
+++ b/drcr/statements/views.py
@@ -82,3 +82,29 @@ def statement_line_reconcile_transfer():
db.session.commit()
return redirect('/statement-lines')
+
+@app.route('/statement-lines/import', methods=['GET', 'POST'])
+def statement_lines_import():
+ if request.method == 'GET':
+ return render_template('statements/import.html')
+
+ # Import using importer
+ if request.form['format'] == 'ofx1':
+ from .importers.ofx1 import import_ofx1
+ statement_lines = import_ofx1(request.files['file'])
+ else:
+ abort(400)
+
+ # Fill in source_account
+ for statement_line in statement_lines:
+ statement_line.source_account = request.form['source-account']
+
+ if request.form['action'] == 'preview':
+ return render_template('statements/import_preview.html', statement_lines=statement_lines)
+
+ # Add to database
+ for statement_line in statement_lines:
+ db.session.add(statement_line)
+ db.session.commit()
+
+ return redirect('/statement-lines')
diff --git a/drcr/templates/base.html b/drcr/templates/base.html
index 3720aa4..541be5b 100644
--- a/drcr/templates/base.html
+++ b/drcr/templates/base.html
@@ -49,6 +49,7 @@
{% endblock %}
+ {##}
{% block scripts %}{% endblock %}