вторник, 23 июня 2020 г.

Simple Rest API in D365FO, D365F, D365SCM

Dynamics 365 FO Rest API

Содержание

Вступление

Данная статья поможет разобраться в базовых различиях в техниках ти технологиях одного из механизмов обмена данными в современной Аксапте. Многие моменты упрощены для более легкого понимания. Все точные формулировки, вызовы и спецификации, сложные примеры использования есть в открытом доступе. 

Практически все материалы показаны в SMART TALKS 203

Что такое XML


XML это текстовый формат представления данных. У него есть заголовок, и произвольный набор тегов. Теги должны быть парными. Внутри тегов может быть представлены различные типы данных как простые (текст, число, дата), так и более сложных - например список элементов.

XML выглядит приблизительно так

<?xml version="1.0" encoding="UTF-8"?>
<SalesOrder>
    <OrderId>"SO-00023"</OrderId>
    <CustomerId>"Very impotant customer"</CustomerId>
    <OrderLines>
        <SalesOrderLine>
            <OrderId>"SO-00023"</OrderId>
            <ItemId>"It00123-5"</ItemId>
            <Qty>5</Qty>
            <Price>10</Price>
            <Amount>50</Amount>
        </SalesOrderLine>
        <SalesOrderLine>
            <OrderId>"SO-00023"</OrderId>
            <ItemId>"It00123-6"</ItemId>
            <Qty>1</Qty>
            <Price>15</Price>
            <Amount>15</Amount>
        </SalesOrderLine>
    </OrderLines>
</SalesOrder>

Что такое JSON


Так же как и XML - текстовый формат для передачи данных, очень похож на XML только нотация чуть-чуть другая. Рассмотрим как выглядят эти же данные в формате JSON

{
    "SalesOrder": {
      "OrderId""SO-00023",
      "CustomerId""Very impotant customer",
      "OrderLines": {
        "SalesOrderLine": [
          {
            "OrderId""SO-00023",
            "ItemId""It00123-5",
            "Qty"5,
            "Price"10,
            "Amount"50
          },
          {
            "OrderId""SO-00023",
            "ItemId""It00123-6",
            "Qty"1,
            "Price"15,
            "Amount"15
          }
        ]
      }
    }
  }

В чем разница или почему используют один или другой формат? Исторически сложилось, так что сначала был XML и он позволял описывать необходимые данные. К недостаткам можно отнести большую громоздкость (на больших объемах данных это становится важным). JSON (JavaScript object notation) - был взят из JavaScript где он имеет нативную поддержку и получил распространение для обмена данными как более легковесная альтернатива XML. С точки зрения Enterprise - есть протокол SOAP - с использованием XML, есть протокол Rest API - с использованием XML. SOAP дает возможность получить схему документа, REST API - нет. SOAP - тяжелый, REST легковесный. В D365FO* можно применять любой из подходов либо их комбинацию. Мы заглянем в схему 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 и создаем новую запись



1 - вносим client Id
2 - описание записи (обычно назначение канала или название системы)
3 - пользователь Акс

Таким образом мы связали clientId с пользователем Акс. Осталось передать clientId, client secret внешней системе.

Отступление. Что такое запрос, какие они бывают, что такое 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 - отправить запрос на выполнение
Пример запроса, который мы использовали в браузере




Результат - внизу - на закладке Body


Браузер таблиц

Мы можем просмотреть записи таблицы если введем ссыдку в виде
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

Указываем список полей для выборки

