Merge pull request #8359 from hrwX/google_cont
feat(Integrations): Google Contacts
This commit is contained in:
commit
0a46aa2fa4
6 changed files with 269 additions and 77 deletions
|
|
@ -61,6 +61,15 @@ frappe.ui.form.on("Contact", {
|
|||
}
|
||||
}
|
||||
]);
|
||||
},
|
||||
sync_with_google_contacts: function(frm) {
|
||||
if (frm.doc.sync_with_google_contacts) {
|
||||
frappe.db.get_value("Google Contacts", {"email_id": frappe.session.user}, "name", (r) => {
|
||||
if (r && r.name) {
|
||||
frm.set_value("google_contacts", r.name);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,21 @@
|
|||
"email_id",
|
||||
"user",
|
||||
"address",
|
||||
"sync_with_google_contacts",
|
||||
"cb00",
|
||||
"status",
|
||||
"salutation",
|
||||
"designation",
|
||||
"gender",
|
||||
"phone",
|
||||
"company_name",
|
||||
"image",
|
||||
"sb_00",
|
||||
"google_contacts",
|
||||
"google_contacts_id",
|
||||
"cb_00",
|
||||
"pulled_from_google_contacts",
|
||||
"sb_01",
|
||||
"email_ids",
|
||||
"phone_nos",
|
||||
"contact_details",
|
||||
|
|
@ -29,10 +36,7 @@
|
|||
"links",
|
||||
"more_info",
|
||||
"department",
|
||||
"unsubscribed",
|
||||
"column_break_17",
|
||||
"source",
|
||||
"google_contacts_description"
|
||||
"unsubscribed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -155,36 +159,23 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Designation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unsubscribed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Unsubscribed"
|
||||
},
|
||||
{
|
||||
"fieldname": "source",
|
||||
"fieldtype": "Data",
|
||||
"label": "Source"
|
||||
},
|
||||
{
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Middle Name"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.source==\"Google Contacts\"",
|
||||
"fieldname": "google_contacts_description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Google Contacts Description"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:doc.sync_with_google_contacts || doc.pulled_from_google_contacts",
|
||||
"fieldname": "sb_00",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Details"
|
||||
"label": "Google Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "email_ids",
|
||||
|
|
@ -203,13 +194,52 @@
|
|||
"fieldtype": "Table",
|
||||
"label": "Phone Nos",
|
||||
"options": "Contact Phone"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "pulled_from_google_contacts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pulled from Google Contacts",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "sync_with_google_contacts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sync with Google Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "google_contacts",
|
||||
"fieldtype": "Link",
|
||||
"label": "Google Contacts",
|
||||
"options": "Google Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_00",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_01",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "google_contacts_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Google Contacts Id",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Company Name"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 1,
|
||||
"image_field": "image",
|
||||
"modified": "2019-08-09 10:23:00.486673",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2019-09-13 15:50:38.999884",
|
||||
"modified_by": "himanshu@erpnext.com",
|
||||
"module": "Contacts",
|
||||
"name": "Contact",
|
||||
"name_case": "Title Case",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ class Contact(Document):
|
|||
if self.email_id and not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
if self.sync_with_google_contacts and not self.google_contacts:
|
||||
frappe.throw(_("Select Google Contacts to which contact should be synced."))
|
||||
|
||||
deduplicate_dynamic_links(self)
|
||||
|
||||
def set_user(self):
|
||||
|
|
|
|||
|
|
@ -136,6 +136,15 @@ doc_events = {
|
|||
"on_change": [
|
||||
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
|
||||
],
|
||||
},
|
||||
"Event": {
|
||||
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",
|
||||
"on_update": "frappe.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar",
|
||||
"on_trash": "frappe.integrations.doctype.google_calendar.google_calendar.delete_event_from_google_calendar",
|
||||
},
|
||||
"Contact": {
|
||||
"after_insert": "frappe.integrations.doctype.google_contacts.google_contacts.insert_contacts_to_google_contacts",
|
||||
"on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@
|
|||
"cb_00",
|
||||
"last_sync_on",
|
||||
"authorization_code",
|
||||
"refresh_token"
|
||||
"refresh_token",
|
||||
"next_sync_token",
|
||||
"sync",
|
||||
"pull_from_google_contacts",
|
||||
"column_break_12",
|
||||
"push_to_google_contacts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -62,10 +67,38 @@
|
|||
"fieldname": "authorize_google_contacts_access",
|
||||
"fieldtype": "Button",
|
||||
"label": "Authorize Google Contacts Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "next_sync_token",
|
||||
"fieldtype": "Password",
|
||||
"hidden": 1,
|
||||
"label": "Next Sync Token"
|
||||
},
|
||||
{
|
||||
"depends_on": "enable",
|
||||
"fieldname": "sync",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sync"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "pull_from_google_contacts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pull from Google Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "push_to_google_contacts",
|
||||
"fieldtype": "Check",
|
||||
"label": "Push to Google Contacts"
|
||||
}
|
||||
],
|
||||
"modified": "2019-08-23 13:50:52.789503",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2019-09-13 15:53:19.569924",
|
||||
"modified_by": "himanshu@erpnext.com",
|
||||
"module": "Integrations",
|
||||
"name": "Google Contacts",
|
||||
"owner": "Administrator",
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import requests
|
||||
import googleapiclient.discovery
|
||||
import google.oauth2.credentials
|
||||
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
from googleapiclient.errors import HttpError
|
||||
from frappe.utils import get_request_site_address
|
||||
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
|
||||
|
||||
SCOPES = "https://www.googleapis.com/auth/contacts"
|
||||
REQUEST = "https://people.googleapis.com/v1/people/me/connections"
|
||||
PARAMS = {"personFields": "names,emailAddresses,organizations,phoneNumbers"}
|
||||
|
||||
class GoogleContacts(Document):
|
||||
|
||||
|
|
@ -60,7 +62,7 @@ def authorize_access(g_contact, reauthorize=None):
|
|||
|
||||
if not google_contact.authorization_code or reauthorize:
|
||||
frappe.cache().hset("google_contacts", "google_contact", google_contact.name)
|
||||
return google_callback(client_id=google_settings.client_id, redirect_uri=redirect_uri)
|
||||
return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri)
|
||||
else:
|
||||
try:
|
||||
data = {
|
||||
|
|
@ -83,21 +85,42 @@ def authorize_access(g_contact, reauthorize=None):
|
|||
except Exception as e:
|
||||
frappe.throw(e)
|
||||
|
||||
def get_authentication_url(client_id=None, redirect_uri=None):
|
||||
return {
|
||||
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri)
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def google_callback(client_id=None, redirect_uri=None, code=None):
|
||||
def google_callback(code=None):
|
||||
"""
|
||||
Authorization code is sent to callback as per the API configuration
|
||||
"""
|
||||
if code is None:
|
||||
return {
|
||||
"url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri)
|
||||
}
|
||||
else:
|
||||
google_contact = frappe.cache().hget("google_contacts", "google_contact")
|
||||
frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code)
|
||||
frappe.db.commit()
|
||||
google_contact = frappe.cache().hget("google_contacts", "google_contact")
|
||||
frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code)
|
||||
frappe.db.commit()
|
||||
|
||||
authorize_access(google_contact)
|
||||
authorize_access(google_contact)
|
||||
|
||||
def get_google_contacts_object(g_contact):
|
||||
"""
|
||||
Returns an object of Google Calendar along with Google Calendar doc.
|
||||
"""
|
||||
google_settings = frappe.get_doc("Google Settings")
|
||||
account = frappe.get_doc("Google Contacts", g_contact)
|
||||
|
||||
credentials_dict = {
|
||||
"token": account.get_access_token(),
|
||||
"refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False),
|
||||
"token_uri": get_auth_url(),
|
||||
"client_id": google_settings.client_id,
|
||||
"client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False),
|
||||
"scopes": "https://www.googleapis.com/auth/contacts"
|
||||
}
|
||||
|
||||
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
|
||||
google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials)
|
||||
|
||||
return google_contacts, account
|
||||
|
||||
@frappe.whitelist()
|
||||
def sync(g_contact=None):
|
||||
|
|
@ -108,58 +131,143 @@ def sync(g_contact=None):
|
|||
|
||||
google_contacts = frappe.get_list("Google Contacts", filters=filters)
|
||||
|
||||
for google_contact in google_contacts:
|
||||
doc = frappe.get_doc("Google Contacts", google_contact.name)
|
||||
access_token = doc.get_access_token()
|
||||
for g in google_contacts:
|
||||
return sync_contacts_from_google_contacts(g.name)
|
||||
|
||||
headers = {"Authorization": "Bearer {}".format(access_token)}
|
||||
def sync_contacts_from_google_contacts(g_contact):
|
||||
"""
|
||||
Syncs Contacts from Google Contacts.
|
||||
https://developers.google.com/people/api/rest/v1/people.connections/list
|
||||
"""
|
||||
google_contacts, account = get_google_contacts_object(g_contact)
|
||||
|
||||
if not account.pull_from_google_contacts:
|
||||
return
|
||||
|
||||
results = []
|
||||
contacts_updated = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
r = requests.get(REQUEST, headers=headers, params=PARAMS)
|
||||
except Exception as e:
|
||||
frappe.throw(e)
|
||||
sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None
|
||||
contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token,
|
||||
personFields="names,emailAddresses,organizations,phoneNumbers").execute()
|
||||
except HttpError as err:
|
||||
frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))
|
||||
|
||||
try:
|
||||
r = r.json()
|
||||
except Exception as e:
|
||||
# if request doesn't return json show HTML ask permissions or to identify the error on google side
|
||||
frappe.throw(e)
|
||||
for contact in contacts.get("connections"):
|
||||
results.append(contact)
|
||||
|
||||
connections = r.get("connections")
|
||||
contacts_updated = 0
|
||||
if not contacts.get("nextPageToken"):
|
||||
if contacts.get("nextSyncToken"):
|
||||
frappe.db.set_value("Google Contacts", account.name, "next_sync_token", contacts.get("nextSyncToken"))
|
||||
frappe.db.commit()
|
||||
break
|
||||
|
||||
frappe.db.set_value("Google Contacts", doc.name, "last_sync_on", frappe.utils.now_datetime())
|
||||
frappe.db.set_value("Google Contacts", account.name, "last_sync_on", frappe.utils.now_datetime())
|
||||
|
||||
if connections:
|
||||
for idx, connection in enumerate(connections):
|
||||
frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user)
|
||||
for idx, connection in enumerate(results):
|
||||
frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=len(results)), user=frappe.session.user)
|
||||
|
||||
for name in connection.get("names"):
|
||||
if name.get("metadata").get("primary"):
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"salutation": name.get("honorificPrefix") or "",
|
||||
"first_name": name.get("givenName") or "",
|
||||
"middle_name": name.get("middleName") or "",
|
||||
"last_name": name.get("familyName") or "",
|
||||
"designation": get_indexed_value(connection.get("organizations"), 0, "title"),
|
||||
"source": "Google Contacts",
|
||||
"google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name")
|
||||
})
|
||||
for name in connection.get("names"):
|
||||
if name.get("metadata").get("primary"):
|
||||
contacts_updated += 1
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": name.get("givenName") or "",
|
||||
"middle_name": name.get("middleName") or "",
|
||||
"last_name": name.get("familyName") or "",
|
||||
"designation": get_indexed_value(connection.get("organizations"), 0, "title"),
|
||||
"pulled_from_google_contacts": 1,
|
||||
"google_contacts": account.name,
|
||||
"company_name": get_indexed_value(connection.get("organizations"), 0, "name")
|
||||
})
|
||||
|
||||
for email in connection.get("emailAddresses", []):
|
||||
contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("primary") else 0)
|
||||
for email in connection.get("emailAddresses", []):
|
||||
contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0)
|
||||
|
||||
for phone in connection.get("phoneNumbers", []):
|
||||
contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("primary") else 0)
|
||||
for phone in connection.get("phoneNumbers", []):
|
||||
contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("metadata").get("primary") else 0)
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.insert(ignore_permissions=True)
|
||||
|
||||
if g_contact:
|
||||
return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.")
|
||||
return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 \
|
||||
else _("No new Google Contacts synced.")
|
||||
|
||||
if g_contact:
|
||||
return _("No Google Contacts present to sync.") # If no Google Contacts to sync
|
||||
def insert_contacts_to_google_contacts(doc, method=None):
|
||||
"""
|
||||
Syncs Contacts from Google Contacts.
|
||||
https://developers.google.com/people/api/rest/v1/people/createContact
|
||||
"""
|
||||
if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.pulled_from_google_contacts \
|
||||
or not doc.sync_with_google_contacts:
|
||||
return
|
||||
|
||||
google_contacts, account = get_google_contacts_object(doc.google_contacts)
|
||||
|
||||
if not account.push_to_google_contacts:
|
||||
return
|
||||
|
||||
names = {
|
||||
"givenName": doc.first_name,
|
||||
"middleName": doc.middle_name,
|
||||
"familyName": doc.last_name
|
||||
}
|
||||
|
||||
phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos]
|
||||
emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids]
|
||||
|
||||
try:
|
||||
contact = google_contacts.people().createContact(parent='people/me', body={"names": [names],"phoneNumbers": phoneNumbers,
|
||||
"emailAddresses": emailAddresses}).execute()
|
||||
frappe.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName"))
|
||||
except HttpError as err:
|
||||
frappe.msgprint(_("Google Calendar - Could not insert contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))
|
||||
|
||||
def update_contacts_to_google_contacts(doc, method=None):
|
||||
"""
|
||||
Syncs Contacts from Google Contacts.
|
||||
https://developers.google.com/people/api/rest/v1/people/updateContact
|
||||
"""
|
||||
# Workaround to avoid triggering updation when Event is being inserted since
|
||||
# creation and modified are same when inserting doc
|
||||
if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.is_new() \
|
||||
or not doc.sync_with_google_contacts:
|
||||
return
|
||||
|
||||
if doc.sync_with_google_contacts and not doc.google_contacts_id:
|
||||
# If sync_with_google_contacts is checked later, then insert the contact rather than updating it.
|
||||
insert_contacts_to_google_contacts(doc)
|
||||
return
|
||||
|
||||
google_contacts, account = get_google_contacts_object(doc.google_contacts)
|
||||
|
||||
if not account.push_to_google_contacts:
|
||||
return
|
||||
|
||||
names = {
|
||||
"givenName": doc.first_name,
|
||||
"middleName": doc.middle_name,
|
||||
"familyName": doc.last_name
|
||||
}
|
||||
|
||||
phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos]
|
||||
emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids]
|
||||
|
||||
try:
|
||||
contact = google_contacts.people().get(resourceName=doc.google_contacts_id, \
|
||||
personFields="names,emailAddresses,organizations,phoneNumbers").execute()
|
||||
|
||||
contact["names"] = [names]
|
||||
contact["phoneNumbers"] = phoneNumbers
|
||||
contact["emailAddresses"] = emailAddresses
|
||||
|
||||
google_contacts.people().updateContact(resourceName=doc.google_contacts_id,body={"names":[names],
|
||||
"phoneNumbers":phoneNumbers,"emailAddresses":emailAddresses,"etag":contact.get("etag")},
|
||||
updatePersonFields="names,emailAddresses,organizations,phoneNumbers").execute()
|
||||
frappe.msgprint(_("Contact Synced with Google Contacts."))
|
||||
except HttpError as err:
|
||||
frappe.msgprint(_("Google Contacts - Could not update contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status))
|
||||
|
||||
def get_indexed_value(d, index, key):
|
||||
if not d:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue