Подводные камни и рецепты 1С ODATA
Как пользоваться разделом
Каждый пункт сформулирован как симптом → причина → что делать. Внизу — рецепты для типовых сценариев. Если ищешь конкретный код ошибки 1С — см. endpoints.md §3.
Содержание
- Quirks: query-параметры
- Quirks:
$filterи функции - Quirks: форматы и метаданные
- Quirks: имена, суффиксы, кодировка
- Quirks: мутации (POST/PATCH/PUT/DELETE)
- Quirks: права доступа и
allowedOnly - Рецепты типовых сценариев
1. Quirks: query-параметры
1.1. $inlinecount=allpages гасит $top и $skip
Симптом. Запрос ?$top=20&$skip=40&$inlinecount=allpages — в ответе все записи и поле odata.count. Пагинация не сработала.
Причина. 1С (как и спецификация OData v3) трактует $inlinecount как «вернуть всё + общее число»; $top/$skip при этом игнорируются.
Что делать. Считать и листать разными запросами:
GET /Catalog_X/$count?$filter=...→ число (text/plain).GET /Catalog_X?$filter=...&$top=20&$skip=40&$orderby=...→ страница.
1.2. $expand не работает на одиночной сущности
Симптом. GET /Catalog_X(guid'...')?$expand=Контрагент — параметр молча игнорируется, навигационное свойство не разворачивается.
Причина. reference §17.4.6.4: $expand запрещён на single-entity, на табличных частях и на одиночных записях регистров.
Что делать. Либо через коллекцию ?$filter=Ref_Key eq guid'...'&$expand=...&$top=1, либо отдельным GET после чтения _Key.
1.3. $expand на составном реквизите требует явного типа
Симптом. $expand=Договор/* — ошибка 14 или пустой результат.
Причина. Составной реквизит может ссылаться на разные типы; OData не знает, какой из них раскрывать.
Что делать. Указать тип явно: $expand=Договор/Document_ДоговорНайма/*. Развёрнутое значение придёт в свойство с суффиксом _Expanded.
1.4. $expand на виртуальной таблице — отклонение от OData v3
Симптом. Balance?$expand=Контрагент — поле возвращается, но не туда, куда ждёт стандартный OData-клиент.
Причина. Для измерений 1С кладёт развёрнутый объект рядом с _Key (Контрагент_Key + Контрагент); для составных типов — в свойство с суффиксом _Expanded. Это не стандарт.
Что делать. Парсер клиента должен знать про _Expanded и про возможность «измерение + развёрнутый объект» рядом.
1.5. allowedOnly работает только на коллекциях
Симптом. Добавил ?allowedOnly=true на single-entity или на POST/PATCH — игнорируется, ловишь 401 или код 20.
Причина. Параметр применим только к GET коллекций (entity-set).
Что делать. На одиночных эндпоинтах либо иметь права на конкретный объект, либо предварительно отфильтровать через коллекцию.
1.6. ____Presentation не попадает в * и **
Симптом. ?$select=* или ** — поля ____Presentation отсутствуют.
Причина. reference §17.4.3: представления не входят в */** по умолчанию.
Что делать. Перечислить явно ($select=*, Контрагент____Presentation) или взять представления всех реквизитов ($select=*, *____Presentation).
2. Quirks: $filter и функции
2.1. Сравнение составного реквизита с guid возвращает пусто
Симптом. $filter=Документ eq guid'...' — результат всегда пуст, даже когда совпадение есть.
Причина. Составной реквизит нельзя сравнивать с guid напрямую — нужен cast().
Что делать:
$filter=Документ eq cast(guid'...', 'Document_ПриходнаяНакладная')2.2. Не работают length, indexof, replace, tolower, toupper, trim
Симптом. Ошибка 21 на этих стандартных OData-функциях.
Причина. 1С их не реализовала.
Что делать. Заменять на like(...), startswith(...), endswith(...), substring(...), substringof(...). Сравнение без учёта регистра — на клиенте или через like с шаблонами.
2.3. Не работают years, days, hours, seconds, floor, ceiling
Симптом. Ошибка 21.
Причина. 1С реализовала единственные year/month/day/hour/minute/second/quarter/dayofweek/dayofyear, плюс dateadd/datedifference. Множественных аналогов и floor/ceiling нет.
Что делать. Использовать только реализованные имена. Разности дат — через datedifference(d1, d2, 'day').
2.4. Лямбда без any / all не работает
Симптом. Хочу отфильтровать по реквизиту строки табличной части — $filter=Товары/Цена gt 10000 → ошибка 21.
Причина. Коллекции (табличные части) фильтруются только через any/all с лямбда-переменной.
Что делать:
$filter=Товары/any(d: d/Цена gt 10000)2.5. Condition= в виртуальной таблице — $filter в одинарных кавычках
Симптом. Ошибка 14 при попытке передать Condition=Валюта_Key eq guid'...' без обёртки.
Причина. Значение Condition — строка, поэтому требует одинарных кавычек снаружи, а guid-литералы внутри экранируются удвоением кавычек.
Что делать:
.../SliceLast(Period=datetime'2024-01-01T00:00:00',Condition='Валюта_Key eq guid''aaa-bbb-...''')2.6. Цепочка cast() для отбора через составной реквизит
Симптом. Хочу отфильтровать МаршрутныйЛист по складу из накладной — реквизит ОснованиеОтгрузки составной (Накладная | ВнутреннееПеремещение), а МестоОтгрузки накладной — тоже составной (Склад | АдресаОтгрузки).
Что делать:
cast(cast(ОснованиеОтгрузки, 'Document_Накладная')/МестоОтгрузки, 'Catalog_Склад')/Название eq 'Основной'
3. Quirks: форматы и метаданные
3.1. $format=json игнорируется на $metadata
Причина. CSDL — только XML. Управлять детализацией json-ответов сущностей (не метаданных) можно через $format=application/json;odata=<level>, но это про данные.
Что делать. Парсить $metadata только как XML.
3.2. $format=json игнорируется на SelectChanges
Причина. План обмена возвращает atom-feed + <at:deleted-entry> (RFC 6721). JSON-варианта не предусмотрено.
Что делать. Держать отдельный atom-парсер для SelectChanges.
3.3. odata=nometadata ломает последующий PATCH
Симптом. Взял ответ как есть (с nometadata) и попытался отправить его PATCH'ем обратно — ошибка 4 или 16.
Причина. При nometadata пропадает @odata.type у коллекций (табличные части, RecordSet), и при последующем PATCH сервер не знает, во что разворачивать массив.
Что делать. Для read-modify-PATCH использовать минимум odata=minimalmetadata (значение по умолчанию) или fullmetadata.
4. Quirks: имена, суффиксы, кодировка
4.1. Кириллица в URL должна быть percent-encoded
Симптом. Ручная сборка URL с Catalog_Товары без кодирования — на некоторых прокси/балансировщиках 400.
Причина. В URI допустим только ASCII; всё остальное — percent-encoded байты.
Что делать. httpx / requests кодируют автоматически. При ручной сборке — urllib.parse.quote.
4.2. Одинарная кавычка в строке — удвоением
Причина. В OData v3 нет backslash-экранирования; одинарная кавычка внутри литерала строки записывается как ''.
Что делать. 'O''Brien', 'это ''цитата'''.
4.3. Стандартные реквизиты в dot-traversal — только английские
Симптом. /Document_X(guid'...')/Контрагент/Наименование — ошибка 10.
Причина. В dot-path для стандартных реквизитов 1С использует английские имена.
Что делать. Description, Code, Ref_Key, DataVersion, Posted, Date, Number, DeletionMark, IsFolder, Parent.
4.4. В PUT ссылки через @odata.bind, в POST — через _Key
Причина. Разные контракты: POST создаёт по плоским полям, PUT перезаписывает целиком и требует навигационных ссылок.
Что делать.
- POST:
"Контрагент_Key": "<guid>". - PUT:
"Контрагент@odata.bind": "Catalog_Контрагенты(guid'<guid>')". Можно полный URI или короткий путь послеstandard.odata.
4.5. Составной реквизит при записи — пара значение + _Type
Симптом. POST/PATCH с составным реквизитом — поле сохраняется как «Неопределено».
Причина. Для составного типа сервер ждёт значение и диспетчеризационное свойство <Реквизит>_Type со строкой типа ("StandardODATA.Catalog_Контрагенты").
Что делать. Всегда писать пару. Для «Неопределено»: _Type = "StandardODATA.Undefined", само значение игнорируется.
4.6. Ref_Key против Контрагент_Key
Ref_Key — ссылка на саму сущность (первичный ключ). <Реквизит>_Key — суффикс свойства, содержащего значение ссылки на связанную сущность. В $filter для отбора по связанному реквизиту использовать Контрагент_Key eq guid'...' (а не Контрагент eq guid'...').
5. Quirks: мутации (POST/PATCH/PUT/DELETE)
5.1. Прямой POST на табличную часть запрещён
Симптом. POST /Document_X_Товары с телом строки — ошибка 13.
Что делать. PATCH родителя с полной перезаписью массива Товары.
5.2. Табличная часть пишется целиком
Причина. OData v3 не поддерживает merge коллекций по ключу строки; 1С не реализовала ничего сверху.
Что делать. Прочитать родителя → локально модифицировать массив → PATCH с полным массивом + аннотацией <TP>@odata.type. См. endpoints.md §8.3.
5.3. Регистр бухгалтерии — только read-modify-PATCH
Причина. Записи регистра бухгалтерии — набор по регистратору; OData не даёт писать запись по отдельности.
Что делать. GET → модификация RecordSet → PATCH с обязательной "шапкой" (odata.type, Recorder_Key, RecordSet@odata.type). См. endpoints.md §8.4.
5.4. Posted через PATCH не работает
Причина. Проведение — действие, а не флаг.
Что делать. POST .../Post?PostingModeOperational=false. Для отмены — POST .../Unpost.
5.5. DELETE удаляет физически
Симптом. Ожидал помеченный объект — на самом деле его нет в базе.
Что делать. Для пометки — PATCH {"DeletionMark": true}. DELETE — только когда нужно физическое удаление.
5.6. If-Match: <DataVersion> хранить байт-в-байт
Причина. DataVersion — opaque-строка; пересоздавать, нормализовать, обрезать пробелы — нельзя.
Что делать. Хранить как получено. При 412 — перечитать сущность и повторить с новым значением.
5.7. 1C_OData-DataLoadMode: true — только для миграции
Причина. Заголовок отключает проверки прикладного решения: валидации, автозаполнения, движения регистров.
Что делать. Не использовать в обычных операциях. Только при синхронизации/миграции, когда данные заведомо целостны. Не все объекты поддерживают режим — попытка вернёт код 17.
5.8. POST на сущность вместо коллекции → код 19
Причина. POST принимает только entity-set URL. POST на single-entity бессмыслен — для обновления есть PATCH/PUT.
Что делать. Создавать — POST B/Catalog_X (без ключа). Обновлять — PATCH B/Catalog_X(guid'...').
6. Quirks: права доступа и allowedOnly
6.1. Без allowedOnly=true запрос может упасть в 401
Причина. При выборке, попадающей под ограничение доступа к данным, 1С возвращает 401 (внутренний код 20). Это не аутентификация — это RLS.
Что делать. Для GET коллекций — добавлять ?allowedOnly=true. Для одиночных — иметь права на конкретный объект или предварительно отфильтровать.
6.2. allowedOnly=false + явный фильтр под разрешённые тоже работает
Симптом. allowedOnly=false, но запрос проходит — почему?
Причина. Если явный $filter оставляет в выборке только разрешённые данные, 401 не возникает.
Что делать. Можно так, но для устойчивости проще всегда ставить allowedOnly=true на коллекциях.
7. Рецепты типовых сценариев
7.1. Постраничный список + общее число
GET B/Catalog_Товары/$count?$filter=Цена gt 500
→ 19
GET B/Catalog_Товары?$filter=Цена gt 500&$top=20&$skip=0&$orderby=Description&allowedOnly=true
→ страница 1
Не использовать $inlinecount=allpages совместно с $top/$skip (см. 1.1).
7.2. Чтение составного реквизита
В ответе сущности с составным реквизитом Документ:
Документ— значение (guid или пустое, в зависимости от типа).Документ_Type— строка вида"StandardODATA.Document_ПриходнаяНакладная"или"StandardODATA.Undefined".
Чтобы получить связанный объект — прочитать _Type, затем второй GET по соответствующему эндпоинту. Inline-$expand требует явного типа в пути.
7.3. Запись табличной части
PATCH B/Catalog_Магазины(guid'...')?$format=json
Content-Type: application/json
{
"ТорговыеЗалы@odata.type": "Collection(StandardODATA.Catalog_Магазины_ТорговыеЗалы_RowType)",
"ТорговыеЗалы": [
{"LineNumber": 1, "Название": "Синий", "Площадь": 56},
{"LineNumber": 2, "Название": "Красный", "Площадь": 64}
]
}
Полный массив, даже если меняется одна строка.
7.4. Запись набора регистра бухгалтерии
# 1. Read
GET B/AccountingRegister_X(guid'<recorder>')?$format=json
# 2. Modify RecordSet locally
# 3. Write
PATCH B/AccountingRegister_X(guid'<recorder>')?$format=json
{
"odata.type": "StandardODATA.AccountingRegister_X_RowType",
"Recorder_Key": "<recorder>",
"RecordSet@odata.type": "Collection(StandardODATA.AccountingRegister_X_RowType)",
"RecordSet": [ /* строки */ ]
}7.5. Optimistic locking
# Read
GET B/Catalog_X(guid'...')?$format=json
→ {... "DataVersion": "AAAAA...==", ...}
# Update with check
PATCH B/Catalog_X(guid'...')?$format=json
If-Match: AAAAA...==
{...}
# 412 → перечитать, повторить7.6. Фильтр по составной ссылке
$filter=ДокументОснование eq cast(guid'aaa-bbb', 'Document_РасходТовара')
Проверка только типа (без значения):
$filter=isof(ДокументОснование, 'Document_РасходТовара')7.7. Проведение документа
POST B/Document_РасходТовара(guid'...')/Post?PostingModeOperational=false
Тела нет. В ответе — обновлённый документ с Posted: true. Для отмены — POST .../Unpost без параметров.
7.8. Получение изменений из плана обмена
POST B/SelectChanges?DataExchangePoint='B/ExchangePlan_X(guid'<node>')'&MessageNo=42
Парсить как atom-feed: <entry> — новые/изменённые элементы, <at:deleted-entry> — удалённые (RFC 6721). После обработки — POST B/NotifyChangesReceived?... с тем же MessageNo.
7.9. Срез последних по подчинённому регистру сведений
GET B/InformationRegister_КурсыВалют_RecordType/SliceLast?Period=datetime'2024-01-01T00:00:00',Condition='Валюта_Key eq guid''aaa-bbb-...'''&$format=json
Обратить внимание: для подчинённого регистра функция вызывается на _RecordType, не на самом регистре.
7.10. Пометка на удаление vs физическое удаление
# Пометка
PATCH B/Catalog_X(guid'...')?$format=json
{"DeletionMark": true}
# Физическое удаление
DELETE B/Catalog_X(guid'...')
If-Match: <DataVersion>
DELETE не помечает — удаляет сразу. Снять пометку — PATCH {"DeletionMark": false}.
См. также
- Типы эндпоинтов — какое семейство какие quirks несёт.
- Префиксы объектов — quirks, привязанные к конкретному префиксу.
- endpoints.md §3 — индекс кодов ошибок 1С.
- reference.md §17.4.5.2 — полный синтаксис
$filter.