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 %} diff --git a/drcr/templates/statements/import.html b/drcr/templates/statements/import.html new file mode 100644 index 0000000..d7e231a --- /dev/null +++ b/drcr/templates/statements/import.html @@ -0,0 +1,43 @@ +{# 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 . +#} + +{% extends 'base.html' %} +{% block title %}Import statement{% endblock %} + +{% block content %} +

Import statement

+ +

OFX 1.x

+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+{% endblock %} diff --git a/drcr/templates/statements/import_preview.html b/drcr/templates/statements/import_preview.html new file mode 100644 index 0000000..167a42c --- /dev/null +++ b/drcr/templates/statements/import_preview.html @@ -0,0 +1,48 @@ +{# 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 . +#} + +{% extends 'base.html' %} +{% block title %}Import statement preview{% endblock %} + +{% block content %} +

Import statement preview

+ +

Imported statement lines

+ + + + + + + + + + + + + {% for line in statement_lines %} + + + + + + + + {% endfor %} + +
DateDescriptionDrCrBalance
{{ line.dt.strftime('%Y-%m-%d') }}{{ line.description }}{{ line.amount().format() if line.quantity >= 0 else '' }}{{ (line.amount()|abs).format() if line.quantity < 0 else '' }}{{ line.balance or '' }}
+{% endblock %} diff --git a/drcr/templates/statements/statement_lines.html b/drcr/templates/statements/statement_lines.html index 2ed8270..90119b6 100644 --- a/drcr/templates/statements/statement_lines.html +++ b/drcr/templates/statements/statement_lines.html @@ -22,9 +22,10 @@

Statement lines

- {#
- -
#} +
+ {##} + Import statement +
diff --git a/requirements.txt b/requirements.txt index 8ba627c..1e970cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ Flask==2.2.2 Flask-SQLAlchemy==3.0.2 +# For OFX 1.x import +lxml==4.9.2 + # Dependencies click==8.1.3 greenlet==2.0.1