For Zoho Services only:


I'm actually part of a bigger team at Ascent Business Solutions where we have support technicians and project consultants. Support is for smaller technical fixes but this can include developments, reports or integrations; depending on the size of the task. Projects are for more time-consuming developments such as revamps of the Zoho Suite of apps or on-site training. The advantage of a team is that if I am out-of-office for a day or so, there is always someone at Ascent Business Solutions who can deal with any queries/issues you may have.

Our support rates can be found and purchased at http://ascentbusiness.co.uk/zoho-support-2. A support bundle doesn't have an expiry date. So whether we can do what you want within the bundle and a year later need further support, if there are minutes left on the bundle then there is no additional charge.

Our project rates for bigger developments can be found at http://ascentbusiness.co.uk/crm-solutions/zoho-crm-packages-prices and will involve a dedicated project consultant along with developers who will hold your hand through the development process.

If you want help building a solution for one of the Zoho Apps in the Zoho Suite, contact us on 0121 293 8140 (UK) or by email at info@ascentbusiness.co.uk. You can also visit our website at http://ascentbusiness.co.uk.

I regularly build and specialize in 2-way API integrations for Xero, Shopify and eBay.

Zoho CRM: Update a custom field in line items / product details using REST API v2.1

What?
This is a quick article documenting how to update custom fields in a line items or product details section of a transactional module such as Quotes, Sales Orders or Invoices using code: Zoho Deluge.

Why?
At time of print, Zoho had recently introduced the ability to have custom fields in your line items, alongside the product name, list price, quantity, tax, etc. In the example below, we have added a column called "Group Name" in the CRM Quote module as per the following screenshot:
Populating Custom Field in a Quote Line Item

How?
Again at the time of this article, this is only modifiable when using REST API v2.1. We are going to update the field with label "Group Name" but API name "Grouping" in a list item column.

The standard code you used to use even for REST API v2.0 would have been something like the following:
copyraw
//
// init
l_CrmLineItems = List();
//
// some sample values
v_CrmProductID = "123456789012345678";
//
// build up product details JSON to send
m_LineItemProduct = Map();
m_LineItemProduct.put("id",v_CrmProductID);
m_LineItem = Map();
m_LineItem.put("product",m_LineItemProduct);
m_LineItem.put("quantity",5);
m_LineItem.put("list_price",0.99);
v_LineItemTax = 0.99 * 5 * 0.2;
m_LineItem.put("Tax",v_LineItemTax);
if(v_LineItemTax>0)
{
	l_TaxOptions = List();
	m_TaxOption1 = Map();
	m_TaxOption1.put("percentage",20);
	m_TaxOption1.put("name","Sales Tax");
	m_TaxOption1.put("value",v_LineItemTax);
	l_TaxOptions.add(m_TaxOption1);
	m_LineItem.put("line_tax",l_TaxOptions);
}
m_LineItem.put("total",5.94);
m_LineItem.put("product_description","My Test Description");
l_CrmLineItems.add(m_LineItem);
//
m_CreateQuote = Map();
m_CreateQuote.put("Subject","My Test Quote");
m_CreateQuote.put("Product_Details",l_CrmLineItems);
  1.  // 
  2.  // init 
  3.  l_CrmLineItems = List()
  4.  // 
  5.  // some sample values 
  6.  v_CrmProductID = "123456789012345678"
  7.  // 
  8.  // build up product details JSON to send 
  9.  m_LineItemProduct = Map()
  10.  m_LineItemProduct.put("id",v_CrmProductID)
  11.  m_LineItem = Map()
  12.  m_LineItem.put("product",m_LineItemProduct)
  13.  m_LineItem.put("quantity",5)
  14.  m_LineItem.put("list_price",0.99)
  15.  v_LineItemTax = 0.99 * 5 * 0.2
  16.  m_LineItem.put("Tax",v_LineItemTax)
  17.  if(v_LineItemTax>0) 
  18.  { 
  19.      l_TaxOptions = List()
  20.      m_TaxOption1 = Map()
  21.      m_TaxOption1.put("percentage",20)
  22.      m_TaxOption1.put("name","Sales Tax")
  23.      m_TaxOption1.put("value",v_LineItemTax)
  24.      l_TaxOptions.add(m_TaxOption1)
  25.      m_LineItem.put("line_tax",l_TaxOptions)
  26.  } 
  27.  m_LineItem.put("total",5.94)
  28.  m_LineItem.put("product_description","My Test Description")
  29.  l_CrmLineItems.add(m_LineItem)
  30.  // 
  31.  m_CreateQuote = Map()
  32.  m_CreateQuote.put("Subject","My Test Quote")
  33.  m_CreateQuote.put("Product_Details",l_CrmLineItems)
