# Сборка обработки «с нуля» через XML-выгрузку + платформу (предпочтительный путь)

Когда нужен **свой набор колонок ТЧ** (особенно если их больше, чем в любом готовом
v8unpack-шаблоне) или вообще новая обработка — НЕ мучай v8unpack (добавление колонок
там непрактично, см. `object_metadata.md`). Авторь **XML-выгрузку Конфигуратора**
(человекочитаемый XML) и собирай **настоящей платформой** через `onec-build-agent`.
Платформа = ground truth: HTTP 200 ⇒ синтаксис BSL, типы, форма валидны.

Выверено: `ИсправлениеСчетаРасчетовАгентскихУслуг` (11.06.2026), образец структуры —
`аСверкаРеестровыхКомпаний`. Готовый генератор — `examples/gen_xml_epf_from_scratch.py`
(копируй и правь: UUIDs, реквизиты, ТЧ, форма, модуль).

## Структура XML-выгрузки (формат DumpExternalDataProcessorToFiles)

```
<Имя>.xml                              ← корень: MetaDataObject>ExternalDataProcessor
<Имя>/Forms/Форма.xml                  ← дескриптор формы (Name, FormType=Managed)
<Имя>/Forms/Форма/Ext/Form.xml         ← РАЗМЕТКА формы (ChildItems/Table/Attributes/Commands)
<Имя>/Forms/Форма/Ext/Form/Module.bsl  ← модуль формы (вся логика)
<Имя>/Ext/ObjectModule.bsl             ← модуль объекта (часто почти пустой)
```

**Корневой XML** (namespace-шапка `NS_MD` — копируй из `аСверкаРеестровыхКомпаний.xml`
дословно): `<ExternalDataProcessor uuid>` → `InternalInfo` (ContainedObject ClassId
**фиксирован** `c3831ec8-d8d5-4f93-8a22-f9bfae07327f`, ObjectId новый; GeneratedType
Object с новыми TypeId/ValueId) → `Properties` (Name, Synonym, `DefaultForm =
ExternalDataProcessor.<Имя>.Form.Форма`) → `ChildObjects` (Attribute… объекта,
TabularSection… с GeneratedType TS+TSRow, `<Form>Форма</Form>`).

- **Реквизит объекта**: блок `<Attribute uuid><Properties>…</Properties></Attribute>`
  с полным набором тэгов (PasswordMode, ChoiceFoldersAndItems, … — копируй болванку).
- **Реквизит ТЧ**: то же + `<FillFromFillingValue>` и `<FillValue>`.
- **Типы** (`<Type>`): Date=`xs:dateTime`+DateQualifiers(Date); Boolean=`xs:boolean`;
  ссылка=`cfg:CatalogRef.X` / `cfg:DocumentRef.X` / `cfg:ChartOfAccountsRef.X` /
  `cfg:EnumRef.X`; число=`xs:decimal`+NumberQualifiers; строка=`xs:string`+StringQualifiers.

**Форма** (`Ext/Form.xml`, namespace `NS_FORM`): `AutoCommandBar id=-1` → `Events`
(OnCreateAtServer→ПриСозданииНаСервере) → `ChildItems` (группы, InputField'ы фильтров,
`Table` с DataPath `Объект.<ТЧ>`, его `AutoCommandBar` с `Button`/CommandName=
`Form.Command.<имя>`, колонки = InputField с DataPath `Объект.<ТЧ>.<реквизит>`) →
`Attributes` (главный `Объект` тип `cfg:ExternalDataProcessorObject.<Имя>` +
`MainAttribute=true` + Save-поля; доп. реквизиты формы — фильтры-СписокЗначений:
Type `v8:ValueListType` + `Settings xsi:type="v8:TypeDescription"`) → `Commands`
(name/id/Title/Action).

- **Колонка-чекбокс**: обычный InputField на Boolean-реквизит (рисуется галкой).
- **ВСЕ колонки — РЕДАКТИРУЕМЫЕ по умолчанию** (устойчивое правило юзера, см.
  память `onec-epf-fields-editable`): НЕ ставь `<ReadOnly>true</ReadOnly>` на колонки,
  пока юзер явно не попросит. В болванке `col(...)` дефолт уже `readonly=False`.
- **Цвет кнопки**: `<BackColor>#RRGGBB</BackColor>` внутри `<Button>`. ✅ **Round-trip
  работает** — платформа хранит цвет как BGR-упакованное число (напр. `#70AD47`→
  `4697456`, `#FFE100`→`57855`). Не ищи hex-литерал в v8unpack — ищи BGR-int.
