feat(booking): automatically link 1C appointment to Bitrix24 lead ID and implement do_DELETE

This commit is contained in:
Dana Medical Developer 2026-10-15 10:35:12 +00:00
parent 2d803fcd28
commit 6597464ac3
3 changed files with 483 additions and 297 deletions

View File

@ -456,6 +456,7 @@ class BridgeHandler(BaseHTTPRequestHandler):
if path == "/appointments":
doctor_id = req_json.get("doctor_id", "")
bitrix_lead_id = str(req_json.get("bitrix_lead_id", "")).strip()
branch_id = req_json.get("branch_id", "")
patient_name = req_json.get("patient_name", "")
patient_phone = req_json.get("patient_phone", "")
@ -620,11 +621,23 @@ class BridgeHandler(BaseHTTPRequestHandler):
Попытка Док.БИТ_ИсточникИнформации = Ист; Исключение КонецПопытки;
КонецЕсли;
Если ЗначениеЗаполнено("{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");
@ -640,12 +653,165 @@ class BridgeHandler(BaseHTTPRequestHandler):
КонецПопытки;
"""
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":
appointment_id = str(req_json.get("appointment_id", "")).strip()
bitrix_lead_id = str(req_json.get("bitrix_lead_id", "")).strip()
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
self.send_json(404, {"code": "not_found", "message": "Unknown endpoint"})
def do_DELETE(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path.rstrip("/")
parts = path.split("/")
if len(parts) == 3 and parts[1] == "appointments":
appointment_id = urllib.parse.unquote(parts[2]).strip()
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():

View File

@ -188,6 +188,14 @@ func (a *API) processConfirmQueue(ctx context.Context, now time.Time) error {
}
it.BitrixLeadID = id
slog.Info("booking confirm: лид создан", "appointment", it.ID, "lead", id)
if it.OnecID != "" {
if err := a.onec.LinkBitrix(ctx, it.OnecID, id); err != nil {
slog.Warn("booking confirm: не удалось связать 1С с Битриксом", "onec_id", it.OnecID, "lead", id, "err", err)
} else {
slog.Info("booking confirm: заявка в 1С связана с лидом Битрикса", "onec_id", it.OnecID, "lead", id)
}
}
}
ids = append(ids, it.BitrixLeadID)
}

View File

@ -193,6 +193,18 @@ func (c *Client) Cancel(ctx context.Context, id string) error {
return err
}
func (c *Client) LinkBitrix(ctx context.Context, appointmentID, bitrixLeadID string) error {
body, err := json.Marshal(map[string]string{
"appointment_id": appointmentID,
"bitrix_lead_id": bitrixLeadID,
})
if err != nil {
return err
}
_, err = c.do(ctx, http.MethodPost, "/appointments/link-bitrix", nil, body)
return err
}
// getList принимает ответ и в виде массива, и в виде объекта {"<key>": [...]}.
func (c *Client) getList(ctx context.Context, path string, q url.Values, key string, out any) error {
raw, err := c.do(ctx, http.MethodGet, path, q, nil)