diff --git a/site/app.py b/site/app.py index b31fec0..28ff433 100644 --- a/site/app.py +++ b/site/app.py @@ -9,9 +9,15 @@ app.py — Точка входа и сборка слоёв. """ from dotenv import load_dotenv -from flask import Flask, render_template +from flask import Flask, render_template, request import schema +import db +import parser as parser_mod +import textify +import extractor +import differ +import mimeutil from test_routes import test_bp from upload import upload_bp from api import api_bp @@ -51,9 +57,135 @@ class ContractsApp: return "OK", 200, {"Content-Type": "text/plain"} def _upload_page(self): - """Страница загрузки договора (UI).""" + """Страница загрузки: GET — форма, POST — обработка.""" + if request.method == "POST": + return self._process_upload() return render_template("upload.html") + def _process_upload(self): + """Обработка загруженных файлов.""" + files = request.files.getlist("files") + if not files or not any(f.filename for f in files): + return render_template("upload.html", error="Нет файлов") + + # Создать договор + conn, err = db.connect() + if err: + return render_template("upload.html", error=f"БД: {err}") + + cur = conn.cursor() + cur.execute( + "INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id", + ("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""), + ) + contract_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() + + results = [] + total_rows = 0 + + 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" + file_bytes = f.read() + + # Сохранить документ + 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 not doc_id: + results.append({"name": filename, "ok": False, "error": "doc save failed"}) + continue + + # Парсинг + pr = parser_mod.parse(file_bytes, mime) + if pr.get("errors"): + results.append({"name": filename, "ok": False, "error": "; ".join(pr["errors"])}) + continue + + text = textify.to_text(pr.get("elements", [])) + db.execute("UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s", (text, str(doc_id))) + + # Допник + db.execute( + "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s, %s, 'initial')", + (str(contract_id), str(doc_id)), + ) + supp, _ = db.query_one( + "SELECT id FROM supplements WHERE document_id=%s ORDER BY created_at DESC LIMIT 1", + (str(doc_id),), + ) + supp_id = supp["id"] if supp else None + + if not supp_id: + results.append({"name": filename, "ok": False, "error": "supplement save failed"}) + continue + + # LLM-извлечение + ext = extractor.extract(text) + if "error" in ext: + results.append({"name": filename, "ok": False, "error": ext["error"]}) + continue + + rows = ext.get("rows", []) + total_rows += len(rows) + + # Сохранить строки + for row in rows: + db.execute( + "INSERT INTO spec_rows (supplement_id, row_num, name, price, qty, sum, date_start) " + "VALUES (%s,%s,%s,%s,%s,%s,%s)", + (str(supp_id), row.get("row_num"), row.get("name"), + row.get("price"), row.get("qty"), row.get("sum"), row.get("date_start")), + ) + + # Diff с предыдущим + prev, _ = 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 AND s.id!=%s ORDER BY sr.row_num", + (str(contract_id), str(supp_id)), + ) + prev_rows = [dict(zip(prev["columns"], r)) for r in prev["rows"]] if prev else [] + diff_r = differ.diff(prev_rows, rows) + + for ch in diff_r.get("changes", []): + db.execute( + "INSERT INTO spec_history (contract_id, supplement_id, row_num, change_type, old_values, new_values) " + "VALUES (%s,%s,%s,%s,%s,%s)", + (str(contract_id), str(supp_id), ch["row_num"], ch["change_type"], + __import__("json").dumps(ch.get("old_values"), ensure_ascii=False, default=str) if ch.get("old_values") else None, + __import__("json").dumps(ch.get("new_values"), ensure_ascii=False, default=str) if ch.get("new_values") else None), + ) + + db.execute("UPDATE documents SET status='extracted' WHERE id=%s", (str(doc_id),)) + results.append({"name": filename, "ok": True, "rows": len(rows), "summary": diff_r.get("summary", {})}) + + # Получить финальные строки для отображения + all_rows = [] + rows_result, _ = 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", + (str(contract_id),), + ) + if rows_result: + all_rows = [dict(zip(rows_result["columns"], r)) for r in rows_result["rows"]] + + return render_template("upload.html", results=results, all_rows=all_rows, total_rows=total_rows) + def run(self): """Запуск Flask (только для разработки, в production — gunicorn).""" self.app.run(host="0.0.0.0", port=5000) diff --git a/site/templates/upload.html b/site/templates/upload.html index 595020e..952b187 100644 --- a/site/templates/upload.html +++ b/site/templates/upload.html @@ -8,34 +8,22 @@
- -