- **Кнопка-картинка**: `<Representation>Picture</Representation>` + `<Picture><xr:Ref>StdPicture.X</xr:Ref></Picture>`
  внутри `<Button>` (порядок: Type, Representation, CommandName, Picture). Валидные
  StdPicture (из обработок юзера): `AppearanceCheckIcon` (галочка → Поставить метку),
  `AppearanceCircleEmpty` (пустой → Снять метку), `InputFieldSelect`, `RestoreValues`, `Today`, `Print`, `Close`.
  Проверка в v8unpack: имя StdPicture НЕ хранится литералом — смотри слот Representation
  узла кнопки `'31'`: `b[10]=="1"` → Picture, `"3"` → текст/Авто. Невалидный StdPicture → build 422.

## Сборка через агент

```bash
TOKEN=$(cat ~/Services/onec-build-agent/.token)
# zip ТОЛЬКО питоном (CLI zip портит кириллицу). В zip: ровно один корневой Имя.xml + Имя/
python3 -c "import zipfile,os; r='build'; z=zipfile.ZipFile('src.zip','w',zipfile.ZIP_DEFLATED); [z.write(os.path.join(d,f),os.path.relpath(os.path.join(d,f),r)) for d,_,fs in os.walk(r) for f in fs]; z.close()"
# uid → персональная база с конфигурацией юзера (нужны .ready). Без uid — пустая база (ссылки CatalogRef.* не разрешатся!)
curl -s -X POST -H "Authorization: Bearer $TOKEN" --data-binary @src.zip \
  -o out.epf -w "%{http_code}\n" "http://127.0.0.1:8788/build?uid=1005954706"
```

- **uid обязателен** для обработок со ссылками на типы конфигурации; база
  `ib_u<uid>` должна иметь флаг `.ready` (промоутится при `/dump-config?uid=`).
- HTTP **200** → тело = готовый `.epf`. **422** → JSON с полем `log` Конфигуратора
  (читай ошибку, правь XML/BSL, пересобирай). Итерируй по log.

## Проверка результата

`epf.sh unpack out.epf v/` → `obj_edit.py attrs` (типы колонок) →
`form_cmd.py list` (команды) → `epf.sh roundtrip` (чистый). Платформенная сборка
уже гарантирует валидность; структурные тулы — для глаз. **Функционально (какие
документы вернёт запрос, запись/перепроведение) проверить НЕЛЬЗЯ** — в базе только
конфигурация, без данных; это делает юзер на боевых данных.

## Кнопки стандартных команд (сортировка и т.п.) — закрепление на видимой панели

По умолчанию стандартные команды таблицы (сортировка по возрастанию/убыванию, и т.д.)
доступны только через «Ещё» (overflow), даже если пользователь ожидает их на панели.
Чтобы закрепить — добавь `<Button>` в `AutoCommandBar` таблицы с явным
`<LocationInCommandBar>InCommandBar</LocationInCommandBar>` (без этого тега платформа
всё равно прячет команду в overflow, ДАЖЕ если кнопка явно добавлена):

```xml
<Button name="аДокументыСортироватьПоВозрастанию" id="31">
    <Type>CommandBarButton</Type>
    <Representation>Picture</Representation>
    <CommandName>Form.Item.аДокументы.StandardCommand.SortListAsc</CommandName>
    <LocationInCommandBar>InCommandBar</LocationInCommandBar>
</Button>
<Button name="аДокументыСортироватьПоУбыванию" id="32">
    <Type>CommandBarButton</Type>
    <Representation>Picture</Representation>
    <CommandName>Form.Item.аДокументы.StandardCommand.SortListDesc</CommandName>
    <LocationInCommandBar>InCommandBar</LocationInCommandBar>
</Button>
```

`CommandName` = `Form.Item.<ИмяТаблицы>.StandardCommand.<Команда>` (НЕ просто
`StandardCommand.X` — нужен префикс `Form.Item.<Таблица>.`, иначе платформа не
резолвит команду для конкретного элемента). Picture НЕ указывается — платформа даёт
дефолтную иконку для известных стандартных команд. Полный список доступных
`StandardCommand.*` для конфигурации — `grep -rho "StandardCommand\.[A-Za-zА-Яа-я]*"
НоваяКонфа/ | sort -u`. Рабочий образец с сортировкой —
`ПисьмаОтелям_/Форма_ЗакрывающиеДокументы` (таблица «Отели»).

## Грабли

