diff --git a/site/app.py b/site/app.py index d3f9d5f..9ba99b1 100644 --- a/site/app.py +++ b/site/app.py @@ -1,15 +1,8 @@ -""" -app.py — Точка входа и сборка слоёв. - -Бизнес-логики НОЛЬ. Только: - 1. Инициализация схемы БД (schema.ensure_schema) - 2. Регистрация Blueprint-ов (test, api — будут добавляться) - -Правило: если в app.py появляется if/for или бизнес-слово — это лишнее. -""" +"""app.py — Точка входа и сборка слоёв. Бизнес-логики НОЛЬ.""" from dotenv import load_dotenv from flask import Flask, render_template, request +import json import schema import db @@ -26,44 +19,27 @@ load_dotenv() class ContractsApp: - """ - Главный класс приложения. - Собирает все слои вместе, не содержит логики. - """ - def __init__(self): self.app = Flask(__name__) - - # 1. Создать таблицы при старте (если их ещё нет) schema.ensure_schema() - - # 2. Собрать маршруты self._add_routes() def _add_routes(self): - """Регистрация всех Blueprint-ов и системных маршрутов.""" - - # -- Системные маршруты -- self.app.add_url_rule("/", "index", self._upload_page, methods=["GET", "POST"]) self.app.add_url_rule("/health", "health", self._health) - - # -- Blueprint-ы (каждый слой — отдельный Blueprint) -- self.app.register_blueprint(test_bp) self.app.register_blueprint(upload_bp) self.app.register_blueprint(api_bp) def _health(self): - """Health check для платформы.""" return "OK", 200, {"Content-Type": "text/plain"} def _upload_page(self): - """Страница загрузки: 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="Нет файлов") @@ -72,7 +48,6 @@ class ContractsApp: 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", @@ -89,14 +64,13 @@ class ContractsApp: 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')", + "INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s,%s,%s,'uploaded')", (filename, mime, file_bytes), ) doc, _ = db.query_one( @@ -104,23 +78,39 @@ class ContractsApp: (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"])}) + + # ZIP: объединяем элементы из всех файлов + if mime == "application/zip" and "files" in pr: + all_el = [] + zip_errs = [] + for zf in pr["files"]: + if zf.get("errors"): + zip_errs.extend(zf["errors"]) + all_el.extend(zf.get("elements", [])) + if zip_errs and not all_el: + results.append({"name": filename, "ok": False, "error": "; ".join(zip_errs)}) + continue + elements = all_el + else: + elements = pr.get("elements", []) + + if not elements: + errs = pr.get("errors", []) + results.append({"name": filename, "ok": False, "error": "; ".join(errs) if errs else "нет таблиц"}) continue - text = textify.to_text(pr.get("elements", [])) + text = textify.to_text(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')", + "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')", (str(contract_id), str(doc_id)), ) supp, _ = db.query_one( @@ -128,7 +118,6 @@ class ContractsApp: (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 @@ -142,59 +131,52 @@ class ContractsApp: rows = ext.get("rows", []) total_rows += len(rows) - # Сохранить строки + # Сохранить spec_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)", + "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 " + prev_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 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 [] + prev_rows = [dict(zip(prev_res["columns"], r)) for r in prev_res["rows"]] if prev_res 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) " + "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), + json.dumps(ch.get("old_values"), ensure_ascii=False, default=str) if ch.get("old_values") else None, + 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 " + 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", (str(contract_id),), ) - if rows_result: - all_rows = [dict(zip(rows_result["columns"], r)) for r in rows_result["rows"]] + if rows_res: + all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["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) -# ── Точка входа ───────────────────────────────────────────────── - if __name__ == "__main__": - app_instance = ContractsApp() - app_instance.run() - app_instance = ContractsApp() - app_instance.run() + ContractsApp().run()