GEThttps://{{base_url}}/data/CustomerGroups?$top=2&$select=dataAreaId,CustomerGroupId
Получим
{
  "@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

сортируем записи, по умолчанию ASK, доступно использование DESC

GET https://{{base_url}}/data/CustomerGroups?$count=true&$filter=PaymentTermId eq 'Net30'&$orderby=Description

{
  "@odata.context""https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
  "@odata.count"2,
  "value": [
    {
      "@odata.etag""W/\"Jzc1MDAwNDY4LDIyNTY1NDIxMTc3Jw==\"",
      "dataAreaId""usmf",
      "CustomerGroupId""20",
      "ClearingPeriodPaymentTermName""Net30",
      "CustomerAccountNumberSequence""",
      "DefaultDimensionDisplayValue""",
      "Description""Major customers new",
      "IsSalesTaxIncludedInPrice""No",
      "WriteOffReason""GOODWILL",
      "PaymentTermId""Net30",
      "TaxGroupId"""
    },
    {
      "@odata.etag""W/\"JzAsMjI1NjU0MjExNzYn\"",
      "dataAreaId""usmf",
      "CustomerGroupId""10",
      "ClearingPeriodPaymentTermName""Net30",
      "CustomerAccountNumberSequence""",
      "DefaultDimensionDisplayValue""",
      "Description""Wholesales customers",
      "IsSalesTaxIncludedInPrice""No",
      "WriteOffReason""",
      "PaymentTermId""Net30",
      "TaxGroupId"""
    }
  ]
}

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 вернет такой ответ

GET https://{{base_url}}/data/CustomerGroups?$count=true&$filter=CustomerGroupId eq '10'
{
  "@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


Ответ

{
  "@odata.context""https://devaos.cloudax.dynamics.com/data/$metadata#SalesOrderHeadersV2/$entity",
  "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderHeaderV2",
  "@odata.id""https://devaos.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",
  "OrderTotalChargesAmount"0,
 ...
  "RevRecLatestReverseJournal"0,
  "RevRecContractStartDate@odata.type""#DateTimeOffset",
  "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",
  "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",
  "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",
  "SalesOrderLines": [
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderLine",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')",
      "@odata.etag""W/\"JzAsNTYzNzE0NDU3NjswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
      "@odata.editLink""SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')",
      "dataAreaId""usmf",
      "InventoryLotId""000221",
    ...
      "SalesOrderLineStatus""Invoiced",
      "ProjectCategoryId""",
      "ItemNumber""D0001",
      "DeliveryAddressDescription""",
...
      "SalesOrderNumber""000002",
...
      "RevRecContractStartDate@odata.type""#DateTimeOffset",
      "RevRecContractStartDate""1900-01-01T12:00:00Z",
      "SalesOrderHeaderV2@odata.associationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/SalesOrderHeaderV2/$ref",
      "SalesOrderHeaderV2@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/SalesOrderHeaderV2",
      "SalesOrderHeader@odata.associationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/SalesOrderHeader/$ref",
      "SalesOrderHeader@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/SalesOrderHeader",
      "DimensionSet@odata.associationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/DimensionSet/$ref",
      "DimensionSet@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/DimensionSet",
      "DimensionCombination@odata.associationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/DimensionCombination/$ref",
      "DimensionCombination@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000221')/DimensionCombination"
    },
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderLine",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000222')",
      "@odata.etag""W/\"JzAsNTYzNzE0NDU3NzswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
      "@odata.editLink""SalesOrderLines(dataAreaId='usmf',InventoryLotId='000222')",
      "dataAreaId""usmf",
      "InventoryLotId""000222",
...    
    },
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderLine",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000223')",
      "@odata.etag""W/\"JzAsNTYzNzE0NDU3ODswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
      "@odata.editLink""SalesOrderLines(dataAreaId='usmf',InventoryLotId='000223')",
      "dataAreaId""usmf",
      "InventoryLotId""000223",
   ...  
    },
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderLine",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000224')",
      "@odata.etag""W/\"JzAsNTYzNzE0NDU3OTswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
      "@odata.editLink""SalesOrderLines(dataAreaId='usmf',InventoryLotId='000224')",
      "dataAreaId""usmf",
      "InventoryLotId""000224",
...  
    },
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.SalesOrderLine",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/SalesOrderLines(dataAreaId='usmf',InventoryLotId='000225')",
      "@odata.etag""W/\"JzAsNTYzNzE0NDU4MDswLDA7MCwwOzAsMDswLDA7MCwwJw==\"",
      "@odata.editLink""SalesOrderLines(dataAreaId='usmf',InventoryLotId='000225')",
      "dataAreaId""usmf",
      "InventoryLotId""000225",
 ...
    }
  ]
}

мы можем использовать дополнительные параметры в $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"
        }
      ]
    }
  ]
}


cross-company=true

По умолчанию запросы выполняются к компании, в которой по умолчанию привязан пользователь ассоциированный с парой clientId, secret Id

Для того что бы выполнить запрос по всем компаниям, необходимо добавить параметр cross-company=true


Вставка записей (Insert/POST)

