"""CRUD для услуг и временных интервалов обязательств. Денежные поля принимаются как десятичные строки. SQLite здесь отвечает за хранение и фильтрацию дат, а арифметика остаётся в сервисном слое на Decimal. """ import uuid from db.connection import execute, query, transaction def insert_service_item(dossier_id, service_family, canonical_name, instance_anchor="", catalog_sku=None, sku_status="UNKNOWN"): """Создать устойчивую идентичность услуги в пределах досье.""" service_id = str(uuid.uuid4()) execute( """INSERT INTO service_items (id, dossier_id, service_family, instance_anchor, canonical_name, catalog_sku, sku_status) VALUES (%s, %s, %s, %s, %s, %s, %s)""", (service_id, dossier_id, service_family, instance_anchor or "", canonical_name, catalog_sku, sku_status), ) return get_service_item(service_id) def get_service_item(service_item_id): """Получить услугу по ID либо вернуть `None`.""" rows = query("SELECT * FROM service_items WHERE id = %s", (service_item_id,)) return rows[0] if rows else None def insert_interval(data): """Создать интервал, сохраняя точные цены/количества строками. `data` должен содержать dossier_id, service_item_id и raw_name; остальные поля берутся из значений по умолчанию схемы либо передаются явно. """ interval_id = str(uuid.uuid4()) fields = ( "dossier_id", "service_item_id", "charge_type", "valid_from", "valid_to", "date_status", "date_basis", "raw_name", "config_params_json", "price", "qty", "sum", "vat_mode", "source_document_id", "closed_by_document_id", "table_ref", "quote", "status", ) values = [data[field] for field in fields if field in data] selected_fields = [field for field in fields if field in data] columns = ["id", *selected_fields] placeholders = ", ".join(["%s"] * len(columns)) execute( f"INSERT INTO timeline_intervals ({', '.join(columns)}) VALUES ({placeholders})", [interval_id, *values], ) rows = query("SELECT * FROM timeline_intervals WHERE id = %s", (interval_id,)) return rows[0] if rows else None def close_interval(interval_id, valid_to, closed_by_document_id=None): """Закрыть интервал полуоткрытой границей и указать закрывший документ.""" return execute( """UPDATE timeline_intervals SET valid_to = %s, closed_by_document_id = %s WHERE id = %s""", (valid_to, closed_by_document_id, interval_id), ) def list_timeline(dossier_id, as_of_date=None): """Получить историю либо активный на дату срез по досье. Для среза используется единая полуоткрытая семантика: начало включено, окончание исключено. NULL-дата начала не считается подтверждённо активной. """ if as_of_date: return query( """SELECT * FROM timeline_intervals WHERE dossier_id = %s AND valid_from <= %s AND (valid_to IS NULL OR valid_to > %s) ORDER BY valid_from, service_item_id, id""", (dossier_id, as_of_date, as_of_date), ) return query( """SELECT * FROM timeline_intervals WHERE dossier_id = %s ORDER BY valid_from, service_item_id, id""", (dossier_id,), ) def reset_dossier_timeline(dossier_id): """Сбросить вычисленный таймлайн и открытые исключения сопоставления. Решённые исключения сохраняются как аудит пользовательского решения; нерешённые lineage-исключения пересоздаются вместе с новым построением. """ with transaction(): execute( """DELETE FROM exceptions_queue WHERE dossier_id = %s AND stage = 'lineage_matching' AND status = 'OPEN'""", (dossier_id,), ) return execute("DELETE FROM service_items WHERE dossier_id = %s", (dossier_id,))