05/31 Phase 7: receipt ocr
This commit is contained in:
@@ -9,6 +9,7 @@ from app.models.account import Account
|
||||
from app.models.category import Category
|
||||
from app.services.account_service import calc_balance
|
||||
from datetime import date, datetime
|
||||
import os
|
||||
from sqlalchemy import or_
|
||||
|
||||
transactions_bp = Blueprint('transactions', __name__, url_prefix='/transactions')
|
||||
@@ -220,3 +221,106 @@ def transfer():
|
||||
return redirect(url_for('transactions.index'))
|
||||
|
||||
return render_template('transactions/transfer.html', form=form)
|
||||
|
||||
|
||||
@transactions_bp.route('/ocr', methods=['POST'])
|
||||
@login_required
|
||||
def ocr_receipt():
|
||||
"""
|
||||
POST a receipt image, get back extracted transaction data as JSON.
|
||||
Used by both new expense form and edit form.
|
||||
"""
|
||||
from app.services.ocr_service import extract_from_bytes
|
||||
from app.models.category import Category
|
||||
|
||||
if 'receipt' not in request.files:
|
||||
return jsonify({'error': 'No file uploaded'}), 400
|
||||
|
||||
f = request.files['receipt']
|
||||
if not f.filename:
|
||||
return jsonify({'error': 'Empty filename'}), 400
|
||||
|
||||
ext = os.path.splitext(f.filename)[1].lower()
|
||||
allowed = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
if ext not in allowed:
|
||||
return jsonify({'error': f'Unsupported type: {ext}. Use JPG, PNG, GIF, WEBP'}), 400
|
||||
|
||||
# Read bytes — limit 10MB
|
||||
f.seek(0, 2)
|
||||
size = f.tell()
|
||||
f.seek(0)
|
||||
if size > 10 * 1024 * 1024:
|
||||
return jsonify({'error': 'File too large (max 10MB)'}), 400
|
||||
|
||||
mime_map = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp'}
|
||||
mime_type = mime_map.get(ext, 'image/jpeg')
|
||||
image_bytes = f.read()
|
||||
|
||||
result = extract_from_bytes(image_bytes, mime_type)
|
||||
|
||||
if result['error']:
|
||||
return jsonify({'error': result['error']}), 422
|
||||
|
||||
# Look up category ID from suggestion
|
||||
category_id = None
|
||||
if result['category_suggestion']:
|
||||
cat = Category.query.filter(
|
||||
Category.name.ilike(result['category_suggestion']),
|
||||
Category.is_active == True,
|
||||
).first()
|
||||
if cat:
|
||||
category_id = cat.id
|
||||
|
||||
return jsonify({
|
||||
'amount': result['amount'],
|
||||
'date': result['date'],
|
||||
'description': result['merchant'] or result['notes'] or '',
|
||||
'notes': result['notes'],
|
||||
'category_suggestion': result['category_suggestion'],
|
||||
'category_id': category_id,
|
||||
})
|
||||
|
||||
|
||||
@transactions_bp.route('/ocr-file', methods=['POST'])
|
||||
@login_required
|
||||
def ocr_receipt_file():
|
||||
"""
|
||||
Re-extract from an already-uploaded receipt file stored on disk.
|
||||
Body: { "filename": "abc123.jpg" }
|
||||
"""
|
||||
from app.services.ocr_service import extract_from_file
|
||||
from app.models.category import Category
|
||||
from flask import current_app
|
||||
|
||||
data = request.get_json()
|
||||
if not data or not data.get('filename'):
|
||||
return jsonify({'error': 'No filename provided'}), 400
|
||||
|
||||
# Security: only allow basenames, no path traversal
|
||||
filename = os.path.basename(data['filename'])
|
||||
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/home/pfm/app/uploads')
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
result = extract_from_file(file_path)
|
||||
|
||||
if result['error']:
|
||||
return jsonify({'error': result['error']}), 422
|
||||
|
||||
category_id = None
|
||||
if result['category_suggestion']:
|
||||
cat = Category.query.filter(
|
||||
Category.name.ilike(result['category_suggestion']),
|
||||
Category.is_active == True,
|
||||
).first()
|
||||
if cat:
|
||||
category_id = cat.id
|
||||
|
||||
return jsonify({
|
||||
'amount': result['amount'],
|
||||
'date': result['date'],
|
||||
'description': result['merchant'] or result['notes'] or '',
|
||||
'notes': result['notes'],
|
||||
'category_suggestion': result['category_suggestion'],
|
||||
'category_id': category_id,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user