Question

I am trying to get the Custom Attribute I have in the sales_order_item to display on the rest/V1/orders/items API call. I was able to get the Attribute to display on the rest/V1/orders API call. Using a Magento\Sales\Api\OrderRepositoryInterface plugin. But the Attribute don't display on the rest/V1/orders/items API call. I was trying to use the OrderItemRepositoryInterface plugin, but I don't know what functions to add or if this is the correct way to do this.

Thanks.

No correct solution

OTHER TIPS

You have to make your Custom Attribute attribute as extension attribute for Magento\Sales\Api\Data\OrderItemInterface

create extension_attributes.xml at your modules app/code/{Vendorname}/{ModuleName}/etc.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
    <extension_attributes for="Magento\Sales\Api\Data\OrderItemInterface">
        <attribute code="{Your_Custom_Field}" type="string" />
    </extension_attributes>
</config>

I assume that your field as varchar that why I have add type as string type variable (type="string").

Update,

As you want to expose that rest/V1/orders/items Api point then you have to create a plugin on Magento\Sales\Model\Order\ProductOption::add()

Here the plugin Class:

<?php


namespace {VendorName}\{ModuleName}\Plungin;

use Magento\Sales\Api\Data\OrderItemInterface;

class ProductOption
{


    /**
     * @var \Magento\Sales\Api\Data\OrderItemExtensionFactory
     */
    private $orderItemExtensionFactory;

    public function __construct(
        \Magento\Sales\Api\Data\OrderItemExtensionFactory $orderItemExtensionFactory
    ) {

        $this->orderItemExtensionFactory = $orderItemExtensionFactory;
    }
    public function beforeAdd(
        \Magento\Sales\Model\Order\ProductOption $subject,
        OrderItemInterface $orderItem
    ) {
        $extensionAttributes = $orderItem->getExtensionAttributes();
        if(null=== $extensionAttributes){
            $extensionAttributes= $this->orderItemExtensionFactory->create();

        }
        $extensionAttributes->setCustomField($order->getCustomField());
        $orderItem->setExtensionAttributes($extensionAttributes);
    }
}

I know it is a little late, but for anyone else looking for a way to get label values of custom attributes, and configurable item values via the rest api.

I am presuming you want the custom attribute's label & value label, and not id & value_id as those do exist in the orders api.

There are 3 steps we need to make in order to find all of the different custom attributes labels.

  1. Fetch the orders: /rest/V1/orders
  2. To find the value of product_option.extension_attributes.custom_options, we will fetch the products and resolve the custom option labels from there:

/rest/V1/products?searchCriteria[filterGroups][0][filters][0][field]=entity_id&searchCriteria[filterGroups][0][filters][0][condition_type]=in&searchCriteria[filterGroups][0][filters][0][value]=${itemIds}

  1. Finally to find the value of product_option.extenstion_attributes.configurable_item_options, we will fetch the attributes by id, and resolve the attribute labels:

/rest/V1/products/attributes?searchCriteria[filterGroups][0][filters][0][field]=attribute_id&searchCriteria[filterGroups][0][filters][0][condition_type]=in&searchCriteria[filterGroups][0][filters][0][value]=${attributeIds}&fields=items[attribute_id,options,default_frontend_label]

Full example, with some optimisations, such as fetching the needed products and attributes at once (not something you can just copy and use, as it is dependent on some external helpers):

import fetch from 'node-fetch'
import { path, uniq } from 'ramda'
import checkStatus from '../../../../common/utils/checkStatus'
import { getLastUpdate, setLastUpdate } from '../../utils/storage'
import config from '../../config.json'
import mapOrder from './mapOrder'

const getOrdersUrl = async () => {
  const previousUpdate = await getLastUpdate()

  return `${config.magento.endpointAddress}/orders?searchCriteria[sortOrders][0][direction]=desc&searchCriteria[sortOrders][0][field]=updated_at&searchCriteria[filterGroups][0][filters][0][conditionType]=gteq&searchCriteria[filterGroups][0][filters][0][field]=updated_at&searchCriteria[filterGroups][0][filters][0][value]=${previousUpdate}`
}

const getItemsProductsUrl = items => {
  // Filter out items that have a parent id, as their parents are the configurable products which we want to work with, and not the children.
  // Filter out items that don't have product options, as we don't need to enrich them either.
  const itemIds = uniq(
    items
      .filter(item => !item.parent_item_Id && item.product_option)
      .map(item => +item.product_id)
  ).join(',')

  return `${config.magento.endpointAddress}/products?searchCriteria[filterGroups][0][filters][0][field]=entity_id&searchCriteria[filterGroups][0][filters][0][condition_type]=in&searchCriteria[filterGroups][0][filters][0][value]=${itemIds}`
}

