Skip to content

Commit

Permalink
Added support for modules other than Leads in CRM, Added support for …
Browse files Browse the repository at this point in the history
…records attached to parent records, modified the readme to reflect these changes. Also fixed the CRM authentication to reflect new changes in the Zoho API.
  • Loading branch information
vprime committed Oct 31, 2013
1 parent 6953d9d commit a6486f6
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 43 deletions.
95 changes: 79 additions & 16 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Features
*mfabrik.zoho* is intended to use for machine-to-machine communication and
will work with any web framework, like Plone, Django, Google App Engine.

To communicate with Zoho you need username, password and API KEY.
To communicate with Zoho you need username, password or API KEY.
For further information, see *Setup > Admin > Developer key* in
your Zoho application.

Expand All @@ -33,6 +33,7 @@ API support
Currently out of box support includes:

* CRM apis: insert_records, get_records, delete_lead
* Support API: add_records

You can easily wrap Zoho API calls you need using this library.
Please contribute patches to the package.
Expand All @@ -58,43 +59,105 @@ To learn how to use this library, it is best to study its unit test source code

Example usage::

# Import CRM connector class
# Import CRM connector class for Zoho CRM
from mfabrik.zoho.crm import CRM
# Import Support connector class for Zoho Support
from mfabrik.zoho.crm import SUPPORT
# (optional) Use to raise a ZohoException in your application.
from mfabrik.zoho.core import ZohoException

# Initialize Zoho CRM API connection
# You need valid Zoho credentials and API key for this.
# You need valid Zoho API key for this.
# You get necessary data from Zoho > Setup > Admin > Developer key
crm = CRM(username="myusername", password="foobar", apikey="12312312312312312323")

# Open connection can be used to make as many as possible API calls
# This will raise ZohoException if credentials are incorrect.
# Also IOError or URLError will be raised if you the connection to Zoho servers
# does not work.
crm = CRM(authtoken="authtoken", scope="crmapi")
# same for support
support = SUPPORT(authtoken="authtoken", scope="supportapi")

# If you're going to use session tickets, include a username and password
# Then run MODULE.open() to generate the session ticket.
# This functionality is currently Deprecated for CRM & Support and will cause a 4500 error
# It might be available with other Zoho API's
crm = CRM(username="myusername", password="foobar", authtoken="12312312312312312323", scope="crmapi")
crm.open()


CRM input example::

# Lead is just a bunch of dictionaried data
# For possible lead parameters see crm.py.
# Input is just a bunch of dictionaried data
# For possible parameters see https://www.zoho.com/crm/help/api/insertrecords.html
# To discover required fields in CRM, log into zoho and visit Setup > Customization > Layouts
# Everything marked with red, or with a padlock is a required field.

# Zoho default compulsory fields: Last Name, Company
# Zoho default compulsory fields for Leads: Last Name, Company

lead = {
u"First Name" : u"Mikko",
u"Last Name" : u"Ohtamaa",
u"Company" : u"mFabrik Research Oy"
}
# Special fields *Lead Owner* must be the Email of the CRM user.

# Insert a new lead to Zoho CRM lead database.
# We support multiple leads per call, so we need to listify our one lead first.
responses = crm.insert_records([lead]) # This will raise ZohoException if lead data is invalid

responses = crm.insert_records('Leads',[lead]) # This will raise ZohoException if lead data is invalid

# To insert records attached to a parent record, like a Product on a SalesOrder
# This functionality is currently only written for the CRM.
SalesOrders = {
'Subject': "Subjuct is required", # Subject is required
'Sales Order Owner': "[email protected]", # Must be the registered Email for the CRM user
'Contact Name': Zoho ID from either Leads, Contacts, OR a string containing the Full Contact Name.
'Sub Total': "100",
'Tax': "5",
'Adjustment':"5",
'Grand Total': "110",
'Billing Street': "123 Fake Street",
'Shipping Street': "123 Fake Street",
'Billing City': "San Francisco",
'Shipping City': "San Francisco",
'Billing State': "CA",
'Shipping State': "CA",
'Billing Code': "90001",
'Shipping Code': "90001",
'Billing Country': "US",
'Shipping Country': "US",
'Product Details':{ # Add a For Each, add a list full of dictionaries contaning at least a Product Id
'product': [
{
'Product Id': '1470000001', # The Zoho ID from the item in the "Products" module
'Product Name': "Foo",
'Qty': "20",
'Unit Price': "10",
'List Price': "10",
'Total': "200",
'Discount': "100",
'Total After Discount':"100",
'Net Total':"100",
},
],
},
}
response = crm.insert_records('SalesOrders', [SalesOrders])

