"""app.py — Точка входа и сборка слоёв. Бизнес-логики НОЛЬ.""" from dotenv import load_dotenv from flask import Flask, render_template, request import json 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 load_dotenv() class ContractsApp: def __init__(self): self.app = Flask(__name__) schema.ensure_schema() self._add_routes() def _add_routes(self): self.app.add_url_rule("/", "index", self._upload_page, methods=["GET", "POST"]) self.app.add_url_rule("/health", "health", self._health) self.app.register_blueprint(test_bp) self.app.register_blueprint(upload_bp) self.app.register_blueprint(api_bp) def _health(self): return "OK", 200, {"Content-Type": "text/plain"} def _upload_page(self): 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) # 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(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) # Сохранить 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)", (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_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_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) " "VALUES (%s,%s,%s,%s,%s,%s)", (str(contract_id), str(supp_id), ch["row_num"], ch["change_type"], 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_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_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): self.app.run(host="0.0.0.0", port=5000) if __name__ == "__main__": ContractsApp().run()