Вставка осуществляется при помощи запроса POST

POST https://{{base_url}}/data/CustomerGroups


Закладка Body

{
"@odata.type":"#Microsoft.Dynamics.DataEntities.CustomerGroup",
"CustomerGroupId":"T1",
"dataAreaId":"usmf",
"Description":"Test group T1"
}

Где
@odata.type - тип записи который мы вставляем. Очень грубо говоря - Entity в которую мы осуществляем вставку называется CustomerGroups, мы в нее вставляем запись типа #Microsoft.Dynamics.DataEntities.CustomerGroup. Внешне энтити и тип погут быть похожи, а могут быть не похожи.
Заполняем поля - поля со звездочной - обязательные для заполнения поля
* "CustomerGroupId":"T1",
* "dataAreaId":"usmf",
"Description":"Test group T1"

Выполняем запрос, получаем ответ



Статус ответа 201 Created - запись создана

Выполняем запрос с фильтром на выборку и проверяем

GET https://{{base_url}}/data/CustomerGroups?$filter=CustomerGroupId eq 'T1'

{
  "@odata.context""https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
  "value": [
    {
      "@odata.type""#Microsoft.Dynamics.DataEntities.CustomerGroup",
      "@odata.id""https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')",
      "@odata.etag""W/\"JzEsNjg3MTk0ODI4MzMn\"",
      "@odata.editLink""CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')",
      "dataAreaId""usmf",
      "CustomerGroupId""T1",
      "ClearingPeriodPaymentTermName""",
      "CustomerAccountNumberSequence""",
      "DefaultDimensionDisplayValue""",
      "Description""Test group T1",
      "IsSalesTaxIncludedInPrice@odata.type""#Microsoft.Dynamics.DataEntities.NoYes",
      "IsSalesTaxIncludedInPrice""No",
      "WriteOffReason""",
      "PaymentTermId""",
      "TaxGroupId""",
      "DimensionSet@odata.associationLink""https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')/DimensionSet/$ref",
      "DimensionSet@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')/DimensionSet",
      "Prospects@odata.associationLink""https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')/Prospects/$ref",
      "Prospects@odata.navigationLink""https://devaos.cloudax.dynamics.com/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')/Prospects"
    }
  ]
}

Обновление записей (Update/PUT)

PUT https://{{base_url}}/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')

Обратите внимание - мы должны четко выбрать одну запись по ключу, и только тогда ее обновить.

Headers - такой же

Body
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.CustomerGroup",
"Description":"Test group T123"
}

Ответ Status 204 No content


проверяем запросом на выборку

GET https://{{base_url}}/data/CustomerGroups?$filter=CustomerGroupId eq 'T1'


{
  "@odata.context""https://devaos.cloudax.dynamics.com/data/$metadata#CustomerGroups",
  "value": [
    {
      "@odata.etag""W/\"JzM0NjU2NTI5MCw2ODcxOTQ4MjgzMyc=\"",
      "dataAreaId""usmf",
      "CustomerGroupId""T1",
      "ClearingPeriodPaymentTermName""",
      "CustomerAccountNumberSequence""",
      "DefaultDimensionDisplayValue""",
      "Description""Test group T123",
      "IsSalesTaxIncludedInPrice""No",
      "WriteOffReason""",
      "PaymentTermId""",
      "TaxGroupId"""
    }
  ]
}


Удаление записей (Delete/DELETE)


Заголовок тот же, боди -пустое, запрос DELETE https://{{base_url}}/data/CustomerGroups(dataAreaId='usmf',CustomerGroupId='T1')

Ответ


Статус 204 - No Content

проверяем
GET https://{{base_url}}/data/CustomerGroups?$filter=CustomerGroupId eq 'T1'



Нет записей для отображения, мы справились

Транзакция ttsbegin..ttscommit BATCH

Для того, что бы выполнить вставку многих записей в один раз мы должны выполнить запрос к сервису batch

POST https://{{base_url}}/data/$batch

В Header:

Content-Type: multipart/mixed; boundary=batch_boundary

Body:

--batch_boundary
Content-Type: multipart/mixed; boundary=changeset_boundary

--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

POST https://{{base_url}}/data/CustomerGroups HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8