Which when converted and sent in JSON would look something like the following request:
copyraw
{
  "Subject": "My Test Quote",
  "Product_Details": [
    {
      "product": {
        "Product_Code": "TEST1",
        "Currency": "GBP",
        "name": "My Test Product",
        "id": "123456789012345678"
      },
      "quantity": 5,
      "Discount": 0,
      "Tax": 0.99,
      "list_price": 0.99,
      "total": 5.94,
      "product_description": "My Test Description",
      "line_tax": [
      {
        "percentage": 20,
        "name": "Sales Tax",
        "value": 0.99
      }
      ]
    }
  ]
}
  1.  { 
  2.    "Subject": "My Test Quote", 
  3.    "Product_Details": [ 
  4.      { 
  5.        "product": { 
  6.          "Product_Code": "TEST1", 
  7.          "Currency": "GBP", 
  8.          "name": "My Test Product", 
  9.          "id": "123456789012345678" 
  10.        }, 
  11.        "quantity": 5, 
  12.        "Discount": 0, 
  13.        "Tax": 0.99, 
  14.        "list_price": 0.99, 
  15.        "total": 5.94, 
  16.        "product_description": "My Test Description", 
  17.        "line_tax": [ 
  18.        { 
  19.          "percentage": 20, 
  20.          "name": "Sales Tax", 
  21.          "value": 0.99 
  22.        } 
  23.        ] 
  24.      } 
  25.    ] 
  26.  } 

Updating with REST API v2.1
The new process doesn't just require you to add in this custom field per line item, but also requires a change to the other fields in the line item. There is an additional note that when using v2.1, you no longer post to the "Product_Details" key but to the respective module line item API name as per the following screenshot:
Populating Custom Field in a Quote Line Item - Referring to API names

