Files
contracts-app/site/app.py
T
naeel 7095d1201f fix: methods=[GET,POST], красная корзина, детект дубликатов
- / принимает GET и POST (было только GET → 405 Method Not Allowed)
- Кнопка удаления: красная иконка trash-2
- Дубликаты: предупреждение + пометка в очереди
2026-06-15 07:15:29 +04:00

201 lines
7.9 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 — Точка входа и сборка слоёв.
Бизнес-логики НОЛЬ. Только:
1. Инициализация схемы БД (schema.ensure_schema)
2. Регистрация Blueprint-ов (test, api — будут добавляться)
Правило: если в app.py появляется if/for или бизнес-слово — это лишнее.
"""
from dotenv import load_dotenv
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
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="Нет файлов")
# Создать договор
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)
# ── Точка входа ─────────────────────────────────────────────────
if __name__ == "__main__":
app_instance = ContractsApp()
app_instance.run()
app_instance = ContractsApp()
app_instance.run()