{
"@odata.type":"#Microsoft.Dynamics.DataEntities.CustomerGroup",
"CustomerGroupId":"Batch1",
"dataAreaId":"usmf",
"Description":"Batch 1 Test group T1"
}

--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2

POST https://{{base_url}}/data/CustomerGroups HTTP/1.1
OData-Version: 4.0
OData-MaxVersion: 4.0
Content-Type: application/json;odata.metadata=minimal
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8

{
"@odata.type":"#Microsoft.Dynamics.DataEntities.CustomerGroup",
"CustomerGroupId":"Batch2",
"dataAreaId":"usmf",
"Description":"Batch 2 Test group T1"
}

--batch_boundary--


Если по какой-то причине какая-то запись не вставилась - сервис batch вернет статус 200 - что он отработал нормально. Другое дело, при вызове multipart нужно анализировать все ответы.

--batchresponse_5a294678-02dd-4b16-94b9-5fd64e297a8e
Content-Type: multipart/mixed; boundary=changesetresponse_1209a811-16fe-40f4-98e9-5519c744549e

--changesetresponse_1209a811-16fe-40f4-98e9-5519c744549e
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; odata.metadata=minimal; charset=utf-8
OData-Version: 4.0

{
  "error":{
    "code":"","message":"An error has occurred.","innererror":{
      "message":"Write failed for table row of type 'CustCustomerGroupEntity'. Infolog: Info: Cannot create a record in Customer groups (CustGroup). Customer group: Batch2, Batch 2 Test group T1.\nThe record already exists..","type":"Microsoft.Dynamics.Platform.Integration.Services.OData.AxODataWriteException","stacktrace":"   at Microsoft.Dynamics.Platform.Integration.Services.OData.Update.UpdateProcessor.CreateEntity_Save(ChangeOperationContext context, ChangeInfo changeInfo)\r\n   at Microsoft.Dynamics.Platform.Integration.Services.OData.Update.UpdateManager.<>c__DisplayClass6_0.<CreateEntity>b__1(ChangeOperationContext context)\r\n   at Microsoft.Dynamics.Platform.Integration.Services.OData.Update.ChangeInfo.ExecuteActionsInCompanyContext(IEnumerable`1 actionList, ChangeOperationContext operationContext)\r\n   at Microsoft.Dynamics.Platform.Integration.Services.OData.Update.ChangeInfo.TrySave(ChangeOperationContext operationContext)\r\n   at Microsoft.Dynamics.Platform.Integration.Services.OData.Update.UpdateManager.SaveChanges()\r\n   at Microsoft.Dynamics.Platform.Integration.Services.OData.AxODataBatchHandler.<ExecuteRequestMessagesAsync>d__8.MoveNext()"
    }
  }
}
--changesetresponse_1209a811-16fe-40f4-98e9-5519c744549e--
--batchresponse_5a294678-02dd-4b16-94b9-5fd64e297a8e--

Номерные серии

Работа с Entity позволяет получать автозаполнение полей стандартным способом. Например, при вставке строк заказов на продажу достаточно заполнить всего пару полей в POST запросе

{
"@odata.type":"#Microsoft.Dynamics.DataEntities.SalesOrderLine",
"ItemNumber":"D0001",
"SalesOrderNumber":"001261"
}

Строка успешно создастся и поля единица измерения, кол-во, цена, сумма ... аналитики - заполнятся по умолчанию.

В случае вставки заказа на продажу (шапка) та же история
{
"@odata.type":"#Microsoft.Dynamics.DataEntities.SalesOrderHeaderV2",
"CurrencyCode": "USD",
"InvoiceCustomerAccountNumber":"US-001",
"OrderingCustomerAccountNumber": "US-001",
"dataAreaId":"usmf"
}

При этом номер заказа для шапки будет сгенерирован автоматически.

Что нам делать, если мы попробуем сделать вставку Шапки заказа и строк в одной транзакции при помощи batch?

Нам нужно что бы шапка и строки были с одним номером. Для этого у нас есть два варианта. Первый - нужно присвоить следующий номер или произвольный( в случае с произвольным номером - номерная серия для номера заказа на продажу должна допускать редактирование).

Заключение


В принципе мы рассмотрели возможности применения данной технологии. Это не единственная технология интеграции в аксапте, но точно очень полезная. Понимая эти принципы - можно дальше идти с интеграцией в MS Power Automate.