Dynamics 365 FO Rest API
Содержание
Вступление
Что такое XML
Что такое JSON
Получение ключей
Если Вы консультант, то лучше всего попросить ключи у системного архитектора, тим лида проекта, ближайшего разработчика ... и не заниматься их генерацией самостоятельно!
Авторизация дело не хитрое и у него есть свои особенности. Предположим наше приложение живет по какому-то адресу ... например https://devboxa5adevaos.cloudax.dynamics.com/?cmp=USMF&mi=DefaultDashboard - это точка входа в приложение. Значит наш базовый URL = devboxa5adevaos.cloudax.dynamics.com
Для того что бы мы получили доступ к приложению как пользователи - необходимо что бы администратор нас добавил как пользователь и назначил некоторые права в системе.
С точки зрения интеграции все в принципе работает так же. Мы специальным образом подключаем интеграцию к пользователю системы. Пользователю системы даем права на объекты системы. Подробная инструкция тут, а короткое описание ниже ...
Мы должны в Azure portal создать новую регистрацию для нашего приложения. При этом мы получим пару значений : clientId, client secret. Эти значения нужно запомнить!
Переходим в D365FO, System administration\setup\Azure Active Directory application и создаем новую запись
Отступление. Что такое запрос, какие они бывают, что такое Postman
Давайте попробуем разобаться что такое запрос. Опять же, все упрощаем, главное сейчас - уловить суть.
Если мы откроем любую страницу в браузере, это и будет обычный запрос GET. Можно даже посмотреть что это GET и что он отправляет, получает.
Google chrome, правой клавишей на странице в любом месте в сплывающем контекстном меню выбрать Inspect (Ctrl+Shift+I), и переходим на закладку Network
Мы увидим, вот такую картину
Номер 5- показывает запрос GET, куда он был сделан, что он получил ответ сервера (Status code) 200. Сам ответ - на закладке Response
Какие еще могут быть запросы - может быть (взято отсюда)
HTTP Method | CRUD | Entire Collection (e.g. /users) | Specific Item (e.g. /users/123) |
POST | Create | 201 (Created), ‘Location’ header with link to /users/{id} containing new ID. | Avoid using POST on single resource |
GET | Read | 200 (OK), list of users. Use pagination, sorting and filtering to navigate big lists. | 200 (OK), single user. 404 (Not Found), if ID not found or invalid. |
PUT | Update/Replace | 405 (Method not allowed), unless you want to update every resource in the entire collection of resource. | 200 (OK) or 204 (No Content). Use 404 (Not Found), if ID not found or invalid. |
PATCH | Partial Update/Modify | 405 (Method not allowed), unless you want to modify the collection itself. | 200 (OK) or 204 (No Content). Use 404 (Not Found), if ID not found or invalid. |
DELETE | Delete | 405 (Method not allowed), unless you want to delete the whole collection — use with caution. | 200 (OK). 404 (Not Found), if ID not found or invalid. |
Что бы увидеть, как отдает данные D365FO давайте попробуем выполнить такой запрос в адресной строке браузера
https://devboxa5adevaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')
И посмотрим на свойства в окошке Inspect
если данных на закладке Network нет - нажмите F5, если страница не открывается вовсе - выполните авторизацию в D365FO
Мы выполнили Rest API запрос GET к CustomersGroups отобрав данные только по полям DataAreaId = 'usmf' и CustomerGroupId = '10'
К сожалению, возможности в браузере ограничены и нам будет проще в дальнейшем использовать другие инструменты, например CURL, Fiddler, Postman.
Postman
Позволяет выполнять запросы из пользовательского окружения, удобный механизм сохранения и передачи запросов от человека к человеку, есть механизм автоматического тестирования запросов! Итак, шаг за шагом.
Где его взять ... - тут
1. Коллекции - тут создаем для себя папочку с текущим нашим проектом. Здесь будут размещаться наши сохраненные запросы (создали, описали)
2. Запросы - создаются плюсиком - основная рабочая область
3. Окружение - сюда можно будет заносить значения переменных. Об этом позже, но в двух словах, мы можем базовый URL записать в переменную и один и тот же запрос выполнять для разных окружений - в каждом окружении значение этой переменной базовый URL может быть свое ...
Что бы создать запрос - нажимаем + из 2)
1 - можно запросу дать имя
2 - тип запроса GET, POST ...
3 - к чему запрос
4 - заголовки - рассмотрим дальше отдельно. В двух словах - мы говорим сервису что мы от него хотим - например мы ему можем сообщить что хотим получить результат в формате xml или json. Другой пример - здесь задается токен авторизации
5 - тело запроса. Если мы хотим получить данные от сервиса - тело пустое. А если хотим вставить данные - то в этой секции описываем что хотим вставить
6 - отправить запрос на выполнение
Пример запроса, который мы использовали в браузере
Браузер таблиц
Мы можем просмотреть записи таблицы если введем ссыдку в виде
https://BASEURL/?mi=SysTableBrowser&TableName=CustGroup&cmp=USMF&lng=en-us&limitednav=true
где
TableName=Имя Таблицы или представления (View) - CustGroup
cmp - имя компании - USMF
lng - язык - un-us
В результате получим
Список Entity
Table and Entity Chrome extension
Просто необходимый инструмент. Вводится BASEURL, есть возможность получить список всех таблиц, View, Entity с возможностью найти по названию, просмотреть свойства ...
Однако, вернемся к запросам .....
Получение токена авторизации
Авториация производится при помощи протокола OAuth 2.0
Для этого выполняется запрос
POST https://login.windows.net/<tenant>/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: login.windows.net
resource=https%3A%2F%2FBASEURL&client_id=f78fe522-29b3-4acf-a61b-f5581e121496&client_secret=bZGMw91F7SwVyBCuIllJH7umNu2SutM0qOczMPUOzOg%3D&grant_type=client_credentials
где
<tenant> - значение нужно уточнить у технических специалистов либо в D365 знак вопроса, About/в секци This product is licensed to:
BASEURL- значение описано выше
client_id, client_secret - значения которые мы запомнили выше
Запустим этот запрос в Postman - в результате мы получим ответ типа
В крассном квадрате access_token - который будет использован для авторизации. Этот токен который будет использоваться в каждом запросе.
Обратите внимание на использование переменных в секции body. Эти переменные установлены в окружении
Если в запросе на авторизацию на закладке Tests этот код
pm.environment.set("token", pm.response.json().access_token);
и выполнить запрос еще раз - то в переменную token будет записано вот это значение и мы сможем дальше ее использовать в запросах.
Запрос на чтение данных
Чтение данных происходит запросом GET
Запрос к Entity CustomerGroups вернет 9 записей
$top
$top - параметр - вернет первых несколько записей
Например в этом же запросе GET https://{{base_url}}/data/CustomerGroups?$top=1
Вернет только одну запись (первую запись)
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"value": [
{
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": ""
}
]
}
$skip
$skip - параметр - пропустит первых несколько записей
В этом запросе GET https://{{base_url}}/data/CustomerGroups?$top=1&$skip=1
Вернет только вторую запись
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"value": [
{
"@odata.etag": "W/\"Jzc1MDAwNDY4LDIyNTY1NDIxMTc3Jw==\"",
"dataAreaId": "usmf",
"CustomerGroupId": "20",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Major customers new",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "GOODWILL",
"PaymentTermId": "Net30",
"TaxGroupId": ""
}
]
}
$count
Возвращает кол-во записей запроса
В этом виде GET https://{{base_url}}/data/CustomerGroups/$count
Получаем 9
Запрос GET https://{{base_url}}/data/CustomerGroups?$count=true
Вернет
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"@odata.count": 9,
"value": [
{
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": ""
},
{
и т.д. ...
Запрос GET https://{{base_url}}/data/CustomerGroups?$count=true&$top=1
Вернет все равно 9 записей
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"@odata.count": 9,
"value": [
{
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": ""
}
]
}
$select
Указываем список полей для выборки
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups(dataAreaId,CustomerGroupId)",
"value": [
{
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10"
},
{
"@odata.etag": "W/\"Jzc1MDAwNDY4LDIyNTY1NDIxMTc3Jw==\"",
"dataAreaId": "usmf",
"CustomerGroupId": "20"
}
]
}
$filter
фильтруем записи
GET https://{{base_url}}/data/CustomerGroups?$count=true&$filter=PaymentTermId eq 'Net30'
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"@odata.count": 2,
"value": [
{
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": ""
},
{
"@odata.etag": "W/\"Jzc1MDAwNDY4LDIyNTY1NDIxMTc3Jw==\"",
"dataAreaId": "usmf",
"CustomerGroupId": "20",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Major customers new",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "GOODWILL",
"PaymentTermId": "Net30",
"TaxGroupId": ""
}
]
}
Допускается использовать AND, OR и
$orderby
KEY
Возможность выбирать данные по ключу - одну запись. Очень важное свойство в операциях PATCH, PUT, DELETE
GET https://{{base_url}}/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')
Ответ
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups/$entity",
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": ""
}
как узнать ключ записи
Accept: odata.metadata=minimal, odata.metadata=full
Свойство header запроса Accept говорит о том, как мы ожидаем получить данные от сервиса
В случае odata.metadata=minimal - сервис выдаст минимальный набор данных - все запросы до этого момента были выполенены с этим параметром
Как выглядит ответ odata.metadata=full вернет такой ответ
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
"@odata.count": 1,
"value": [
{
"@odata.type": "#Microsoft.Dynamics.DataEntities.CustomerGroup",
"@odata.id": "https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')",
"@odata.etag": "W/\"JzAsMjI1NjU0MjExNzYn\"",
"@odata.editLink": "CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')",
"dataAreaId": "usmf",
"CustomerGroupId": "10",
"ClearingPeriodPaymentTermName": "Net30",
"CustomerAccountNumberSequence": "",
"DefaultDimensionDisplayValue": "",
"Description": "Wholesales customers",
"IsSalesTaxIncludedInPrice@odata.type": "#Microsoft.Dynamics.DataEntities.NoYes",
"IsSalesTaxIncludedInPrice": "No",
"WriteOffReason": "",
"PaymentTermId": "Net30",
"TaxGroupId": "",
"DimensionSet@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')/DimensionSet/$ref",
"DimensionSet@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')/DimensionSet",
"Prospects@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')/Prospects/$ref",
"Prospects@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='10')/Prospects"
}
]
}
где
@odata.type - название Entity
@odata.id - ссылка на получение уникальной текущей записи - в скобках - ключи
@odata.nextLink - ссылка на следующую страницу
Ограничения
Максимальная выборка ограничена только 1000 записями. Если в результирующей выборке записей больше - включается постраничное отображение. Постраничное отображение реализовано при помощи $top и $skip. Дополнительно появляется тег @odata.nextLink со ссылкой на следующий набор записей.
$expand
Позволяет присоединить один набор данных в другому
Возьмем другой набор данных - GET https://{{base_url}}/data/SalesOrderHeadersV2?$top=1
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#SalesOrderHeadersV2/$entity",
"@odata.type": "#Microsoft.Dynamics.DataEntities.SalesOrderHeaderV2",
"@odata.id": "https://adevaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')",
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3NjswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"@odata.editLink": "SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')",
"dataAreaId": "usmf",
"SalesOrderNumber": "000002",
"OrderTotalChargesAmount@odata.type": "#Decimal",
......
"RevRecContractStartDate": "1900-01-01T12:00:00Z",
"SalesOrderOrigin@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/SalesOrderOrigin/$ref",
"SalesOrderOrigin@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/SalesOrderOrigin",
"Project@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/Project/$ref",
"Project@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/Project",
"DimensionSet@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/DimensionSet/$ref",
"DimensionSet@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/DimensionSet",
"SalesOrderLines@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/SalesOrderLines/$ref",
"SalesOrderLines@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/SalesOrderLines",
"QualityOrderHeaders@odata.associationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/QualityOrderHeaders/$ref",
"QualityOrderHeaders@odata.navigationLink": "https://devaos.cloudax.dynamics.com/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')/QualityOrderHeaders"
}
Теперь его расширим за счет expand
Запрос
GET https://{{base_url}}/data/SalesOrderHeadersV2(dataAreaId='usmf',SalesOrderNumber='000002')?$expand=SalesOrderLines
мы можем использовать дополнительные параметры в $expand запросе
GET
https://{{base_url}}/data/SalesOrderHeadersV2?$top=1&$select=SalesOrderNumber&$expand=SalesOrderLines($select=InventoryLotId,ItemNumber,SalesOrderNumber)
вернет
{
"@odata.context": "https://devaos.cloudax.dynamics.com/data/$metadata#SalesOrderHeadersV2(SalesOrderNumber,SalesOrderLines,SalesOrderLines(InventoryLotId,ItemNumber,SalesOrderNumber))",
"value": [
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3NjswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"SalesOrderNumber": "000002",
"SalesOrderLines": [
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3NjswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"InventoryLotId": "000221",
"ItemNumber": "D0001",
"SalesOrderNumber": "000002"
},
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3NzswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"InventoryLotId": "000222",
"ItemNumber": "L0001",
"SalesOrderNumber": "000002"
},
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3ODswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"InventoryLotId": "000223",
"ItemNumber": "P0001",
"SalesOrderNumber": "000002"
},
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU3OTswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"InventoryLotId": "000224",
"ItemNumber": "D0003",
"SalesOrderNumber": "000002"
},
{
"@odata.etag": "W/\"JzAsNTYzNzE0NDU4MDswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
"InventoryLotId": "000225",
"ItemNumber": "D0004",
"SalesOrderNumber": "000002"
}
]
}
]
}