Following the API names in this example, the Deluge code to send would be something like the following instead (reminder: instead of Product_Details, post to the API name in this case "Quoted_Items"):
copyraw
//
// init
l_CrmLineItems = List();
//
// some sample values
v_CrmProductID = "123456789012345678";
//
// build up product details JSON to send
m_LineItem = Map();
m_LineItem.put("Product_Name",v_CrmProductID);
m_LineItem.put("Quantity",5);
m_LineItem.put("List_Price",0.99);
// setting a tax as an example
v_LineItemTax = 0.99 * 5 * 0.2;
m_LineItem.put("Tax",v_LineItemTax);
l_TaxOptions = List();
if(v_LineItemTax>0)
{
	m_TaxOption1 = Map();
	m_TaxOption1.put("percentage",20);
	m_TaxOption1.put("name","Sales Tax");
	m_TaxOption1.put("value",v_LineItemTax);
	l_TaxOptions.add(m_TaxOption1);
}
else
{
	m_TaxOption0 = Map();
	m_TaxOption0.put("percentage",0);
	m_TaxOption0.put("name","Zero Tax");
	m_TaxOption0.put("value",0);
	l_TaxOptions.add(m_TaxOption0);
}
m_LineItem.put("Line_Tax",l_TaxOptions);
m_LineItem.put("Discount",0);
m_LineItem.put("Description","My Test Description");
m_LineItem.put("Grouping","My Test Group");
l_CrmLineItems.add(m_LineItem);
//
m_CreateQuote = Map();
m_CreateQuote.put("Subject","My Test Quote");
m_CreateQuote.put("Quoted_Items",l_CrmLineItems);
//
// send to CRM
l_RecordsToSend = List();
l_RecordsToSend.add(m_CreateQuote);
m_Data = Map();
m_Data.put("data", l_RecordsToSend);
//
// this is REST API so by default all triggers run.  Use an empty list to stop or prevent these from triggering.
m_Data.put("triggers",[]);
//
// send via REST API v2.1 on EU datacenter
r_CreateCrmQuote = invokeurl
[
	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
	type :POST
	parameters:m_Data.toString()
	connection:"myconnection"
];
  1.  // 
  2.  // init 
  3.  l_CrmLineItems = List()
  4.  // 
  5.  // some sample values 
  6.  v_CrmProductID = "123456789012345678"
  7.  // 
  8.  // build up product details JSON to send 
  9.  m_LineItem = Map()
  10.  m_LineItem.put("Product_Name",v_CrmProductID)
  11.  m_LineItem.put("Quantity",5)
  12.  m_LineItem.put("List_Price",0.99)
  13.  // setting a tax as an example 
  14.  v_LineItemTax = 0.99 * 5 * 0.2
  15.  m_LineItem.put("Tax",v_LineItemTax)
  16.  l_TaxOptions = List()
  17.  if(v_LineItemTax>0) 
  18.  { 
  19.      m_TaxOption1 = Map()
  20.      m_TaxOption1.put("percentage",20)
  21.      m_TaxOption1.put("name","Sales Tax")
  22.      m_TaxOption1.put("value",v_LineItemTax)
  23.      l_TaxOptions.add(m_TaxOption1)
  24.  } 
  25.  else 
  26.  { 
  27.      m_TaxOption0 = Map()
  28.      m_TaxOption0.put("percentage",0)
  29.      m_TaxOption0.put("name","Zero Tax")
  30.      m_TaxOption0.put("value",0)
  31.      l_TaxOptions.add(m_TaxOption0)
  32.  } 
  33.  m_LineItem.put("Line_Tax",l_TaxOptions)
  34.  m_LineItem.put("Discount",0)
  35.  m_LineItem.put("Description","My Test Description")
  36.  m_LineItem.put("Grouping","My Test Group")
  37.  l_CrmLineItems.add(m_LineItem)
  38.  // 
  39.  m_CreateQuote = Map()
  40.  m_CreateQuote.put("Subject","My Test Quote")
  41.  m_CreateQuote.put("Quoted_Items",l_CrmLineItems)
  42.  // 
  43.  // send to CRM 
  44.  l_RecordsToSend = List()
  45.  l_RecordsToSend.add(m_CreateQuote)
  46.  m_Data = Map()
  47.  m_Data.put("data", l_RecordsToSend)
  48.  // 
  49.  // this is REST API so by default all triggers run.  Use an empty list to stop or prevent these from triggering. 
  50.  m_Data.put("triggers",[])
  51.  // 
  52.  // send via REST API v2.1 on EU datacenter 
  53.  r_CreateCrmQuote = invokeUrl 
  54.  [ 
  55.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
  56.      type :POST 
  57.      parameters:m_Data.toString() 
  58.      connection:"myconnection" 
  59.  ]
or if converted to JSON, the request would read as:
copyraw
{
  "data": {
    "Subject": "My Test Quote",
    "Quoted_Items": [
      {
        "Product_Name": "123456789012345678",
        "Quantity": 5,
        "Discount": 0,
        "Tax": 0.99,
        "List_Price": 0.99,
        "Total": 5.94,
        "Description": "My Test Description",
        "Grouping": "My Test Group",
        "Line_Tax": [
            {
              "percentage": 20,
              "name": "Sales Tax",
              "value": 0.99
            }
          ]
      }
    ]
  },
  "triggers": []
}
  1.  { 
  2.    "data": { 
  3.      "Subject": "My Test Quote", 
  4.      "Quoted_Items": [ 
  5.        { 
  6.          "Product_Name": "123456789012345678", 
  7.          "Quantity": 5, 
  8.          "Discount": 0, 
  9.          "Tax": 0.99, 
  10.          "List_Price": 0.99, 
  11.          "Total": 5.94, 
  12.          "Description": "My Test Description", 
  13.          "Grouping": "My Test Group", 
  14.          "Line_Tax": [ 
  15.              { 
  16.                "percentage": 20, 
  17.                "name": "Sales Tax", 
  18.                "value": 0.99 
  19.              } 
  20.            ] 
  21.        } 
  22.      ] 
  23.    }, 
  24.    "triggers": [] 
  25.  } 

