997 lines
51 KiB
Python
997 lines
51 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
1C:Enterprise HTTP Bridge for Dana Medical Website.
|
||
Translates REST API calls from Go backend to 1C MCP Toolkit /api/execute_code.
|
||
Runs in Docker container on 195.95.133.47 and talks to 1C at 10.240.70.70:6003.
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import json
|
||
import hmac
|
||
import urllib.parse
|
||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||
from datetime import datetime, timezone
|
||
import requests
|
||
import psycopg2
|
||
|
||
MCP_URL = os.environ.get("ONEC_MCP_URL", "http://10.240.70.70:6003/api/execute_code")
|
||
PG_HOST = os.environ.get("PG_HOST", "db")
|
||
PG_PORT = int(os.environ.get("PG_PORT", "5432"))
|
||
PG_USER = os.environ.get("PG_USER", "dana")
|
||
PG_DB = os.environ.get("PG_DB", "dana")
|
||
PG_PASS = os.environ.get("POSTGRES_PASSWORD", "")
|
||
|
||
# Токен для вызова 1C MCP (Bearer) и ключ, которым Go-бэкенд авторизуется перед мостом.
|
||
AUTH_TOKEN = os.environ.get("ONEC_MCP_TOKEN", "")
|
||
BRIDGE_API_KEY = os.environ.get("BRIDGE_API_KEY", "")
|
||
if not AUTH_TOKEN:
|
||
sys.exit("ONEC_MCP_TOKEN is not set")
|
||
if not BRIDGE_API_KEY:
|
||
sys.exit("BRIDGE_API_KEY is not set")
|
||
|
||
MAX_BODY = 64 * 1024
|
||
UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||
|
||
|
||
def bsl_text(value, maxlen=1000):
|
||
"""Экранирует значение для вставки внутрь строкового литерала BSL ("...").
|
||
|
||
В 1С кавычка внутри строки удваивается. Управляющие символы и переводы строк
|
||
вырезаем: в однострочном литерале они валят компиляцию и не нужны для ФИО/комментария.
|
||
"""
|
||
v = "".join(c for c in str(value) if c.isprintable())[:maxlen]
|
||
return v.replace('"', '""')
|
||
|
||
|
||
def is_uuid(value):
|
||
return isinstance(value, str) and bool(UUID_RE.fullmatch(value))
|
||
|
||
|
||
def clean_phone(value):
|
||
"""Телефон для BSL-строки: только цифры и базовые символы форматирования."""
|
||
v = re.sub(r"[^\d+\-() ]", "", str(value))[:40]
|
||
return v
|
||
|
||
def get_linked_doctor_ids():
|
||
try:
|
||
conn = psycopg2.connect(
|
||
host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASS, dbname=PG_DB, connect_timeout=3
|
||
)
|
||
cur = conn.cursor()
|
||
cur.execute("SELECT onec_id FROM onec_doctors WHERE doctor_id IS NOT NULL")
|
||
rows = cur.fetchall()
|
||
conn.close()
|
||
ids = [r[0] for r in rows if r[0]]
|
||
if ids:
|
||
return ids
|
||
except Exception as e:
|
||
print(f"[DB Warning] Could not fetch linked doctors from Postgres: {e}", flush=True)
|
||
|
||
# Fallback to known linked doctors
|
||
return [
|
||
"e27d7269-b659-11f1-8958-00155d46cd22",
|
||
"f07031a2-21a6-11f0-8712-00155d46cd22",
|
||
"a748a2aa-8771-11ed-840b-00155d46cd22",
|
||
"fce19e6d-3d52-11ec-adba-00155d46cd1d",
|
||
"137a17ba-0af6-11f0-86f5-00155d46cd22",
|
||
"a748a2a7-8771-11ed-840b-00155d46cd22",
|
||
]
|
||
|
||
def call_1c(code: str):
|
||
try:
|
||
headers = {"Authorization": f"Bearer {AUTH_TOKEN}", "Content-Type": "application/json"}
|
||
resp = requests.post(MCP_URL, json={"code": code}, headers=headers, timeout=30)
|
||
if resp.status_code != 200:
|
||
return None, f"1C MCP returned HTTP {resp.status_code}: {resp.text[:200]}"
|
||
res_json = resp.json()
|
||
if not res_json.get("success", False):
|
||
return None, f"1C execution failed: {res_json.get('error', 'unknown error')}"
|
||
raw_data = res_json.get("data", "")
|
||
if isinstance(raw_data, str):
|
||
try:
|
||
parsed = json.loads(raw_data)
|
||
if isinstance(parsed, str) and (parsed.strip().startswith("{") or parsed.strip().startswith("[")):
|
||
try:
|
||
return json.loads(parsed), None
|
||
except Exception:
|
||
return parsed, None
|
||
return parsed, None
|
||
except Exception:
|
||
pass
|
||
return raw_data, None
|
||
except Exception as e:
|
||
return None, f"Network error connecting to 1C: {e}"
|
||
|
||
class BridgeHandler(BaseHTTPRequestHandler):
|
||
def log_message(self, format, *args):
|
||
sys.stderr.write("%s - - [%s] %s\n" % (self.client_address[0], self.log_date_time_string(), format % args))
|
||
|
||
def _authorized(self):
|
||
"""Мост принимает запросы только от Go-бэкенда: X-Api-Key или Bearer."""
|
||
got = self.headers.get("X-Api-Key", "")
|
||
if not got:
|
||
got = self.headers.get("Authorization", "").removeprefix("Bearer ").strip()
|
||
return bool(got) and hmac.compare_digest(got.encode("utf-8"), BRIDGE_API_KEY.encode("utf-8"))
|
||
|
||
def _require_auth(self):
|
||
if self._authorized():
|
||
return True
|
||
self.send_json(401, {"code": "unauthorized", "message": "Invalid API key"})
|
||
return False
|
||
|
||
def send_json(self, status_code: int, data: any):
|
||
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||
self.send_response(status_code)
|
||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||
self.send_header("Content-Length", str(len(body)))
|
||
self.end_headers()
|
||
self.wfile.write(body)
|
||
|
||
def do_GET(self):
|
||
parsed = urllib.parse.urlparse(self.path)
|
||
path = parsed.path.rstrip("/")
|
||
query = urllib.parse.parse_qs(parsed.query)
|
||
|
||
if path == "/healthz":
|
||
self.send_json(200, {"ok": True})
|
||
return
|
||
if not self._require_auth():
|
||
return
|
||
|
||
if path == "/branches":
|
||
code = """
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст =
|
||
"ВЫБРАТЬ
|
||
| Филиалы.Ссылка КАК Ссылка,
|
||
| Филиалы.Наименование КАК Наименование
|
||
|ИЗ
|
||
| Справочник.Филиалы КАК Филиалы
|
||
|ГДЕ
|
||
| Филиалы.ПометкаУдаления = ЛОЖЬ
|
||
| И (Филиалы.Наименование ПОДОБНО ""%Дана%"" ИЛИ Филиалы.Наименование ПОДОБНО ""%Ляхова 22%"")
|
||
| И НЕ Филиалы.Наименование ПОДОБНО ""%Абилити%""
|
||
| И НЕ Филиалы.Наименование ПОДОБНО ""%Целитель%""
|
||
|УПОРЯДОЧИТЬ ПО
|
||
| Филиалы.Наименование";
|
||
|
||
Таблица = Запрос.Выполнить().Выгрузить();
|
||
Массив = Новый Массив;
|
||
|
||
Для Каждого Стр Из Таблица Цикл
|
||
Элемент = Новый Структура;
|
||
Элемент.Вставить("id", НРег(XMLСтрока(Стр.Ссылка)));
|
||
Элемент.Вставить("name", Стр.Наименование);
|
||
|
||
Адрес = "г. Махачкала, ул. Ляхова";
|
||
Если Найти(ВРег(Стр.Наименование), "22") > 0 Тогда
|
||
Адрес = "г. Махачкала, ул. Ляхова, д. 22";
|
||
ИначеЕсли Найти(ВРег(Стр.Наименование), "7") > 0 Тогда
|
||
Адрес = "г. Махачкала, ул. Ляхова, д. 7";
|
||
ИначеЕсли Найти(ВРег(Стр.Наименование), "ХАСАВЮРТ") > 0 Тогда
|
||
Адрес = "г. Хасавюрт";
|
||
ИначеЕсли Найти(ВРег(Стр.Наименование), "ЦОМ") > 0 ИЛИ Найти(ВРег(Стр.Наименование), "ОСТЕО") > 0 Тогда
|
||
Адрес = "г. Махачкала, ул. Салаватова, д. 53";
|
||
КонецЕсли;
|
||
|
||
Элемент.Вставить("address", Адрес);
|
||
Элемент.Вставить("phone", "+7 (8722) 55-33-11");
|
||
Элемент.Вставить("active", Истина);
|
||
Массив.Добавить(Элемент);
|
||
КонецЦикла;
|
||
|
||
ЗаписьJSON = Новый ЗаписьJSON;
|
||
ЗаписьJSON.УстановитьСтроку();
|
||
ЗаписатьJSON(ЗаписьJSON, Массив);
|
||
Результат = ЗаписьJSON.Закрыть();
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err:
|
||
self.send_json(502, {"code": "1c_error", "message": err})
|
||
else:
|
||
slots = data or []
|
||
while isinstance(slots, str):
|
||
try:
|
||
slots = json.loads(slots)
|
||
except Exception:
|
||
break
|
||
if not isinstance(slots, list):
|
||
slots = []
|
||
self.send_json(200, slots)
|
||
return
|
||
|
||
if path == "/doctors":
|
||
code = """
|
||
Запрос1С = Новый Структура("ПараметрыЗапроса", Новый Соответствие);
|
||
Ответ = ДМ_СайтРасписание.ПолучитьВрачей(Запрос1С);
|
||
Результат = Ответ.ПолучитьТелоКакСтроку();
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err:
|
||
self.send_json(502, {"code": "1c_error", "message": err})
|
||
else:
|
||
doctors = data or []
|
||
while isinstance(doctors, str):
|
||
try:
|
||
doctors = json.loads(doctors)
|
||
except Exception:
|
||
break
|
||
if not isinstance(doctors, list):
|
||
doctors = []
|
||
for d in doctors:
|
||
if isinstance(d, dict):
|
||
try:
|
||
d["kids_from"] = int(d.get("kids_from", 0))
|
||
except (ValueError, TypeError):
|
||
d["kids_from"] = 0
|
||
self.send_json(200, doctors)
|
||
return
|
||
|
||
if path.startswith("/doctors/") and path.endswith("/services"):
|
||
parts = path.split("/")
|
||
doctor_id = parts[2]
|
||
if not is_uuid(doctor_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "doctor_id must be a UUID"})
|
||
return
|
||
code = f"""
|
||
ВрачСсылка = Справочники.Сотрудники.ПолучитьСсылку(Новый УникальныйИдентификатор("{doctor_id}"));
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст =
|
||
"ВЫБРАТЬ ПЕРВЫЕ 100
|
||
| Цены.Номенклатура.Ссылка КАК Ссылка,
|
||
| Цены.Номенклатура.Наименование КАК Наименование,
|
||
| Цены.Цена КАК Цена
|
||
|ИЗ
|
||
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Цены
|
||
|ГДЕ
|
||
| Цены.Номенклатура.ПометкаУдаления = ЛОЖЬ
|
||
| И Цены.Цена > 0
|
||
| И (Цены.бит_Сотрудник = &Врач ИЛИ Цены.бит_Сотрудник = ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка))
|
||
|УПОРЯДОЧИТЬ ПО
|
||
| Цены.Номенклатура.Наименование";
|
||
|
||
Запрос.УстановитьПараметр("Период", ТекущаяДата());
|
||
Запрос.УстановитьПараметр("Врач", ВрачСсылка);
|
||
|
||
Таблица = Запрос.Выполнить().Выгрузить();
|
||
Массив = Новый Массив;
|
||
|
||
Длит = ВрачСсылка.ДлительностьПриема;
|
||
ДлитМин = ?(ТипЗнч(Длит) = Тип("Дата"), Час(Длит) * 60 + Минута(Длит), 30);
|
||
Если ДлитМин < 15 ИЛИ ДлитМин > 60 Тогда ДлитМин = 30; КонецЕсли;
|
||
|
||
Для Каждого Стр Из Таблица Цикл
|
||
Услуга = Новый Структура;
|
||
Услуга.Вставить("id", НРег(XMLСтрока(Стр.Ссылка)));
|
||
Услуга.Вставить("name", Стр.Наименование);
|
||
Услуга.Вставить("duration_min", ДлитМин);
|
||
Услуга.Вставить("price", Стр.Цена);
|
||
Массив.Добавить(Услуга);
|
||
КонецЦикла;
|
||
|
||
ЗаписьJSON = Новый ЗаписьJSON;
|
||
ЗаписьJSON.УстановитьСтроку();
|
||
ЗаписатьJSON(ЗаписьJSON, Массив);
|
||
Результат = ЗаписьJSON.Закрыть();
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err:
|
||
self.send_json(502, {"code": "1c_error", "message": err})
|
||
else:
|
||
services = data or []
|
||
while isinstance(services, str):
|
||
try:
|
||
services = json.loads(services)
|
||
except Exception:
|
||
break
|
||
if not isinstance(services, list):
|
||
services = []
|
||
for s in services:
|
||
if isinstance(s, dict):
|
||
try:
|
||
s["duration_min"] = int(s.get("duration_min", 30))
|
||
except (ValueError, TypeError):
|
||
s["duration_min"] = 30
|
||
try:
|
||
s["price"] = float(s.get("price", 0.0))
|
||
except (ValueError, TypeError):
|
||
s["price"] = 0.0
|
||
self.send_json(200, services)
|
||
return
|
||
|
||
if path == "/schedule":
|
||
doctor_id = query.get("doctor_id", [""])[0]
|
||
from_str = query.get("from", [""])[0]
|
||
to_str = query.get("to", [""])[0]
|
||
branch_id = query.get("branch_id", [""])[0]
|
||
|
||
now = datetime.now()
|
||
dt_from = now
|
||
if from_str:
|
||
try:
|
||
dt_from = datetime.fromisoformat(from_str.replace("Z", "+00:00"))
|
||
except Exception:
|
||
pass
|
||
|
||
dt_to = dt_from
|
||
if to_str:
|
||
try:
|
||
dt_to = datetime.fromisoformat(to_str.replace("Z", "+00:00"))
|
||
except Exception:
|
||
pass
|
||
if dt_to <= dt_from:
|
||
from datetime import timedelta
|
||
dt_to = dt_from + timedelta(days=14)
|
||
|
||
# Determine target doctors
|
||
if doctor_id:
|
||
if not is_uuid(doctor_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "doctor_id must be a UUID"})
|
||
return
|
||
target_ids = [doctor_id]
|
||
else:
|
||
target_ids = get_linked_doctor_ids()
|
||
target_ids = [x for x in target_ids if is_uuid(x)]
|
||
|
||
if branch_id and not is_uuid(branch_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "branch_id must be a UUID"})
|
||
return
|
||
|
||
ids_1c_array = ", ".join([f'Новый УникальныйИдентификатор("{x}")' for x in target_ids])
|
||
|
||
if branch_id:
|
||
branch_clause = f'И Графики.Филиал = Справочники.Филиалы.ПолучитьСсылку(Новый УникальныйИдентификатор("{branch_id}"))'
|
||
else:
|
||
branch_clause = ""
|
||
|
||
code = f"""
|
||
МассивИД = Новый Массив;
|
||
"""
|
||
for x in target_ids:
|
||
code += f'МассивИД.Добавить(Новый УникальныйИдентификатор("{x}"));\n'
|
||
|
||
code += f"""
|
||
МассивСсылок = Новый Массив;
|
||
Для Каждого ИД Из МассивИД Цикл
|
||
Ссылка = Справочники.Сотрудники.ПолучитьСсылку(ИД);
|
||
МассивСсылок.Добавить(Ссылка);
|
||
КонецЦикла;
|
||
|
||
ДатаС = Дата({dt_from.year}, {dt_from.month}, {dt_from.day}, {dt_from.hour}, {dt_from.minute}, {dt_from.second});
|
||
ДатаПо = Дата({dt_to.year}, {dt_to.month}, {dt_to.day}, {dt_to.hour}, {dt_to.minute}, {dt_to.second});
|
||
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст =
|
||
"ВЫБРАТЬ
|
||
| Графики.Сотрудник КАК Сотрудник,
|
||
| Графики.Сотрудник.ДлительностьПриема КАК ДлительностьПриема,
|
||
| Графики.Дата КАК Дата,
|
||
| Графики.ВремяНачала КАК ВремяНачала,
|
||
| Графики.ВремяОкончания КАК ВремяОкончания,
|
||
| Графики.Кабинет КАК Кабинет,
|
||
| Графики.Кабинет.Наименование КАК КабинетИмя,
|
||
| Графики.Филиал КАК Филиал
|
||
|ИЗ
|
||
| РегистрСведений.ГрафикиРаботы КАК Графики
|
||
|ГДЕ
|
||
| Графики.Дата МЕЖДУ &ДатаС И &ДатаПо
|
||
| И Графики.Сотрудник В (&МассивВрачей)
|
||
| И Графики.Сотрудник.ПометкаУдаления = ЛОЖЬ
|
||
| И (Графики.Филиал.Наименование ПОДОБНО ""%Дана%"" ИЛИ Графики.Филиал.Наименование ПОДОБНО ""%Ляхова 22%"")
|
||
| И НЕ Графики.Филиал.Наименование ПОДОБНО ""%Абилити%""
|
||
| И НЕ Графики.Филиал.Наименование ПОДОБНО ""%Целитель%""
|
||
| {branch_clause}";
|
||
|
||
Запрос.УстановитьПараметр("МассивВрачей", МассивСсылок);
|
||
Запрос.УстановитьПараметр("ДатаС", ДатаС);
|
||
Запрос.УстановитьПараметр("ДатаПо", ДатаПо);
|
||
|
||
Таблица = Запрос.Выполнить().Выгрузить();
|
||
|
||
// Получаем существующие заявки для блокировки занятых слотов
|
||
ЗапросЗаявок = Новый Запрос;
|
||
ЗапросЗаявок.Текст =
|
||
"ВЫБРАТЬ
|
||
| Заявка.Сотрудник КАК Сотрудник,
|
||
| Заявка.ДатаНачала КАК ДатаНачала,
|
||
| Заявка.ДатаОкончания КАК ДатаОкончания
|
||
|ИЗ
|
||
| Документ.Заявка КАК Заявка
|
||
|ГДЕ
|
||
| Заявка.ПометкаУдаления = ЛОЖЬ
|
||
| И (Заявка.Состояние.ТипСостояния <> &ТипОтменена ИЛИ Заявка.Состояние.ТипСостояния ЕСТЬ NULL)
|
||
| И Заявка.Сотрудник В (&МассивВрачей)
|
||
| И Заявка.ДатаНачала < &ДатаПо
|
||
| И Заявка.ДатаОкончания > &ДатаС";
|
||
|
||
ЗапросЗаявок.УстановитьПараметр("МассивВрачей", МассивСсылок);
|
||
ЗапросЗаявок.УстановитьПараметр("ДатаС", ДатаС);
|
||
ЗапросЗаявок.УстановитьПараметр("ДатаПо", ДатаПо);
|
||
ЗапросЗаявок.УстановитьПараметр("ТипОтменена", Перечисления.ТипыСостоянийЗаявок.Отменена);
|
||
|
||
ТаблицаЗаявок = ЗапросЗаявок.Выполнить().Выгрузить();
|
||
|
||
Слоты = Новый Массив;
|
||
ТекущееВремя = ТекущаяДата();
|
||
|
||
Для Каждого Стр Из Таблица Цикл
|
||
Длит = Стр.ДлительностьПриема;
|
||
ШагМинут = ?(ТипЗнч(Длит) = Тип("Дата"), Час(Длит) * 60 + Минута(Длит), 30);
|
||
Если ШагМинут < 15 ИЛИ ШагМинут > 60 Тогда ШагМинут = 30; КонецЕсли;
|
||
ШагСекунд = ШагМинут * 60;
|
||
|
||
ДатаСмены = НачалоДня(Стр.Дата);
|
||
НачалоСмены = ДатаСмены + (Стр.ВремяНачала - НачалоДня(Стр.ВремяНачала));
|
||
КонецСмены = ДатаСмены + (Стр.ВремяОкончания - НачалоДня(Стр.ВремяОкончания));
|
||
|
||
IDВрача = НРег(XMLСтрока(Стр.Сотрудник));
|
||
IDФилиала = НРег(XMLСтрока(Стр.Филиал));
|
||
ИмяКабинета = ?(ЗначениеЗаполнено(Стр.Кабинет), Стр.КабинетИмя, "");
|
||
|
||
Текущий = НачалоСмены;
|
||
Пока (Текущий + ШагСекунд) <= КонецСмены Цикл
|
||
СлотКонец = Текущий + ШагСекунд;
|
||
|
||
// Проверка на занятость заявками
|
||
Занят = Ложь;
|
||
Для Каждого СтрЗаявки Из ТаблицаЗаявок Цикл
|
||
Если СтрЗаявки.Сотрудник = Стр.Сотрудник И (СтрЗаявки.ДатаНачала < СлотКонец) И (СтрЗаявки.ДатаОкончания > Текущий) Тогда
|
||
Занят = Истина;
|
||
Прервать;
|
||
КонецЕсли;
|
||
КонецЦикла;
|
||
|
||
Слот = Новый Структура;
|
||
Слот.Вставить("doctor_id", IDВрача);
|
||
Слот.Вставить("branch_id", IDФилиала);
|
||
Слот.Вставить("cabinet", ИмяКабинета);
|
||
Слот.Вставить("start", Формат(Текущий, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
Слот.Вставить("end", Формат(СлотКонец, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
Слот.Вставить("status", ?(Занят, "busy", "free"));
|
||
Слот.Вставить("slot_id", IDВрача + "_" + Формат(Текущий, "ДФ=yyyyMMddHHmmss"));
|
||
Слоты.Добавить(Слот);
|
||
|
||
Текущий = СлотКонец;
|
||
КонецЦикла;
|
||
КонецЦикла;
|
||
|
||
ЗаписьJSON = Новый ЗаписьJSON;
|
||
ЗаписьJSON.УстановитьСтроку();
|
||
ЗаписатьJSON(ЗаписьJSON, Слоты);
|
||
Результат = ЗаписьJSON.Закрыть();
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err:
|
||
self.send_json(502, {"code": "1c_error", "message": err})
|
||
else:
|
||
self.send_json(200, data or [])
|
||
return
|
||
|
||
if path.startswith("/appointments/"):
|
||
appt_id = path.split("/")[2]
|
||
if not is_uuid(appt_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "appointment_id must be a UUID"})
|
||
return
|
||
code = f"""
|
||
ДокСсылка = Документы.Заявка.ПолучитьСсылку(Новый УникальныйИдентификатор("{appt_id}"));
|
||
Ответ = Новый Структура;
|
||
Ответ.Вставить("appointment_id", "{appt_id}");
|
||
Ответ.Вставить("status", "booked");
|
||
Если ЗначениеЗаполнено(ДокСсылка) И Не ДокСсылка.Пустая() Тогда
|
||
Если ДокСсылка.ПометкаУдаления Тогда
|
||
Ответ.Вставить("status", "cancelled");
|
||
ИначеЕсли ДокСсылка.Состояние.ТипСостояния = Перечисления.ТипыСостоянийЗаявок.Отменена Тогда
|
||
Ответ.Вставить("status", "cancelled");
|
||
КонецЕсли;
|
||
Ответ.Вставить("start", Формат(ДокСсылка.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
Ответ.Вставить("end", Формат(ДокСсылка.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
КонецЕсли;
|
||
|
||
ЗаписьJSON = Новый ЗаписьJSON;
|
||
ЗаписьJSON.УстановитьСтроку();
|
||
ЗаписатьJSON(ЗаписьJSON, Ответ);
|
||
Результат = ЗаписьJSON.Закрыть();
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err:
|
||
self.send_json(502, {"code": "1c_error", "message": err})
|
||
else:
|
||
self.send_json(200, data or {"appointment_id": appt_id, "status": "booked"})
|
||
return
|
||
|
||
if path == "/changes":
|
||
self.send_json(200, {"version": 1, "doctor_ids": []})
|
||
return
|
||
|
||
self.send_json(404, {"code": "not_found", "message": "Unknown endpoint"})
|
||
|
||
def do_POST(self):
|
||
parsed = urllib.parse.urlparse(self.path)
|
||
path = parsed.path.rstrip("/")
|
||
|
||
if not self._require_auth():
|
||
return
|
||
try:
|
||
content_len = int(self.headers.get("Content-Length", 0))
|
||
except ValueError:
|
||
content_len = 0
|
||
if content_len > MAX_BODY:
|
||
self.send_json(413, {"code": "too_large", "message": "Request body too large"})
|
||
return
|
||
post_data = self.rfile.read(content_len) if content_len > 0 else b"{}"
|
||
try:
|
||
req_json = json.loads(post_data.decode("utf-8"))
|
||
except Exception:
|
||
self.send_json(400, {"code": "bad_request", "message": "Invalid JSON"})
|
||
return
|
||
|
||
if not isinstance(req_json, dict):
|
||
self.send_json(400, {"code": "bad_request", "message": "JSON object required"})
|
||
return
|
||
|
||
if path == "/appointments":
|
||
doctor_id = req_json.get("doctor_id", "")
|
||
bitrix_lead_id = bsl_text(str(req_json.get("bitrix_lead_id", "")).strip(), 200)
|
||
branch_id = req_json.get("branch_id", "")
|
||
patient_name = req_json.get("patient_name", "")
|
||
patient_phone = req_json.get("patient_phone", "")
|
||
comment = req_json.get("comment", "")
|
||
start_str = req_json.get("start", "")
|
||
if not start_str:
|
||
start_str = req_json.get("start_time", "")
|
||
if "patient" in req_json and isinstance(req_json["patient"], dict):
|
||
p = req_json["patient"]
|
||
if not patient_name:
|
||
patient_name = p.get("name") or f"{p.get('last_name','')} {p.get('first_name','')} {p.get('middle_name','')}".strip()
|
||
if not patient_phone:
|
||
patient_phone = p.get("phone", "")
|
||
if not comment:
|
||
comment = p.get("comment", "")
|
||
if not patient_name:
|
||
patient_name = "Пациент с сайта"
|
||
|
||
# Валидация до сборки BSL-кода: ID — строго UUID, дата — ISO 8601,
|
||
# свободный текст — через bsl_text (удвоение кавычек + срез управляющих).
|
||
if not is_uuid(doctor_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "doctor_id must be a UUID"})
|
||
return
|
||
if branch_id and not is_uuid(branch_id):
|
||
self.send_json(400, {"code": "bad_request", "message": "branch_id must be a UUID"})
|
||
return
|
||
try:
|
||
start_str = datetime.fromisoformat(str(start_str).replace("Z", "+00:00")).isoformat()
|
||
except (ValueError, TypeError):
|
||
self.send_json(400, {"code": "bad_request", "message": "start must be ISO 8601"})
|
||
return
|
||
patient_name = bsl_text(patient_name, 200)
|
||
patient_phone = clean_phone(patient_phone)
|
||
comment = bsl_text(comment, 2000)
|
||
start_str = bsl_text(start_str, 40)
|
||
|
||
# Normalize phone digits and generate search patterns
|
||
digits = "".join([c for c in patient_phone if c.isdigit()])
|
||
if len(digits) >= 10:
|
||
tail = digits[-10:]
|
||
d = list(tail)
|
||
pattern_delims = f"%{d[0]}{d[1]}{d[2]}%{d[3]}{d[4]}{d[5]}%{d[6]}{d[7]}%{d[8]}{d[9]}%"
|
||
pattern_solid = f"%{tail}%"
|
||
else:
|
||
pattern_delims = f"%{digits}%"
|
||
pattern_solid = f"%{digits}%"
|
||
|
||
code = f"""
|
||
Попытка
|
||
ВрачСсылка = Справочники.Сотрудники.ПолучитьСсылку(Новый УникальныйИдентификатор("{doctor_id}"));
|
||
|
||
// 1. Поиск существующего клиента по номеру телефона и подмена на него
|
||
ЗапросКл = Новый Запрос;
|
||
ЗапросКл.Текст =
|
||
"ВЫБРАТЬ ПЕРВЫЕ 1
|
||
| КИ.Объект КАК Клиент
|
||
|ИЗ
|
||
| РегистрСведений.КонтактнаяИнформация КАК КИ
|
||
|ГДЕ
|
||
| КИ.Тип = ЗНАЧЕНИЕ(Перечисление.ТипыКонтактнойИнформации.Телефон)
|
||
| И КИ.Объект ССЫЛКА Справочник.Клиенты
|
||
| И КИ.Объект.ПометкаУдаления = ЛОЖЬ
|
||
| И (КИ.Представление ПОДОБНО &ШаблонРазд
|
||
| ИЛИ КИ.Представление ПОДОБНО &ШаблонСплош
|
||
| ИЛИ КИ.Поле3 ПОДОБНО &ШаблонРазд
|
||
| ИЛИ КИ.Поле3 ПОДОБНО &ШаблонСплош)";
|
||
|
||
ЗапросКл.УстановитьПараметр("ШаблонРазд", "{pattern_delims}");
|
||
ЗапросКл.УстановитьПараметр("ШаблонСплош", "{pattern_solid}");
|
||
ВыбКл = ЗапросКл.Выполнить().Выбрать();
|
||
|
||
КлиентСсылка = Неопределено;
|
||
Если ВыбКл.Следующий() И ЗначениеЗаполнено(ВыбКл.Клиент) Тогда
|
||
// Подменяем на найденного клиента из базы 1С
|
||
КлиентСсылка = ВыбКл.Клиент;
|
||
Иначе
|
||
// Клиент не найден -> создаем нового
|
||
ОбъектКл = Справочники.Клиенты.СоздатьЭлемент();
|
||
ОбъектКл.Наименование = "{patient_name}";
|
||
ОбъектКл.Комментарий = "Создан через сайт danamedical.ru";
|
||
ОбъектКл.Записать();
|
||
КлиентСсылка = ОбъектКл.Ссылка;
|
||
|
||
Попытка
|
||
ВидКИ = Неопределено;
|
||
Попытка
|
||
ВидКИ = Справочники.ВидыКонтактнойИнформации.ТелефонСотовый;
|
||
Исключение
|
||
ВидКИ = Справочники.ВидыКонтактнойИнформации.НайтиПоНаименованию("Телефон сотовый");
|
||
КонецПопытки;
|
||
|
||
МЗ = РегистрыСведений.КонтактнаяИнформация.СоздатьМенеджерЗаписи();
|
||
МЗ.Объект = КлиентСсылка;
|
||
МЗ.Тип = Перечисления.ТипыКонтактнойИнформации.Телефон;
|
||
Если ЗначениеЗаполнено(ВидКИ) Тогда
|
||
МЗ.Вид = ВидКИ;
|
||
КонецЕсли;
|
||
МЗ.Представление = "{patient_phone}";
|
||
МЗ.Поле3 = "{patient_phone}";
|
||
МЗ.Записать(Истина);
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
// Время
|
||
ВремяНач = ДМ_СайтAPI.РазобратьISO8601("{start_str}");
|
||
Если Не ЗначениеЗаполнено(ВремяНач) Тогда
|
||
ВремяНач = ТекущаяДата();
|
||
КонецЕсли;
|
||
Длит = ВрачСсылка.ДлительностьПриема;
|
||
ШагМинут = ?(ТипЗнч(Длит) = Тип("Дата"), Час(Длит) * 60 + Минута(Длит), 30);
|
||
Если ШагМинут < 15 ИЛИ ШагМинут > 60 Тогда ШагМинут = 30; КонецЕсли;
|
||
|
||
// Определение филиала (строго клиника сети Дана)
|
||
ФилиалСсылка = Неопределено;
|
||
Если ЗначениеЗаполнено("{branch_id}") Тогда
|
||
Попытка
|
||
ФилиалКанд = Справочники.Филиалы.ПолучитьСсылку(Новый УникальныйИдентификатор("{branch_id}"));
|
||
Если ЗначениеЗаполнено(ФилиалКанд) И Не ФилиалКанд.Пустая() Тогда
|
||
ИмяФ = ВРег(ФилиалКанд.Наименование);
|
||
Если (Найти(ИмяФ, "ДАНА") > 0 ИЛИ Найти(ИмяФ, "ЛЯХОВА 22") > 0)
|
||
И Найти(ИмяФ, "АБИЛИТИ") = 0
|
||
И Найти(ИмяФ, "ЦЕЛИТЕЛЬ") = 0 Тогда
|
||
ФилиалСсылка = ФилиалКанд;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ФилиалСсылка) Тогда
|
||
ЗапросСмены = Новый Запрос;
|
||
ЗапросСмены.Текст =
|
||
"ВЫБРАТЬ ПЕРВЫЕ 1
|
||
| Графики.Филиал КАК Филиал
|
||
|ИЗ
|
||
| РегистрСведений.ГрафикиРаботы КАК Графики
|
||
|ГДЕ
|
||
| Графики.Сотрудник = &Врач
|
||
| И Графики.Дата = НАЧАЛОПЕРИОДА(&ВремяНач, ДЕНЬ)
|
||
| И (Графики.Филиал.Наименование ПОДОБНО ""%Дана%"" ИЛИ Графики.Филиал.Наименование ПОДОБНО ""%Ляхова 22%"")
|
||
| И НЕ Графики.Филиал.Наименование ПОДОБНО ""%Абилити%""
|
||
| И НЕ Графики.Филиал.Наименование ПОДОБНО ""%Целитель%"" ";
|
||
ЗапросСмены.УстановитьПараметр("Врач", ВрачСсылка);
|
||
ЗапросСмены.УстановитьПараметр("ВремяНач", ВремяНач);
|
||
ВыбСмены = ЗапросСмены.Выполнить().Выбрать();
|
||
Если ВыбСмены.Следующий() Тогда
|
||
ФилиалСсылка = ВыбСмены.Филиал;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ФилиалСсылка) Тогда
|
||
ЗапросФ = Новый Запрос(
|
||
"ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Справочник.Филиалы КАК Филиалы
|
||
|ГДЕ Филиалы.ПометкаУдаления = ЛОЖЬ
|
||
| И (Филиалы.Наименование ПОДОБНО ""%Дана%"" ИЛИ Филиалы.Наименование ПОДОБНО ""%Ляхова 22%"")
|
||
| И НЕ Филиалы.Наименование ПОДОБНО ""%Абилити%""
|
||
| И НЕ Филиалы.Наименование ПОДОБНО ""%Целитель%"" ");
|
||
ВыбФ = ЗапросФ.Выполнить().Выбрать();
|
||
Если ВыбФ.Следующий() Тогда
|
||
ФилиалСсылка = ВыбФ.Ссылка;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
|
||
// Создаем Заявку
|
||
Док = Документы.Заявка.СоздатьДокумент();
|
||
Док.Дата = ТекущаяДата();
|
||
Док.ДатаНачала = ВремяНач;
|
||
Док.ДатаОкончания = ВремяНач + ШагМинут * 60;
|
||
Док.ВремяНачала = Док.ДатаНачала;
|
||
Док.ВремяОкончания = Док.ДатаОкончания;
|
||
Док.Сотрудник = ВрачСсылка;
|
||
Док.Филиал = ФилиалСсылка;
|
||
Док.Клиент = КлиентСсылка;
|
||
Док.Состояние = Справочники.ВидыСостоянийЗаявок.Запланирована;
|
||
Док.Примечание = "Онлайн-запись: {comment}";
|
||
|
||
Ист = Справочники.ИсточникиИнформации.НайтиПоНаименованию("Сайт");
|
||
Если ЗначениеЗаполнено(Ист) Тогда
|
||
Попытка Док.БИТ_ИсточникИнформации = Ист; Исключение КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Если ЗначениеЗаполнено("{bitrix_lead_id}") Тогда
|
||
Попытка
|
||
Док.бит_Б24_СозданИзБитрикс24 = Истина;
|
||
Док.бит_Б24_БитриксИД = "{bitrix_lead_id}";
|
||
Док.бит_НомерКлиентаБ24 = "{bitrix_lead_id}";
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Док.Записать(РежимЗаписиДокумента.Запись);
|
||
ID = НРег(XMLСтрока(Док.Ссылка));
|
||
ID_B64 = Base64Строка(ПолучитьДвоичныеДанныеИзСтроки(ID));
|
||
|
||
Ответ = Новый Структура;
|
||
Ответ.Вставить("appointment_id", ID);
|
||
Ответ.Вставить("appointment_id_b64", ID_B64);
|
||
Ответ.Вставить("number", Док.Номер);
|
||
Ответ.Вставить("status", "booked");
|
||
Ответ.Вставить("start", Формат(Док.ДатаНачала, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
Ответ.Вставить("end", Формат(Док.ДатаОкончания, "ДФ=yyyy-MM-ddTHH:mm:ss") + "+03:00");
|
||
Ответ.Вставить("branch_id", НРег(XMLСтрока(ФилиалСсылка)));
|
||
Ответ.Вставить("patient_id", НРег(XMLСтрока(КлиентСсылка)));
|
||
|
||
ЗаписьJSON = Новый ЗаписьJSON;
|
||
ЗаписьJSON.УстановитьСтроку();
|
||
ЗаписатьJSON(ЗаписьJSON, Ответ);
|
||
Результат = ЗаписьJSON.Закрыть();
|
||
Исключение
|
||
Результат = "ОШИБКА: " + ОписаниеОшибки();
|
||
КонецПопытки;
|
||
"""
|
||
data, err = call_1c(code)
|
||
if isinstance(data, dict) and "appointment_id_b64" in data:
|
||
import base64
|
||
try:
|
||
data["appointment_id"] = base64.b64decode(data["appointment_id_b64"]).decode("utf-8").strip()
|
||
except Exception:
|
||
pass
|
||
if err or (isinstance(data, str) and data.startswith("ОШИБКА")):
|
||
self.send_json(500, {"code": "booking_failed", "message": err or data})
|
||
else:
|
||
self.send_json(201, data)
|
||
return
|
||
|
||
if path == "/appointments/link-bitrix":
|
||
# Legacy linking supports UUIDs and document numbers: escape both.
|
||
appointment_id = bsl_text(str(req_json.get("appointment_id", "")).strip(), 200)
|
||
bitrix_lead_id = bsl_text(str(req_json.get("bitrix_lead_id", "")).strip(), 200)
|
||
if not appointment_id or not bitrix_lead_id:
|
||
self.send_json(400, {"code": "bad_request", "message": "appointment_id and bitrix_lead_id are required"})
|
||
return
|
||
|
||
code = f"""
|
||
Попытка
|
||
ДокСсылка = Неопределено;
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.ПолучитьСсылку(Новый УникальныйИдентификатор("{appointment_id}"));
|
||
Исключение
|
||
КонецПопытки;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.НайтиПоНомеру("{appointment_id}", ТекущаяДата());
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Документ.Заявка ГДЕ Номер = &Номер УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Запрос.УстановитьПараметр("Номер", "{appointment_id}");
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Если Выб.Следующий() Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 30 Ссылка ИЗ Документ.Заявка УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Пока Выб.Следующий() Цикл
|
||
СтрGUID = НРег(XMLСтрока(Выб.Ссылка));
|
||
Если Найти("{appointment_id}", СтрGUID) > 0 ИЛИ (СтрДлина("{appointment_id}") > 20 И Найти(СтрGUID, Прав("{appointment_id}", 25)) > 0) Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
Прервать;
|
||
КонецЕсли;
|
||
КонецЦикла;
|
||
КонецЕсли;
|
||
|
||
Если ЗначениеЗаполнено(ДокСсылка) Тогда
|
||
ДокОбъект = ДокСсылка.ПолучитьОбъект();
|
||
Если ДокОбъект <> Неопределено Тогда
|
||
ДокОбъект.бит_Б24_СозданИзБитрикс24 = Истина;
|
||
ДокОбъект.бит_Б24_БитриксИД = "{bitrix_lead_id}";
|
||
ДокОбъект.бит_НомерКлиентаБ24 = "{bitrix_lead_id}";
|
||
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
|
||
Результат = "OK";
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Исключение
|
||
Результат = "ОШИБКА: " + ОписаниеОшибки();
|
||
КонецПопытки;
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err or (isinstance(data, str) and data.startswith("ОШИБКА")):
|
||
self.send_json(500, {"code": "link_failed", "message": err or data})
|
||
elif data == "NOT_FOUND":
|
||
self.send_json(404, {"code": "not_found", "message": "Запись в 1С не найдена"})
|
||
else:
|
||
self.send_json(200, {"status": "ok", "message": "Связано успешно"})
|
||
return
|
||
|
||
if path == "/appointments/link-crm":
|
||
appointment_id = bsl_text(str(req_json.get("appointment_id", "")).strip(), 200)
|
||
crm_deal_id = bsl_text(str(req_json.get("crm_deal_id", "")).strip(), 200)
|
||
if not appointment_id or not crm_deal_id:
|
||
self.send_json(400, {"code": "bad_request", "message": "appointment_id and crm_deal_id are required"})
|
||
return
|
||
|
||
code = f"""
|
||
Попытка
|
||
ДокСсылка = Неопределено;
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.ПолучитьСсылку(Новый УникальныйИдентификатор("{appointment_id}"));
|
||
Исключение
|
||
КонецПопытки;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.НайтиПоНомеру("{appointment_id}", ТекущаяДата());
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Документ.Заявка ГДЕ Номер = &Номер УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Запрос.УстановитьПараметр("Номер", "{appointment_id}");
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Если Выб.Следующий() Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 30 Ссылка ИЗ Документ.Заявка УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Пока Выб.Следующий() Цикл
|
||
СтрGUID = НРег(XMLСтрока(Выб.Ссылка));
|
||
Если Найти("{appointment_id}", СтрGUID) > 0 ИЛИ (СтрДлина("{appointment_id}") > 20 И Найти(СтрGUID, Прав("{appointment_id}", 25)) > 0) Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
Прервать;
|
||
КонецЕсли;
|
||
КонецЦикла;
|
||
КонецЕсли;
|
||
|
||
Если ЗначениеЗаполнено(ДокСсылка) Тогда
|
||
ДокОбъект = ДокСсылка.ПолучитьОбъект();
|
||
Если ДокОбъект <> Неопределено Тогда
|
||
Если Найти(ДокОбъект.Примечание, "{crm_deal_id}") = 0 Тогда
|
||
ДокОбъект.Примечание = СокрЛП(ДокОбъект.Примечание) + " [CRM: {crm_deal_id}]";
|
||
КонецЕсли;
|
||
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
|
||
Результат = "OK";
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Исключение
|
||
Результат = "ОШИБКА: " + ОписаниеОшибки();
|
||
КонецПопытки;
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err or (isinstance(data, str) and data.startswith("ОШИБКА")):
|
||
self.send_json(500, {"code": "link_failed", "message": err or data})
|
||
elif data == "NOT_FOUND":
|
||
self.send_json(404, {"code": "not_found", "message": "Запись в 1С не найдена"})
|
||
else:
|
||
self.send_json(200, {"status": "ok", "message": "Связано успешно с CRM"})
|
||
return
|
||
|
||
self.send_json(404, {"code": "not_found", "message": "Unknown endpoint"})
|
||
|
||
def do_DELETE(self):
|
||
parsed = urllib.parse.urlparse(self.path)
|
||
path = parsed.path.rstrip("/")
|
||
|
||
if not self._require_auth():
|
||
return
|
||
|
||
parts = path.split("/")
|
||
if len(parts) == 3 and parts[1] == "appointments":
|
||
appointment_id = bsl_text(urllib.parse.unquote(parts[2]).strip(), 200)
|
||
code = f"""
|
||
Попытка
|
||
ДокСсылка = Неопределено;
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.ПолучитьСсылку(Новый УникальныйИдентификатор("{appointment_id}"));
|
||
Исключение
|
||
КонецПопытки;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Попытка
|
||
ДокСсылка = Документы.Заявка.НайтиПоНомеру("{appointment_id}", ТекущаяДата());
|
||
Исключение
|
||
КонецПопытки;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Документ.Заявка ГДЕ Номер = &Номер УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Запрос.УстановитьПараметр("Номер", "{appointment_id}");
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Если Выб.Следующий() Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
КонецЕсли;
|
||
КонецЕсли;
|
||
|
||
Если Не ЗначениеЗаполнено(ДокСсылка) ИЛИ ДокСсылка.ПолучитьОбъект() = Неопределено Тогда
|
||
Запрос = Новый Запрос;
|
||
Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 30 Ссылка ИЗ Документ.Заявка УПОРЯДОЧИТЬ ПО Дата УБЫВ";
|
||
Выб = Запрос.Выполнить().Выбрать();
|
||
Пока Выб.Следующий() Цикл
|
||
СтрGUID = НРег(XMLСтрока(Выб.Ссылка));
|
||
Если Найти("{appointment_id}", СтрGUID) > 0 ИЛИ (СтрДлина("{appointment_id}") > 20 И Найти(СтрGUID, Прав("{appointment_id}", 25)) > 0) Тогда
|
||
ДокСсылка = Выб.Ссылка;
|
||
Прервать;
|
||
КонецЕсли;
|
||
КонецЦикла;
|
||
КонецЕсли;
|
||
|
||
Если ЗначениеЗаполнено(ДокСсылка) Тогда
|
||
ДокОбъект = ДокСсылка.ПолучитьОбъект();
|
||
Если ДокОбъект <> Неопределено Тогда
|
||
Попытка
|
||
ДокОбъект.Состояние = Справочники.ВидыСостоянийЗаявок.Отменена;
|
||
Исключение
|
||
КонецПопытки;
|
||
ДокОбъект.ПометкаУдаления = Истина;
|
||
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
|
||
Результат = "OK";
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Иначе
|
||
Результат = "NOT_FOUND";
|
||
КонецЕсли;
|
||
Исключение
|
||
Результат = "ОШИБКА: " + ОписаниеОшибки();
|
||
КонецПопытки;
|
||
"""
|
||
data, err = call_1c(code)
|
||
if err or (isinstance(data, str) and data.startswith("ОШИБКА")):
|
||
self.send_json(500, {"code": "cancel_failed", "message": err or data})
|
||
elif data == "NOT_FOUND":
|
||
self.send_json(404, {"code": "not_found", "message": "Заявка в 1С не найдена"})
|
||
else:
|
||
self.send_json(200, {"status": "cancelled", "message": "Запись отменена в 1С"})
|
||
return
|
||
|
||
self.send_json(404, {"code": "not_found", "message": "Unknown endpoint"})
|
||
|
||
def run():
|
||
server_address = ("0.0.0.0", 8088)
|
||
httpd = HTTPServer(server_address, BridgeHandler)
|
||
print("Bridge server running on port 8088 with live Postgres link...", flush=True)
|
||
httpd.serve_forever()
|
||
|
||
if __name__ == "__main__":
|
||
run()
|