"""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,))