"""routes/main.py — Главные маршруты: загрузка файлов, просмотр договора.""" import datetime from flask import render_template, request, jsonify import db import mimeutil def _save_file_to_db(filename, file_bytes, mime, contract_id, conn=None): """ Сохранить загруженный файл в БД. Создаёт запись в documents и привязывает к договору через supplements. Если conn передан — используется одно соединение для всей транзакции. Иначе — каждый запрос в своём соединении. Возвращает doc_id или None при ошибке. """ if conn: # Транзакционный режим: все операции в одном соединении cur = conn.cursor() cur.execute( "INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s,%s,%s,'uploaded')", (filename, mime, file_bytes), ) cur.execute( "SELECT id FROM documents WHERE filename=%s AND status='uploaded' ORDER BY created_at DESC LIMIT 1", (filename,), ) row = cur.fetchone() doc_id = row[0] if row else None if doc_id: cur.execute( "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')", (str(contract_id), str(doc_id)), ) conn.commit() cur.close() return doc_id # Автономный режим: каждый вызов db.execute сам управляет соединением db.execute( "INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s,%s,%s,'uploaded')", (filename, mime, file_bytes), ) doc, _ = db.query_one( "SELECT id FROM documents WHERE filename=%s AND status='uploaded' ORDER BY created_at DESC LIMIT 1", (filename,), ) doc_id = doc["id"] if doc else None if doc_id: db.execute( "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')", (str(contract_id), str(doc_id)), ) return doc_id def index(): """ GET / — страница загрузки GET /?id= — просмотр договора POST / — приём файлов (multipart/form-data) """ if request.method == "POST": return _upload_files() cid = request.args.get("id") if cid: return _show_contract(cid) return render_template("upload.html") def _upload_files(): """ Принять файлы через multipart/form-data. Поле формы: 'files' (один или несколько). ?cid=X — добавить файлы к существующему договору. Если cid не указан — создаётся новый договор. """ files = request.files.getlist("files") if not files or not any(f.filename for f in files): return jsonify({"error": "Нет файлов"}), 400 cid = request.args.get("cid") if cid: # Добавляем к существующему договору contract_id = cid else: # Создаём новый договор conn, err = db.connect() if err: return jsonify({"error": f"БД: {err}"}), 500 cur = conn.cursor() cur.execute( "INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id", ("б/н " + datetime.datetime.now().strftime("%Y%m%d-%H%M"), ""), ) contract_id = cur.fetchone()[0] conn.commit() cur.close() db.put_conn(conn) # Сохраняем каждый файл for f in files: if not f.filename: continue filename = f.filename mime = mimeutil.guess_mime(filename) or f.content_type or "application/octet-stream" _save_file_to_db(filename, f.read(), mime, str(contract_id)) return jsonify({"contract_id": str(contract_id)}) def _show_contract(cid): """ Показать страницу договора со списком файлов и кнопкой «Парсинг». Загружает: информацию о договоре, список допников/файлов, существующие строки спецификации (если уже извлечены). """ contract, _ = db.query_one( "SELECT id, number, created_at FROM contracts WHERE id = %s", (cid,), ) if not contract: return render_template("upload.html", error="Договор не найден") # Все файлы, привязанные к договору supps, _ = db.query( "SELECT s.id, s.created_at, d.filename, d.status, d.parsed_text IS NOT NULL as has_text " "FROM supplements s JOIN documents d ON s.document_id = d.id " "WHERE s.contract_id = %s ORDER BY s.created_at", (cid,), ) supp_list = [dict(zip(supps["columns"], r)) for r in supps["rows"]] if supps else [] # Уже извлечённые строки спецификации rows_res, _ = db.query( "SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start " "FROM spec_rows sr JOIN supplements s ON sr.supplement_id = s.id " "WHERE s.contract_id = %s ORDER BY sr.row_num", (cid,), ) all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["rows"]] if rows_res else [] # Файлы, готовые к парсингу unprocessed = [s for s in supp_list if s["status"] in ("uploaded", "parsed")] return render_template("upload.html", contract=contract, supplements=supp_list, all_rows=all_rows, unprocessed=unprocessed, )