Files
contracts-app/site/app.py
T
naeel 9899ede8c6 fix(app): полная перезапись — все шаги пайплайна + ZIP-обработка
- ZIP: elements объединяются из всех файлов
- LLM: extractor.extract(text)
- diff: differ.diff(prev, rows)
- spec_rows + spec_history
- Без дубликатов, без replace_string_in_file
2026-06-15 07:29:47 +04:00

183 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()