- **Кавычки внутри строкового литерала текста запроса ломают модуль без предупреждения от
  build-agent.** Текст запроса в BSL обычно оформлен как `"ВЫБРАТЬ\n\t|...\n\t|";` — единый
  строковый литерал со строками через `|`. Любая одиночная `"` внутри него (например, в
  поясняющем `//`-комментарии САМОГО текста запроса или в значении вроде `Значение = "конс"`)
  ОБЯЗАНА быть удвоена (`""`) — иначе она преждевременно закрывает литерал, и всё, что после
  неё, парсится как голый BSL-код → `Ожидается ключевое слово 'КонецФункции'` при попытке
  ПОДКЛЮЧИТЬ обработку в реальном клиенте. `POST /build` (через
  `DESIGNER /LoadExternalDataProcessorOrReportFromFiles`) этого НЕ ловит — он упаковывает XML в
  `.epf`, но не компилирует модуль; `epf.sh roundtrip` тоже не ловит — он сверяет
  байты/JSON, не компилирует BSL. HTTP 200 + чистый roundtrip НЕ доказывают синтаксическую
  валидность содержимого строковых литералов. Перед сдачей — `grep '"' file.bsl | grep -v '""'`
  по всему файлу (должны остаться только обычные короткие BSL-строки вида `"Текст"`, у которых
  парная кавычка на той же строке) и сверка баланса `Функция/КонецФункции`,
  `Процедура/КонецПроцедуры`. Для пояснительного текста внутри запроса безопаснее вообще не
  использовать `"`, а брать кавычки-ёлочки «».
- ClassId ContainedObject — **фиксированная** константа, не выдумывай.
- `<Form>Форма</Form>` в ChildObjects корня обязателен, иначе форма «потеряется».
- Имя/синоним в XML-выгрузке — обычные тэги `<Name>`/`<Synonym>` (НЕ как в v8unpack,
  где они в бинарном header). Имя=синоним — просто пиши одинаковые значения.
- Кнопки/команды формы, собранной платформой, в v8unpack кодируются иначе, чем
  у форм, прежде распакованных из готового .epf → `form_cmd.py rebind` на них
  ненадёжен. Источник истины для платформенной формы — её XML, а не v8unpack.

## Зависимый выбор в отборе (договор по контрагенту) — `ChoiceParameterLinks`

Чтобы поле отбора показывало только значения, подчинённые другому полю
(напр. «Договор» — только договоры выбранного контрагента; договор подчинён
контрагенту через **Владелец**), добавь в `InputField` декларативную связь —
БЕЗ кода в модуле:

```xml
<InputField name="ДоговорыКонтрагентов" id="9">
    <DataPath>ДоговорыКонтрагентов</DataPath>
    <ChoiceParameterLinks>
        <xr:Link>
            <xr:Name>Отбор.Владелец</xr:Name>                          <!-- Отбор.<реквизит-владелец справочника> -->
            <xr:DataPath xsi:type="xs:string">Контрагенты</xr:DataPath>  <!-- поле-источник (можно ValueListType!) -->
            <xr:ValueChange>Clear</xr:ValueChange>                       <!-- Clear|DontChange при смене источника -->
        </xr:Link>
    </ChoiceParameterLinks>
    ...
</InputField>
```

- `xr:Name` = `Отбор.<ИмяРеквизитаПодчинения>` (для подчинённого справочника — обычно `Отбор.Владелец`).
- `xr:DataPath` = имя поля-источника. **Может быть многозначным** (`v8:ValueListType`):
  пустой список → фильтр не применяется (видны все); непустой → «значение В (списке)».
  Одиночная ссылка → точный фильтр по ней.
- Эталон в коллекции юзера: `ОтчетПоДаннымЗаказа` (`ОтборДоговорКонтрагента` → `Отбор.Владелец` на `Объект.ОтборКонтрагент`).
  Применено в `аСверкаРеестровыхКомпаний` (список контрагентов → договоры всех выбранных). Namespace `xr` уже есть в корне `<Form>`.

## Добавить форму/ТЧ в СУЩЕСТВУЮЩУЮ .epf (через XML + платформа) — НАДЁЖНЫЙ путь

Когда в готовую обработку надо добавить **вторую форму, ТЧ объекта, метод** — НЕ править v8unpack
вслепую (регистрация форм там непрозрачна, можно сделать неоткрываемый .epf). Вместо этого:

1. **Выгрузить .epf → XML** настоящей платформой (нужна база с конфигурацией юзера, т.к. ссылки на типы):
   ```
   V8=/Applications/1cv8.localized/8.3.27.1859/1cv8.app/Contents/MacOS/1cv8
   IB=~/Services/onec-build-agent/ib_u1005954706     # база С конфигурацией (.ready)! не пустая ib
   "$V8" DESIGNER /F "$IB" /DisableStartupDialogs \
     /DumpExternalDataProcessorOrReportToFiles /tmp/out_xml "<путь.epf>" /Out /tmp/dump.log
   # лаунчер детачится → ждать пока pgrep -f /tmp/out_xml не пуст
   ```
   Даёт `Имя.xml` (объект: ChildObjects с `<Attribute>/<TabularSection>/<Form>Имя</Form>`) +
   `Имя/Forms/Ф.xml` (+ `Ф/Ext/Form.xml` layout + `Ф/Ext/Form/Module.bsl`) + `Ext/ObjectModule.bsl`.

