Files
contracts-flask/site/db/timeline.py
T

143 lines
6.3 KiB
Python

"""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 list_service_items(dossier_id):
"""Получить все идентичности услуг досье в стабильном порядке."""
return query(
"SELECT * FROM service_items WHERE dossier_id = %s ORDER BY created_at, id",
(dossier_id,),
)
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", "vat_rate", "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 update_interval(interval_id, values):
"""Обновить разрешённый набор полей интервала без динамических имён от клиента."""
allowed = {
"valid_from", "valid_to", "date_status", "date_basis", "raw_name",
"config_params_json", "price", "qty", "sum", "vat_mode", "vat_rate", "table_ref",
"quote", "status", "closed_by_document_id", "source_document_id",
}
selected = {field: value for field, value in values.items() if field in allowed}
if not selected:
return 0
assignments = ", ".join(f"{field} = %s" for field in selected)
return execute(
f"UPDATE timeline_intervals SET {assignments} WHERE id = %s",
[*selected.values(), interval_id],
)
def list_open_intervals(dossier_id, service_item_id=None):
"""Вернуть открытые интервалы, опционально отфильтрованные по услуге."""
if service_item_id is None:
return query(
"""SELECT * FROM timeline_intervals
WHERE dossier_id = %s AND valid_to IS NULL
ORDER BY valid_from, service_item_id, id""",
(dossier_id,),
)
return query(
"""SELECT * FROM timeline_intervals
WHERE dossier_id = %s AND service_item_id = %s AND valid_to IS NULL
ORDER BY valid_from, id""",
(dossier_id, service_item_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,))