# list of responses. one response is {'Modified Time': '2010-10-07 13:24:49', 'Created By': 'Developer', 'Modified By': 'Developer', 'Id': '177376000000253053', 'Created Time': '2010-10-07 13:24:49'}
# At least one response is guaranteed, otherwise an exception is raised

lead_id = responses[0]["Id"]


Special field *Lead Owner* must be the registered email fo Zoho CRM user.
Support input example::

incomingData = {
'Contact Name': 'John Doe',
'Email': '[email protected]',
'Phone': '555-555-1234',
'Classification': 'Software Issue',
'Subject': 'Support request subject here',
'Description': 'Body of the support request text.'
}
response = support.add_records([incomingData], 'Department Name', 'Portal Name')
record_id = response[0]["Id"]



.. note::
Expand Down
47 changes: 33 additions & 14 deletions mfabrik/zoho/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ class Connection(object):
Subclass this and override necessary methods to support different Zoho API groups.
"""


def __init__(self, username, password, authtoken, scope, extra_auth_params = {}, auth_url="https://accounts.zoho.com/login"):
def __init__(self, **kwargs):
"""
@param username: [email protected]
Expand All @@ -66,15 +65,29 @@ def __init__(self, username, password, authtoken, scope, extra_auth_params = {},
@param extra_auth_params: Dictionary of optional HTTP POST parameters passed to the login call
@param auth_url: Which URL we use for authentication
"""
self.username = username
self.password = password
self.authtoken = authtoken
self.scope = scope
#
self.auth_url = None
options = {
'username': None,
'password': None,
'authtoken': None,
'auth_url': "https://accounts.zoho.com/login",
'scope': None
}
options.update(kwargs)
if options['username'] is not None and options['password'] is not None:
self.username = options["username"]
self.password = options['password']

if options['authtoken']:
self.authtoken = options["authtoken"]

self.auth_url = options['auth_url']

if options['scope'] is not None:
self.scope = options["scope"]
else:
raise ZohoException("No Scope")

# Ticket is none until the conneciton is opened
self.ticket = None
Expand Down Expand Up @@ -149,9 +162,11 @@ def _parse_ticket_response(self, data):

def ensure_opened(self):
""" Make sure that the Zoho Connection is correctly opened """

if self.ticket is None:
raise ZohoException("Need to initialize Zoho ticket first")
if hasattr(self, 'username') and hasattr(self, 'password') and not hasattr(self, 'authtoken'):
if self.ticket is None:
raise ZohoException("Need to initialize Zoho ticket first")
else:
return

def do_xml_call(self, url, parameters, root):
""" Do Zoho API call with outgoing XML payload.
Expand All @@ -178,7 +193,8 @@ def do_call(self, url, parameters):
"""
# Do not mutate orginal dict
parameters = parameters.copy()
parameters["ticket"] = self.ticket
if self.ticket != None:
parameters["ticket"] = self.ticket
parameters["authtoken"] = self.authtoken
parameters["scope"] = self.scope

Expand All @@ -189,7 +205,8 @@ def do_call(self, url, parameters):
logger.debug("Doing ZOHO API call:" + url)
for key, value in parameters.items():
logger.debug(key + ": " + value)

self.parameters = parameters
self.parameters_encoded = urllib.urlencode(parameters)
request = urllib2.Request(url, urllib.urlencode(parameters))
response = urllib2.urlopen(request).read()

Expand Down Expand Up @@ -218,6 +235,8 @@ def check_successful_xml(self, response):
# Check error response
# <response uri="/crm/private/xml/Leads/insertRecords"><error><code>4401</code><message>Unable to populate data, please check if mandatory value is entered correctly.</message></error></response>
for error in root.findall("error"):
parameters = self.parameters
parameters_encoded = self.parameters_encoded
print "Got error"
for message in error.findall("message"):
raise ZohoException(message.text)
Expand Down
32 changes: 21 additions & 11 deletions mfabrik/zoho/crm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@

try:
from xml import etree
from xml.etree.ElementTree import Element, tostring, fromstring
from xml.etree.ElementTree import Element, tostring, fromstring, SubElement
except ImportError:
try:
from lxml import etree
from lxml.etree import Element, tostring, fromstring
from lxml.etree import Element, tostring, fromstring, SubElement
except ImportError:
raise RuntimeError("XML library not available: no etree, no lxml")

Expand All @@ -32,7 +32,7 @@ def get_service_name(self):
""" Called by base class """
return "ZohoCRM"

def insert_records(self, leads, extra_post_parameters={}):
def insert_records(self, module, leads, extra_post_parameters={}):
""" Insert new leads to Zoho CRM database.
The contents of the lead parameters can be defined in Zoho CRM itself.
Expand All @@ -46,11 +46,10 @@ def insert_records(self, leads, extra_post_parameters={}):
Described in Zoho CRM API.
@return: List of record ids which were created by insert recoreds
"""

"""
self.ensure_opened()

root = Element("Leads")
root = Element(module)

# Row counter
no = 1
Expand All @@ -64,9 +63,22 @@ def insert_records(self, leads, extra_post_parameters={}):
for key, value in lead.items():
# <FL val="Lead Source">Web Download</FL>
# <FL val="First Name">contacto 1</FL>
fl = Element("fl", val=key)
fl.text = value
fl = Element("FL", val=key)
if type(value) == dict: # If it's an attached module, accept multiple groups
mod_attach_no = 1
for module_key, module_value in value.items(): # The first group defines the module name, yank that and iterate through the contents
for mod_item in module_value:
mod_fl = SubElement(fl, module_key, no=str(mod_attach_no))
for mod_item_key, mod_item_value in mod_item.items():
attach_fl = SubElement(mod_fl, "FL", val=mod_item_key)
attach_fl.text = mod_item_value
mod_attach_no += 1
elif type(value) != str:
fl.text = str(value)
else:
fl.text = value
row.append(fl)


no += 1

Expand All @@ -77,7 +89,7 @@ def insert_records(self, leads, extra_post_parameters={}):

post.update(extra_post_parameters)

response = self.do_xml_call("https://crm.zoho.com/crm/private/xml/Leads/insertRecords", post, root)
response = self.do_xml_call("https://crm.zoho.com/crm/private/xml/" + module + "/insertRecords", post, root)

self.check_successful_xml(response)

Expand All @@ -98,7 +110,6 @@ def get_records(self, selectColumns='leads(First Name,Last Name,Company)', param
@return: Python list of dictionarizied leads. Each dictionary contains lead key-value pairs. LEADID column is always included.
"""

self.ensure_opened()


Expand Down Expand Up @@ -133,7 +144,6 @@ def delete_record(self, id, parameters={}):
@param parameters: Extra HTTP post parameters
"""

self.ensure_opened()

post_params = {}
Expand Down
4 changes: 2 additions & 2 deletions mfabrik/zoho/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"""

__copyright__ = "2013 Local Motors, mFabrik Research Oy"
__author__ = "Vincent Prime <vprime@local-motors.com>, Mikko Ohtamaa <[email protected]>"
__copyright__ = "2013 mFabrik Research Oy"
__author__ = "Vincent Prime <myself@vincentprime.com>, Mikko Ohtamaa <[email protected]>"
__license__ = "GPL"
__docformat__ = "Epytext"

Expand Down

0 comments on commit a6486f6

Please sign in to comment.