Reading a Response
The response on success would be something like:
copyraw
{
  "data": [
    {
      "code": "SUCCESS",
      "details": {
        "Modified_Time": "2021-05-17T11:02:59+01:00",
        "Modified_By": {
          "name": "Joel Admin",
          "id": "123456"
        },
        "Created_Time": "2021-05-17T11:02:59+01:00",
        "id": "98498465735491156",
        "Created_By": {
          "name": "Joel Admin",
          "id": "123456"
        }
      },
      "message": "record added",
      "status": "success"
    }
  ]
}
  1.  { 
  2.    "data": [ 
  3.      { 
  4.        "code": "SUCCESS", 
  5.        "details": { 
  6.          "Modified_Time": "2021-05-17T11:02:59+01:00", 
  7.          "Modified_By": { 
  8.            "name": "Joel Admin", 
  9.            "id": "123456" 
  10.          }, 
  11.          "Created_Time": "2021-05-17T11:02:59+01:00", 
  12.          "id": "98498465735491156", 
  13.          "Created_By": { 
  14.            "name": "Joel Admin", 
  15.            "id": "123456" 
  16.          } 
  17.        }, 
  18.        "message": "record added", 
  19.        "status": "success" 
  20.      } 
  21.    ] 
  22.  } 
and if using code to capture the ID:
copyraw
v_ResultingQuoteID = 0;
if(r_CreateCrmQuote.toMap().get("data") != null)
{
	if(r_CreateCrmQuote.toMap().get("data").get(0) != null)
	{
		if(r_CreateCrmQuote.toMap().get("data").get(0).get("details") != null)
		{
			v_ResultingQuoteID = r_CreateCrmQuote.toMap().get("data").get(0).get("details").get("id");
		}
	}
}
  1.  v_ResultingQuoteID = 0
  2.  if(r_CreateCrmQuote.toMap().get("data") != null) 
  3.  { 
  4.      if(r_CreateCrmQuote.toMap().get("data").get(0) != null) 
  5.      { 
  6.          if(r_CreateCrmQuote.toMap().get("data").get(0).get("details") != null) 
  7.          { 
  8.              v_ResultingQuoteID = r_CreateCrmQuote.toMap().get("data").get(0).get("details").get("id")
  9.          } 
  10.      } 
  11.  } 

Retrieving a record with 2.1 JSON
The following is a snippet when querying a CRM module (invoices) from a CRM function. The important fields I wanted were some custom fields within the line items subform. Here I'm adding the parameters to ensure we get CRM records that are also pending approval or being converted.
copyraw
m_Params = Map();
m_Params.put("approved","both");
m_Params.put("converted","both");
r_InvoiceDetails = invokeurl
[
	url :"https://www.zohoapis.eu/crm/v2.1/Invoices/" + p_InvoiceID
	type :GET
	parameters: m_Params
	connection:"joels_connector"
];
info r_InvoiceDetails;
  1.  m_Params = Map()
  2.  m_Params.put("approved","both")
  3.  m_Params.put("converted","both")
  4.  r_InvoiceDetails = invokeUrl 
  5.  [ 
  6.      url :"https://www.zohoapis.eu/crm/v2.1/Invoices/" + p_InvoiceID 
  7.      type :GET 
  8.      parameters: m_Params 
  9.      connection:"joels_connector" 
  10.  ]
  11.  info r_InvoiceDetails; 