const getItemAttributesUrl = options => {
  const attributeIds = uniq(options.map(option => +option.option_id)).join(',')

  return `${config.magento.endpointAddress}/products/attributes?searchCriteria[filterGroups][0][filters][0][field]=attribute_id&searchCriteria[filterGroups][0][filters][0][condition_type]=in&searchCriteria[filterGroups][0][filters][0][value]=${attributeIds}&fields=items[attribute_id,options,default_frontend_label]`
}

const getHeaders = () => {
  return {
    Authorization: `Bearer ${config.magento.accessToken}`,
    Accept: '*/*',
    'Accept-Encoding': 'gzip, deflate, br',
    'User-Agent': 'IDS/1.0'
  }
}

const innerFetch = url => {
  return fetch(url, {
    headers: getHeaders()
  })
    .then(checkStatus)
    .then(res => res.json())
    .catch(console.log.bind(console))
}

const fetchOrderProducts = items => innerFetch(getItemsProductsUrl(items))
const fetchItemAttributes = options => innerFetch(getItemAttributesUrl(options))

const getCustomOptionsValues = (customOptions, foundProduct) => {
  return customOptions.map(option => {
    const productOption = foundProduct.options.find(
      productOption => +productOption.option_id === +option.option_id
    )

    if (!productOption) {
      return option
    }

    const splitOptionValues = option.option_value
      .split(',')
      .map(innerValue => +innerValue.trim())

    // We could have multiple selection under option_value
    const optionValueTitle = productOption.values
      .filter(value => splitOptionValues.includes(+value.option_type_id))
      .map(foundOption => foundOption.title)
      .join(', ')

    return Object.assign({}, option, {
      option_title: productOption.title,
      option_value_title: optionValueTitle
    })
  })
}

const getConfiguratbleOptionsValues = (itemOptions, allAttributes) => {
  const attributes = allAttributes.items

  return itemOptions.map(option => {
    const foundAttribute = attributes.find(
      attribute => +attribute.attribute_id === +option.option_id
    )

    if (!foundAttribute) {
      return option
    }

    const optionValueTitle = foundAttribute.options.find(
      attrOption => +attrOption.value === +option.option_value
    ).label

    return Object.assign({}, option, {
      option_title: foundAttribute.default_frontend_label,
      option_value_title: optionValueTitle
    })
  })
}

const addProductOptionValues = (order, allProducts, allAttributes) => {
  // Check if there are options to fetch
  if (order.items.every(item => !item.product_option)) {
    // Nothing to add here
    return order
  }

  const orderClone = Object.assign({}, order)

  orderClone.items = orderClone.items.map(item => {
    const foundProduct = allProducts.items.find(
      product => +product.id === +item.product_id
    )

    const extensionAttributes = path(
      ['product_option', 'extension_attributes'],
      item
    )

    if (!foundProduct || !extensionAttributes) {
      return item
    }

    if (extensionAttributes.custom_options) {
      extensionAttributes.custom_options = getCustomOptionsValues(
        extensionAttributes.custom_options,
        foundProduct
      )
    }

    const itemOptions = extensionAttributes.configurable_item_options

    if (itemOptions) {
      extensionAttributes.configurable_item_options = getConfiguratbleOptionsValues(
        itemOptions,
        allAttributes
      )
    }

    return item
  })

  return orderClone
}

// Prefetch all products in one request as to not send too many requests to the server
const prefetchAllProducts = async orders => {
  const allItems = orders.flatMap(order => order.items)

  if (!allItems.length) {
    return {}
  }

  return await fetchOrderProducts(allItems)
}

// Prefetch all configurable_item_options in one request as to not send too many requests to the server
const prefetchAllItemAttributes = async orders => {
  const allAttributes = orders.flatMap(order =>
    order.items
      .flatMap(item =>
        path(
          [
            'product_option',
            'extension_attributes',
            'configurable_item_options'
          ],
          item
        )
      )
      .filter(x => x)
  )

  if (!allAttributes.length) {
    return {}
  }

  return await fetchItemAttributes(allAttributes)
}

// Items that come from the order api don't include custom attributes labels
// so we need to fetch full order items
const addOrderItems = async response => {
  if (!response || !response.items) {
    return response
  }

  const orders = response.items

  if (!orders || !orders.length) {
    return response
  }

  const allProducts = await prefetchAllProducts(orders)
  const allAttributes = await prefetchAllItemAttributes(orders)

  return response.items.map(order =>
    addProductOptionValues(order, allProducts, allAttributes)
  )
}

export const fetchOrders = async () => {
  const url = await getOrdersUrl()

  await setLastUpdate()

  return innerFetch(url)
    .then(addOrderItems)
    .then(res => {
      return Array.isArray(res) && res.map(mapOrder)
    })
    .catch(console.log.bind(console))
}

export default fetchOrders
Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top