feat: add v3 extraction storage and structured parsing

This commit is contained in:
Repinoid
2026-10-31 09:35:44 +03:00
parent 1878649317
commit 0a318aa006
6 changed files with 669 additions and 23 deletions
+113
View File
@@ -0,0 +1,113 @@
"""CRUD для досье договоров и доказанных связей с документами.
Досье идентифицируется внутренним UUID, а номер договора и его алиасы
сохраняются как изменяемые утверждения. Поэтому опечатки в документах
не становятся ключом базы и могут быть пересмотрены без переноса данных.
"""
import json
import uuid
from db.connection import execute, query, transaction
def _json_value(value, default):
"""Кодировать поля-списки, принимая уже сериализованный JSON без повтора."""
if value is None:
value = default
return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
def _decode_dossier(row):
"""Добавить к строке досье Python-представление списка алиасов."""
if not row:
return None
result = dict(row)
try:
result["alias_numbers"] = json.loads(result.get("alias_numbers_json") or "[]")
except (TypeError, json.JSONDecodeError):
result["alias_numbers"] = []
return result
def create_dossier(batch_id, canonical_number, alias_numbers=None,
counterparty_name="", counterparty_inn="",
has_frame_contract=False, status="ACTIVE"):
"""Создать досье и вернуть его запись со стабильным внутренним ID."""
dossier_id = str(uuid.uuid4())
execute(
"""INSERT INTO contract_dossiers
(id, batch_id, canonical_number, alias_numbers_json,
counterparty_name, counterparty_inn, has_frame_contract, status)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""",
(dossier_id, batch_id, canonical_number,
_json_value(alias_numbers, []), counterparty_name or "",
counterparty_inn or "", int(bool(has_frame_contract)), status),
)
return get_dossier(dossier_id)
def link_document(dossier_id, document_id, role="supplement",
link_status="CONFIRMED", link_reason="", seq_order=0):
"""Создать или обновить связь dossier-document идемпотентно.
Повторная сборка групп не должна создавать дубликаты. При уже имеющейся
связи сохраняется её ID, но актуальные роль, статус, основание и порядок
заменяются значениями нового решения группировщика.
"""
existing = query(
"SELECT id FROM dossier_documents WHERE dossier_id = %s AND document_id = %s",
(dossier_id, document_id),
)
if existing:
link_id = existing[0]["id"]
execute(
"""UPDATE dossier_documents
SET role = %s, link_status = %s, link_reason = %s, seq_order = %s
WHERE id = %s""",
(role, link_status, link_reason or "", seq_order, link_id),
)
else:
link_id = str(uuid.uuid4())
execute(
"""INSERT INTO dossier_documents
(id, dossier_id, document_id, role, link_status, link_reason, seq_order)
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
(link_id, dossier_id, document_id, role, link_status,
link_reason or "", seq_order),
)
rows = query("SELECT * FROM dossier_documents WHERE id = %s", (link_id,))
return rows[0] if rows else None
def list_dossiers_by_batch(batch_id):
"""Получить досье batch вместе с упорядоченным списком документов."""
dossiers = query(
"SELECT * FROM contract_dossiers WHERE batch_id = %s ORDER BY created_at, id",
(batch_id,),
)
result = []
for raw_dossier in dossiers:
dossier = _decode_dossier(raw_dossier)
dossier["documents"] = query(
"""SELECT dd.*, d.filename, d.doc_type, d.own_number, d.parent_number,
d.doc_date, d.counterparty, d.zip_source
FROM dossier_documents dd
JOIN documents d ON d.id = dd.document_id
WHERE dd.dossier_id = %s
ORDER BY dd.seq_order, d.created_at, d.id""",
(dossier["id"],),
)
result.append(dossier)
return result
def get_dossier(dossier_id):
"""Получить досье по внутреннему ID, не загружая список документов."""
rows = query("SELECT * FROM contract_dossiers WHERE id = %s", (dossier_id,))
return _decode_dossier(rows[0]) if rows else None
def clear_batch_dossiers(batch_id):
"""Удалить все досье batch; таблицы с ON DELETE CASCADE очистятся вместе с ними."""
with transaction():
return execute("DELETE FROM contract_dossiers WHERE batch_id = %s", (batch_id,))