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:
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:
// // 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);
- //
- // 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);
{ "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 } ] } ] }
- {
- "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
- }
- ]
- }
- ]
- }
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:
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"):
// // 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("trigger",[]); // // 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" ];
- //
- // 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("trigger",[]);
- //
- // 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"
- ];
{ "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 } ] } ] }, "trigger": [] }
- {
- "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
- }
- ]
- }
- ]
- },
- "trigger": []
- }
Reading a Response
The response on success would be something like:
{ "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" } ] }
- {
- "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"
- }
- ]
- }
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"); } } }
- 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");
- }
- }
- }
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.
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;
- 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;
Common Error(s):
- {"code":"MANDATORY_NOT_FOUND","details":{"api_name":"data"},"message":"required field not found","status":"error"} is because you did this:
copyrawwhen you should have done this (send the payload as the value to the key "data"):
r_CreateCrmQuote = invokeurl [ url :"https://www.zohoapis.eu/crm/v2.1/Quotes" type :POST parameters: m_CreateQuote connection:"myconnection" ];
- r_CreateCrmQuote = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
- type :POST
- parameters: m_CreateQuote
- connection:"myconnection"
- ];
copyrawl_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" ];
- 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"
- ];
- {"code":"INVALID_DATA","details":{"expected_data_type":"jsonobject"},"message":"body","status":"error"} is because you did this:
copyrawwhen you should have done this (note the .toString()):
r_CreateCrmQuote = invokeurl [ url :"https://www.zohoapis.eu/crm/v2.1/Quotes" type :POST parameters:m_Data connection:"myconnection" ];
- r_CreateCrmQuote = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
- type :POST
- parameters:m_Data
- connection:"myconnection"
- ];
copyrawr_CreateCrmQuote = invokeurl [ url :"https://www.zohoapis.eu/crm/v2.1/Quotes" type :POST parameters:m_Data.toString() connection:"myconnection" ];
- r_CreateCrmQuote = invokeUrl
- [
- url :"https://www.zohoapis.eu/crm/v2.1/Quotes"
- type :POST
- parameters:m_Data.toString()
- connection:"myconnection"
- ];
- {"code":"INVALID_DATA","details":{"expected_data_type":"jsonarray","api_name":"data"},"message":"invalid data","status":"error"} is because you did this:
copyrawwhen you should have done this (note that a list is submitted to data / could use .toJSONList()):
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() ];
- 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()
- ];
copyrawl_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() ];
- 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()
- ];
- {"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.