2. **Править XML** (человекочитаемо): ТЧ клонировать из соседней (свежие uuid: TabularSection uuid +
   4 GeneratedType TypeId/ValueId), колонки `<Attribute>` с `<v8:Type>cfg:DocumentRef.X</v8:Type>`;
   форму зарегистрировать строкой `<Form>ИмяФормы</Form>` в ChildObjects объекта; саму форму — 2 файла
   (метаданные `Forms/Имя.xml` + layout `Forms/Имя/Ext/Form.xml`); метод объекта — в `Ext/ObjectModule.bsl`.
   Кнопка: `<Command>` в `<Commands>` + `<Button><CommandName>Form.Command.X</CommandName>` в нужном
   `<AutoCommandBar>`. Обработчик — в `Forms/Форма/Ext/Form/Module.bsl`.

3. **Собрать обратно** через onec-build-agent с тем же uid (платформа = валидатор, 422+log если что):
   ```
   curl -s -X POST -H "Authorization: Bearer $(cat ~/Services/onec-build-agent/.token)" \
     --data-binary @src.zip -o out.epf "http://127.0.0.1:8788/build?uid=1005954706"
   ```
   zip — ТОЛЬКО питоном (кириллица). HTTP 200 = валидно. Итерировать по `log` из 422.

⚠ База `ib_u<uid>` (с конфигурацией) — обязательна и для выгрузки, и для сборки обработок со ссылками
на типы конфигурации; пустая `ib` их не знает. Маркер готовности — `ib_u<uid>/.ready`.
Паттерн «форма-список по строке» (кнопка → ОткрытьФорму(…, Парам, ЭтаФорма, …) → ПриСозданииНаСервере →
РеквизитФормыВЗначение("Объект").Метод(Параметры) → Объект.ТЧ.Загрузить) — эталон `аСверкаРеестровыхКомпаний`.
Выверено: добавление `ФормаСписокРеализаций`+ТЧ`СписокРеализаций`+метод в `УстановкаЮрЛицаИДоговораГостиницы`.

### ⚠ Многозначный отбор (СписокЗначений / `ValueListType`) — `ChoiceParameterLinks` НЕ хватает

`ChoiceParameterLinks` фильтрует выбор корректно только когда источник — **одиночная**
ссылка (как `ОтчетПоДаннымЗаказа`: `Объект.ОтборКонтрагент` → `Отбор.Владелец`). Если
поле-источник — **список** (`v8:ValueListType`, множественный отбор), а у справочника
СВОЯ форма списка по умолчанию (`DefaultListForm`), связь НЕ разворачивает список в
фильтр «в списке» → выбор показывает все значения. Решение — программный `НачалоВыбора`:

```xml
<!-- в InputField поля-приёмника: -->
<Events><Event name="StartChoice">ДоговорыКонтрагентовНачалоВыбора</Event></Events>
```
```bsl
&НаКлиенте
Процедура ДоговорыКонтрагентовНачалоВыбора(Элемент, ДанныеВыбора, СтандартнаяОбработка)
    Массив = Новый Массив;
    Если ТипЗнч(Контрагенты) = Тип("СписокЗначений") Тогда
        Для Каждого Эл Из Контрагенты Цикл
            Если ЗначениеЗаполнено(Эл.Значение) И Массив.Найти(Эл.Значение) = Неопределено Тогда
                Массив.Добавить(Эл.Значение);
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
    Если Массив.Количество() = 0 Тогда Возврат; КонецЕсли;  // пусто → стандартный выбор (все)
    СтандартнаяОбработка = Ложь;
    ПФ = Новый Структура("РежимВыбора, ЗакрыватьПриВыборе, МножественныйВыбор", Истина, Ложь, Истина);
    ПФ.Вставить("Отбор", Новый Структура("Владелец", Массив));  // массив → «В списке»
    ОткрытьФорму("Справочник.ДоговорыКонтрагентов.ФормаВыбора", ПФ, Элемент);  // .ФормаВыбора (системная) уважает Отбор; кастомная .ФормаСписка — нет
КонецПроцедуры
```

Ключевое: открывать `.ФормаВыбора` (системная форма выбора), НЕ кастомную `.ФормаСписка`
(она игнорирует входящий `Отбор`). Кейс: `аСверкаРеестровыхКомпаний` (договор по списку контрагентов).
