06/01 Implement PDF statement file import
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
import logging
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from flask_login import login_required
|
||||
from app.models.account import Account
|
||||
from app.models.category import Category
|
||||
from app.services.bank_import_service import parse_file, do_import
|
||||
|
||||
bank_import_bp = Blueprint('bank_import', __name__, url_prefix='/bank-import')
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_EXTENSIONS = {'csv', 'ofx', 'qfx', 'txt', 'pdf'}
|
||||
|
||||
|
||||
@bank_import_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
|
||||
categories = Category.query.filter_by(is_active=True).order_by(Category.name).all()
|
||||
return render_template('bank_import/index.html',
|
||||
accounts=accounts,
|
||||
categories=categories)
|
||||
|
||||
|
||||
@bank_import_bp.route('/parse', methods=['POST'])
|
||||
@login_required
|
||||
def parse():
|
||||
"""
|
||||
Accepts a multipart/form-data POST with:
|
||||
file — the bank statement file
|
||||
col_map — JSON string (optional); user-supplied column mapping
|
||||
|
||||
Returns JSON with parsed rows, detected format, errors, or needs_mapping flag.
|
||||
"""
|
||||
f = request.files.get('file')
|
||||
if not f or not f.filename:
|
||||
return jsonify({'error': 'No file uploaded'}), 400
|
||||
|
||||
ext = f.filename.rsplit('.', 1)[-1].lower() if '.' in f.filename else ''
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
return jsonify({'error': f'Unsupported file type: .{ext}. Use CSV, OFX, or QFX.'}), 400
|
||||
|
||||
col_map = None
|
||||
raw_map = request.form.get('col_map', '')
|
||||
if raw_map:
|
||||
try:
|
||||
col_map = json.loads(raw_map)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': 'Invalid col_map JSON'}), 400
|
||||
|
||||
file_bytes = f.read()
|
||||
log.info('[bank_import] parse request: filename=%s size=%d bytes', f.filename, len(file_bytes))
|
||||
|
||||
try:
|
||||
result = parse_file(file_bytes, f.filename, col_map=col_map)
|
||||
except Exception as e:
|
||||
log.error('[bank_import] parse failed: %s', e, exc_info=True)
|
||||
return jsonify({'error': f'Failed to parse file: {e}'}), 500
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bank_import_bp.route('/import', methods=['POST'])
|
||||
@login_required
|
||||
def confirm_import():
|
||||
"""
|
||||
Accepts JSON:
|
||||
{
|
||||
"account_id": int,
|
||||
"skip_dupes": bool,
|
||||
"rows": [
|
||||
{"date": "YYYY-MM-DD", "transaction_type": "expense|income",
|
||||
"amount": float, "description": str, "category_id": int|null,
|
||||
"notes": str, "source_id": str|null},
|
||||
...
|
||||
]
|
||||
}
|
||||
Returns {"imported": N, "skipped": N}.
|
||||
"""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'error': 'No JSON payload'}), 400
|
||||
|
||||
rows = data.get('rows', [])
|
||||
account_id = data.get('account_id')
|
||||
skip_dupes = data.get('skip_dupes', True)
|
||||
|
||||
if not rows:
|
||||
return jsonify({'error': 'No rows provided'}), 400
|
||||
if not account_id:
|
||||
return jsonify({'error': 'account_id is required'}), 400
|
||||
|
||||
# Validate account exists
|
||||
acct = Account.query.get(int(account_id))
|
||||
if not acct:
|
||||
return jsonify({'error': f'Account {account_id} not found'}), 400
|
||||
|
||||
log.info('[bank_import] importing %d rows into account_id=%s skip_dupes=%s',
|
||||
len(rows), account_id, skip_dupes)
|
||||
try:
|
||||
imported, skipped = do_import(rows, int(account_id), skip_dupes=skip_dupes)
|
||||
except Exception as e:
|
||||
log.error('[bank_import] import failed: %s', e, exc_info=True)
|
||||
return jsonify({'error': f'Import failed: {e}'}), 500
|
||||
|
||||
return jsonify({'imported': imported, 'skipped': skipped})
|
||||
Reference in New Issue
Block a user