phase-3: replace CFE with main-config patch (per user request)

User decided to patch the main BIT.RA configuration instead of installing an extension.
- bitra-cfe/ removed in previous step
- bitra-patch/ added with BSL files for direct copy-paste into Configurator:
  - CommonModules/bfd_IntegrationAPIHelpers/Module.bsl (134 lines) — privileged server helpers
  - HTTPServices/bfd_IntegrationAPI/Module.bsl (478 lines) — 11 GET handlers
- README.md (274 lines): step-by-step Configurator instructions:
  - object properties (CommonModule flags, HTTPService rootURL, URL templates, methods)
  - role permissions matrix (15 BIT.RA objects + HTTPService + CommonModule)
  - user bfd_api_user creation
  - Apache publication
  - curl tests for each endpoint
This commit is contained in:
Roman Chesnokov
2026-05-14 19:42:48 +05:00
parent 7da9d9dae1
commit f70bbf53f3
9 changed files with 287 additions and 669 deletions
@@ -0,0 +1,478 @@
////////////////////////////////////////////////////////////////////////////////
// HTTPService bfd_IntegrationAPI — read-only REST API для bit-flight-deck.
//
// Корневой URL (RootURL) — bfd-api
// После публикации: http://<host>/<dbname>/hs/bfd-api/v1/...
//
// Все методы GET. Аутентификация Basic (пользователь bfd_api_user).
// Ошибки → JSON {"error":"..."} с HTTP-кодом 500.
// Все ID объектов сериализуются как строковый UUID без префикса класса.
////////////////////////////////////////////////////////////////////////////////
#Область ОбработчикиЗапросов
// GET /v1/health
//
Функция HealthGet(Запрос) Экспорт
Ответ = Новый Структура;
Ответ.Вставить("status", "ok");
Ответ.Вставить("service", "bfd_IntegrationAPI");
Ответ.Вставить("server_time", Формат(ТекущаяДатаСеанса(), "ДФ=yyyy-MM-ddTHH:mm:ss"));
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Ответ);
КонецФункции
// GET /v1/employees
//
Функция EmployeesGet(Запрос) Экспорт
Попытка
Запрос1С = Новый Запрос;
Запрос1С.Текст =
"ВЫБРАТЬ
| Пользователи.Ссылка КАК Ссылка,
| Пользователи.Наименование КАК ФИО,
| Пользователи.EVA_ID КАК EvaID,
| Пользователи.Офис КАК Офис,
| Пользователи.Подразделение КАК Подразделение,
| Пользователи.Ставка КАК Ставка,
| Пользователи.Недействителен КАК Недействителен,
| Пользователи.ДолженЗаполнятьОтчет КАК ДолженЗаполнятьОтчет
|ИЗ
| Справочник.Пользователи КАК Пользователи
|ГДЕ
| НЕ Пользователи.ПометкаУдаления
|УПОРЯДОЧИТЬ ПО
| Пользователи.Наименование";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("id", bfd_IntegrationAPIHelpers.UUID(Выборка.Ссылка));
Запись.Вставить("full_name", Выборка.ФИО);
Запись.Вставить("email", НРег(bfd_IntegrationAPIHelpers.ИзвлечьEmail(Выборка.Ссылка)));
Запись.Вставить("eva_id", Выборка.EvaID);
Запись.Вставить("office", Строка(Выборка.Офис));
Запись.Вставить("department", Строка(Выборка.Подразделение));
Запись.Вставить("rate", Выборка.Ставка);
Запись.Вставить("is_active", НЕ Выборка.Недействителен);
Запись.Вставить("should_fill_report", Выборка.ДолженЗаполнятьОтчет);
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/works?modified_since=<ISO_DATE>&limit=N
//
Функция WorksGet(Запрос) Экспорт
Попытка
МодифицированоПосле = bfd_IntegrationAPIHelpers.ПарсДатуISO(Запрос.ПараметрыЗапроса.Получить("modified_since"));
Лимит = bfd_IntegrationAPIHelpers.ПарсЛимит(Запрос, 1000, 10000);
Запрос1С = Новый Запрос;
Запрос1С.УстановитьПараметр("МодифицированоПосле", МодифицированоПосле);
Запрос1С.Текст =
"ВЫБРАТЬ ПЕРВЫЕ " + Формат(Лимит, "ЧГ=") + "
| Работы.Ссылка КАК Ссылка,
| Работы.Номер КАК Номер,
| Работы.Дата КАК Дата,
| Работы.Исполнитель КАК Исполнитель,
| Работы.Автор КАК Автор,
| Работы.Подразделение КАК Подразделение,
| Работы.Офис КАК Офис,
| Работы.Утвержден КАК Утвержден,
| Работы.Комментарий КАК Комментарий,
| Работы.ИтогоЧасов КАК ИтогоЧасов
|ИЗ
| Документ.Работы КАК Работы
|ГДЕ
| Работы.Дата >= &МодифицированоПосле
| И Работы.Проведен
|УПОРЯДОЧИТЬ ПО
| Работы.Дата УБЫВ";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("id", bfd_IntegrationAPIHelpers.UUID(Выборка.Ссылка));
Запись.Вставить("number", Выборка.Номер);
Запись.Вставить("date", Формат(Выборка.Дата, "ДФ=yyyy-MM-ddTHH:mm:ss"));
Запись.Вставить("employee_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Исполнитель));
Запись.Вставить("author_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Автор));
Запись.Вставить("department", Строка(Выборка.Подразделение));
Запись.Вставить("office", Строка(Выборка.Офис));
Запись.Вставить("approved", Выборка.Утвержден);
Запись.Вставить("comment", Выборка.Комментарий);
Запись.Вставить("total_hours", Выборка.ИтогоЧасов);
Запись.Вставить("rows", ПолучитьСтрокиРаботы(Выборка.Ссылка));
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/projects
//
Функция ProjectsGet(Запрос) Экспорт
Попытка
Запрос1С = Новый Запрос;
Запрос1С.Текст =
"ВЫБРАТЬ
| Проекты.Ссылка КАК Ссылка,
| Проекты.Код КАК Код,
| Проекты.Наименование КАК Наименование,
| Проекты.EVA_ID КАК EvaID,
| Проекты.Кодификатор КАК Кодификатор,
| Проекты.Владелец КАК Клиент,
| Проекты.Владелец.Наименование КАК КлиентИмя,
| Проекты.Договор КАК Договор,
| Проекты.Конфигурация КАК Конфигурация,
| Проекты.Офис КАК Офис,
| Проекты.РуководительПроекта КАК РуководительПроекта,
| Проекты.МенеджерПроекта КАК МенеджерПроекта,
| Проекты.СтатусПроекта КАК СтатусПроекта,
| Проекты.СтатусДоговора КАК СтатусДоговора,
| Проекты.ДатаСтарта КАК ДатаСтарта,
| Проекты.ДатаФиниш КАК ДатаФиниш,
| Проекты.Бюджет КАК Бюджет,
| Проекты.ПометкаУдаления КАК ПометкаУдаления
|ИЗ
| Справочник.Проекты КАК Проекты";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("id", bfd_IntegrationAPIHelpers.UUID(Выборка.Ссылка));
Запись.Вставить("code", Выборка.Код);
Запись.Вставить("name", Выборка.Наименование);
Запись.Вставить("eva_id", Выборка.EvaID);
Запись.Вставить("project_code", Выборка.Кодификатор);
Запись.Вставить("client_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Клиент));
Запись.Вставить("client_name", Выборка.КлиентИмя);
Запись.Вставить("contract_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Договор));
Запись.Вставить("configuration", Строка(Выборка.Конфигурация));
Запись.Вставить("office", Строка(Выборка.Офис));
Запись.Вставить("manager_id", bfd_IntegrationAPIHelpers.UUID(Выборка.РуководительПроекта));
Запись.Вставить("project_manager_id", bfd_IntegrationAPIHelpers.UUID(Выборка.МенеджерПроекта));
Запись.Вставить("status", Строка(Выборка.СтатусПроекта));
Запись.Вставить("contract_status", Строка(Выборка.СтатусДоговора));
Запись.Вставить("start_date", ?(ЗначениеЗаполнено(Выборка.ДатаСтарта), Формат(Выборка.ДатаСтарта, "ДФ=yyyy-MM-dd"), ""));
Запись.Вставить("finish_date", ?(ЗначениеЗаполнено(Выборка.ДатаФиниш), Формат(Выборка.ДатаФиниш, "ДФ=yyyy-MM-dd"), ""));
Запись.Вставить("budget", Выборка.Бюджет);
Запись.Вставить("deleted", Выборка.ПометкаУдаления);
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/stages
//
Функция StagesGet(Запрос) Экспорт
Попытка
Запрос1С = Новый Запрос;
Запрос1С.Текст =
"ВЫБРАТЬ
| ЭтапыПроектов.Ссылка КАК Ссылка,
| ЭтапыПроектов.Код КАК Код,
| ЭтапыПроектов.Наименование КАК Наименование,
| ЭтапыПроектов.Владелец КАК Владелец,
| ЭтапыПроектов.Кодификатор КАК Кодификатор,
| ЭтапыПроектов.ДатаНачала КАК ДатаНачала,
| ЭтапыПроектов.ДатаОкончания КАК ДатаОкончания,
| ЭтапыПроектов.Выполнен КАК Выполнен,
| ЭтапыПроектов.АктПодписан КАК АктПодписан,
| ЭтапыПроектов.ПометкаУдаления КАК ПометкаУдаления
|ИЗ
| Справочник.ЭтапыПроектов КАК ЭтапыПроектов";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("id", bfd_IntegrationAPIHelpers.UUID(Выборка.Ссылка));
Запись.Вставить("code", Выборка.Код);
Запись.Вставить("name", Выборка.Наименование);
Запись.Вставить("project_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Владелец));
Запись.Вставить("project_code",Выборка.Кодификатор);
Запись.Вставить("start_date", ?(ЗначениеЗаполнено(Выборка.ДатаНачала), Формат(Выборка.ДатаНачала, "ДФ=yyyy-MM-dd"), ""));
Запись.Вставить("end_date", ?(ЗначениеЗаполнено(Выборка.ДатаОкончания), Формат(Выборка.ДатаОкончания, "ДФ=yyyy-MM-dd"), ""));
Запись.Вставить("completed", Выборка.Выполнен);
Запись.Вставить("act_signed", Выборка.АктПодписан);
Запись.Вставить("deleted", Выборка.ПометкаУдаления);
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/work_types
//
Функция WorkTypesGet(Запрос) Экспорт
Попытка
Результат = Новый Массив;
МетаОбъект = Метаданные.Перечисления.ВидыРабот;
Для Каждого ЗначениеМД Из МетаОбъект.ЗначенияПеречисления Цикл
Запись = Новый Структура;
Запись.Вставить("code", ЗначениеМД.Имя);
Запись.Вставить("label", ЗначениеМД.Синоним);
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/dictionaries
//
Функция DictionariesGet(Запрос) Экспорт
Попытка
Ответ = Новый Структура;
Ответ.Вставить("offices", ПростойСправочник("Офис"));
Ответ.Вставить("departments", ПростойСправочник("Подразделение"));
Ответ.Вставить("managers", ПростойСправочник("Менеджеры"));
Ответ.Вставить("configurations", ПростойСправочник("Конфигурации"));
Ответ.Вставить("contracts", ПростойСправочник("Договоры"));
Ответ.Вставить("planning_scenarios", ПростойСправочник("СценарииПланирования"));
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Ответ);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/dept_history?modified_since=<ISO_DATE>
//
Функция DeptHistoryGet(Запрос) Экспорт
Попытка
МодифицированоПосле = bfd_IntegrationAPIHelpers.ПарсДатуISO(Запрос.ПараметрыЗапроса.Получить("modified_since"));
Запрос1С = Новый Запрос;
Запрос1С.УстановитьПараметр("МодифицированоПосле", МодифицированоПосле);
Запрос1С.Текст =
"ВЫБРАТЬ
| ИсторияПодразделений.Период КАК Период,
| ИсторияПодразделений.Сотрудник КАК Сотрудник,
| ИсторияПодразделений.Подразделение КАК Подразделение
|ИЗ
| РегистрСведений.ПодразделениеСотрудников КАК ИсторияПодразделений
|ГДЕ
| ИсторияПодразделений.Период >= &МодифицированоПосле";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("period", Формат(Выборка.Период, "ДФ=yyyy-MM-dd"));
Запись.Вставить("employee_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Сотрудник));
Запись.Вставить("department_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Подразделение));
Запись.Вставить("department_name", Строка(Выборка.Подразделение));
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/project_register?modified_since=<ISO_DATE>
// Для MVP-3, на старте можно не использовать.
//
Функция ProjectRegisterGet(Запрос) Экспорт
Попытка
МодифицированоПосле = bfd_IntegrationAPIHelpers.ПарсДатуISO(Запрос.ПараметрыЗапроса.Получить("modified_since"));
Запрос1С = Новый Запрос;
Запрос1С.УстановитьПараметр("МодифицированоПосле", МодифицированоПосле);
Запрос1С.Текст =
"ВЫБРАТЬ
| Регистр.Период КАК Период,
| Регистр.Регистратор КАК Регистратор,
| Регистр.Сценарий КАК Сценарий,
| Регистр.Проект КАК Проект,
| Регистр.ЭтапПроекта КАК ЭтапПроекта,
| Регистр.ВидАналитики КАК ВидАналитики,
| Регистр.Исполнитель КАК Исполнитель,
| Регистр.Сумма КАК Сумма,
| Регистр.СуммаАкт КАК СуммаАкт,
| Регистр.СуммаРасход КАК СуммаРасход
|ИЗ
| РегистрНакопления.ОборотыПроектныхПоказателей_v2 КАК Регистр
|ГДЕ
| Регистр.Период >= &МодифицированоПосле";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("period", Формат(Выборка.Период, "ДФ=yyyy-MM-ddTHH:mm:ss"));
Запись.Вставить("registrator_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Регистратор));
Запись.Вставить("scenario", Строка(Выборка.Сценарий));
Запись.Вставить("project_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Проект));
Запись.Вставить("stage_id", bfd_IntegrationAPIHelpers.UUID(Выборка.ЭтапПроекта));
Запись.Вставить("analytics_type", Строка(Выборка.ВидАналитики));
Запись.Вставить("employee_id", bfd_IntegrationAPIHelpers.UUID(Выборка.Исполнитель));
Запись.Вставить("sum_total", Выборка.Сумма);
Запись.Вставить("sum_acted", Выборка.СуммаАкт);
Запись.Вставить("sum_expense", Выборка.СуммаРасход);
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
// GET /v1/eva_mapping/projects
//
Функция EvaMappingProjectsGet(Запрос) Экспорт
Возврат ОтдатьРегистрМаппинга("СоответствиеПроектовEVA_РА");
КонецФункции
// GET /v1/eva_mapping/clients
//
Функция EvaMappingClientsGet(Запрос) Экспорт
Возврат ОтдатьРегистрМаппинга("СоответствиеКонтрагентовEVA_РА");
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
// Возвращает массив строк ТЧ "Работы" документа.
//
Функция ПолучитьСтрокиРаботы(Знач ДокументСсылка)
Объект = ДокументСсылка.ПолучитьОбъект();
Если Объект = Неопределено Тогда
Возврат Новый Массив;
КонецЕсли;
Строки = Новый Массив;
Для Каждого Стр Из Объект.Работы Цикл
Запись = Новый Структура;
Запись.Вставить("row_index", Стр.НомерСтроки);
Запись.Вставить("description", Стр.СодержаниеРабот);
Запись.Вставить("hours", Стр.КоличествоЧасов);
Запись.Вставить("work_type", Строка(Стр.ВидРаботы));
Запись.Вставить("work_type_code", ?(ЗначениеЗаполнено(Стр.ВидРаботы), XMLСтрока(Стр.ВидРаботы), ""));
Запись.Вставить("client_id", bfd_IntegrationAPIHelpers.UUID(Стр.Клиент));
Запись.Вставить("client_name", Строка(Стр.Клиент));
Запись.Вставить("manager_id", bfd_IntegrationAPIHelpers.UUID(Стр.Менеджер));
Запись.Вставить("project_id", bfd_IntegrationAPIHelpers.UUID(Стр.Проект));
Запись.Вставить("stage_id", bfd_IntegrationAPIHelpers.UUID(Стр.Этап));
Запись.Вставить("request_number", Стр.НомерЗаявки);
Запись.Вставить("lt_id", bfd_IntegrationAPIHelpers.UUID(Стр.ЛТ));
Запись.Вставить("work_done", Стр.РаботаВыполнена);
Строки.Добавить(Запись);
КонецЦикла;
Возврат Строки;
КонецФункции
// Возвращает простой плоский справочник как массив объектов {id, code, name}.
//
Функция ПростойСправочник(Знач ИмяСправочника)
Запрос1С = Новый Запрос;
Запрос1С.Текст =
"ВЫБРАТЬ
| Спр.Ссылка КАК Ссылка,
| Спр.Код КАК Код,
| Спр.Наименование КАК Наименование
|ИЗ
| Справочник." + ИмяСправочника + " КАК Спр
|ГДЕ
| НЕ Спр.ПометкаУдаления";
Выборка = Запрос1С.Выполнить().Выбрать();
Результат = Новый Массив;
Пока Выборка.Следующий() Цикл
Запись = Новый Структура;
Запись.Вставить("id", bfd_IntegrationAPIHelpers.UUID(Выборка.Ссылка));
Запись.Вставить("code", Выборка.Код);
Запись.Вставить("name", Выборка.Наименование);
Результат.Добавить(Запись);
КонецЦикла;
Возврат Результат;
КонецФункции
// Универсальный helper для регистров маппинга EVA_РА.
// Перебирает колонки регистра, ссылки сериализует UUID, даты ISO.
//
Функция ОтдатьРегистрМаппинга(Знач ИмяРегистра)
Попытка
Запрос1С = Новый Запрос;
Запрос1С.Текст = "ВЫБРАТЬ * ИЗ РегистрСведений." + ИмяРегистра;
ТЗ = Запрос1С.Выполнить().Выгрузить();
Результат = Новый Массив;
Для Каждого Стр Из ТЗ Цикл
Запись = Новый Структура;
Для Каждого Колонка Из ТЗ.Колонки Цикл
Значение = Стр[Колонка.Имя];
Если ТипЗнч(Значение) = Тип("Дата") Тогда
Запись.Вставить(Колонка.Имя, Формат(Значение, "ДФ=yyyy-MM-ddTHH:mm:ss"));
ИначеЕсли ТипЗнч(Значение) = Тип("Булево") ИЛИ ТипЗнч(Значение) = Тип("Число") ИЛИ ТипЗнч(Значение) = Тип("Строка") Тогда
Запись.Вставить(Колонка.Имя, Значение);
Иначе
Запись.Вставить(Колонка.Имя, ?(ЗначениеЗаполнено(Значение), bfd_IntegrationAPIHelpers.UUID(Значение), ""));
КонецЕсли;
КонецЦикла;
Результат.Добавить(Запись);
КонецЦикла;
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветJSON(Результат);
Исключение
Возврат bfd_IntegrationAPIHelpers.СформироватьОтветОшибки(ОписаниеОшибки());
КонецПопытки;
КонецФункции
#КонецОбласти