Common Error(s):
  • {"code":"MANDATORY_NOT_FOUND","details":{"api_name":"data"},"message":"required field not found","status":"error"} is because you did this:
    copyraw
    r_CreateCrmQuote = invokeurl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :POST
    	parameters: m_CreateQuote
    	connection:"myconnection"
    ];
    1.  r_CreateCrmQuote = invokeUrl 
    2.  [ 
    3.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    4.      type :POST 
    5.      parameters: m_CreateQuote 
    6.      connection:"myconnection" 
    7.  ]
    when you should have done this (send the payload as the value to the key "data"):
    copyraw
    l_RecordsToSend = List();
    l_RecordsToSend.add(m_CreateQuote);
    m_Data = Map();
    m_Data.put("data", l_RecordsToSend);
    r_CreateCrmQuote = invokeurl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :POST
    	parameters: m_Data.toString()
    	connection:"myconnection"
    ];
    1.  l_RecordsToSend = List()
    2.  l_RecordsToSend.add(m_CreateQuote)
    3.  m_Data = Map()
    4.  m_Data.put("data", l_RecordsToSend)
    5.  r_CreateCrmQuote = invokeUrl 
    6.  [ 
    7.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    8.      type :POST 
    9.      parameters: m_Data.toString() 
    10.      connection:"myconnection" 
    11.  ]

  • {"code":"INVALID_DATA","details":{"expected_data_type":"jsonobject"},"message":"body","status":"error"} is because you did this:
    copyraw
    r_CreateCrmQuote = invokeurl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :POST
    	parameters:m_Data
    	connection:"myconnection"
    ];
    1.  r_CreateCrmQuote = invokeUrl 
    2.  [ 
    3.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    4.      type :POST 
    5.      parameters:m_Data 
    6.      connection:"myconnection" 
    7.  ]
    when you should have done this (note the .toString()):
    copyraw
    r_CreateCrmQuote = invokeurl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :POST
    	parameters:m_Data.toString()
    	connection:"myconnection"
    ];
    1.  r_CreateCrmQuote = invokeUrl 
    2.  [ 
    3.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    4.      type :POST 
    5.      parameters:m_Data.toString() 
    6.      connection:"myconnection" 
    7.  ]

  • {"code":"INVALID_DATA","details":{"expected_data_type":"jsonarray","api_name":"data"},"message":"invalid data","status":"error"} is because you did this:
    copyraw
    m_Data = Map();
    m_Data.put("data",m_RecordData);
    r_RecordUpdate = invokeUrl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :PUT
    	headers: m_Header
    	parameters:m_Data.toString() 
    ];
    1.  m_Data = Map()
    2.  m_Data.put("data",m_RecordData)
    3.  r_RecordUpdate = invokeUrl 
    4.  [ 
    5.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    6.      type :PUT 
    7.      headers: m_Header 
    8.      parameters:m_Data.toString() 
    9.  ]
    when you should have done this (note that a list is submitted to data / could use .toJSONList()):
    copyraw
    l_Records = List();
    l_Records.add(m_RecordData);
    m_Data = Map();
    m_Data.put("data",l_Records);
    r_RecordUpdate = invokeUrl
    [
    	url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
    	type :PUT
    	headers: m_Header
    	parameters:m_Data.toString() 
    ];
    1.  l_Records = List()
    2.  l_Records.add(m_RecordData)
    3.  m_Data = Map()
    4.  m_Data.put("data",l_Records)
    5.  r_RecordUpdate = invokeUrl 
    6.  [ 
    7.      url :"https://www.zohoapis.eu/crm/v2.1/Quotes" 
    8.      type :PUT 
    9.      headers: m_Header 
    10.      parameters:m_Data.toString() 
    11.  ]

  • {"code":"INVALID_TOKEN","details":{},"message":"invalid oauth token","status":"error"} is probably because you are on the .COM datacenter and you should be using https://www.zohoapis.com/crm/v2.1/Quotes but if you were on the EU datacenter, you would use https://www.zohoapis.eu/crm/v2.1/Quotes.
  • Line Item Tax does not submit in API v2.1 Trial and error resolved this one. Submit the line item but instead of "line_tax", replace this with "Line_Tax" (note the capitalized letters). I have included this in the example above.
  • Line Item Tax doesn't display at line item lvel Previously submitting no tax would still put a 0.00 in the tax column of a line item. I have found that you have to include the line tax option of zero tax (or whatever you call zero rated tax) in the line item. Again this is included in my v2.1 example above. Should be submitted even if tax at line item level is 0.
  • Triggers when using REST API v2.1 When using the REST API (even v2.0), the workflow/approval/blueprint triggers will all run by default. Set this to an empty list [] to prevent/stop CRM triggers from running any workflows.
Category: Zoho :: Article: 744

Related Articles

Joes Revolver Map

Accreditation

Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2021 Joel Lipman .com. All Rights Reserved.