Merge branch 'develop' into social-media-integration-feat
diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/__init__.py
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
new file mode 100644
index 0000000..50b98e9
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -0,0 +1,71 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('LinkedIn Settings', {
+	onload: function(frm){
+		if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
+			frappe.confirm(
+				__('Session not valid, Do you want to login?'),
+				function(){
+					frm.trigger("login");
+				},
+				function(){
+					window.close();
+				}
+			);
+		}
+	},
+	refresh: function(frm){
+		if (frm.doc.session_status=="Expired"){
+			let msg = __("Session Not Active. Save doc to login.");
+			frm.dashboard.set_headline_alert(
+				`<div class="row">
+					<div class="col-xs-12">
+						<span class="indicator whitespace-nowrap red"><span class="hidden-xs">${msg}</span></span>
+					</div>
+				</div>`
+			);
+		}
+
+		if (frm.doc.session_status=="Active"){
+			let d = new Date(frm.doc.modified);
+			d.setDate(d.getDate()+60);
+			let dn = new Date();
+			let days = d.getTime() - dn.getTime();
+			days = Math.floor(days/(1000 * 3600 * 24));
+			let msg,color;
+
+			if (days>0){
+				msg = __("Your Session will be expire in ") + days + __(" days.");
+				color = "green";
+			}
+			else {
+				msg = __("Session is expired. Save doc to login.");
+				color = "red";
+			}
+
+			frm.dashboard.set_headline_alert(
+				`<div class="row">
+					<div class="col-xs-12">
+						<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
+					</div>
+				</div>`
+			);
+		}
+	},
+	login: function(frm){
+		if (frm.doc.consumer_key && frm.doc.consumer_secret){
+			frappe.dom.freeze();
+			frappe.call({
+				doc: frm.doc,
+				method: "get_authorization_url",
+				callback : function(r) {
+					window.location.href = r.message;
+				}
+			});
+		}
+	},
+	after_save: function(frm){
+		frm.trigger("login");
+	}
+});
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
new file mode 100644
index 0000000..9eacb00
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
@@ -0,0 +1,111 @@
+{
+ "actions": [],
+ "creation": "2020-01-30 13:36:39.492931",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "account_name",
+  "column_break_2",
+  "company_id",
+  "oauth_details",
+  "consumer_key",
+  "column_break_5",
+  "consumer_secret",
+  "user_details_section",
+  "access_token",
+  "person_urn",
+  "session_status"
+ ],
+ "fields": [
+  {
+   "fieldname": "account_name",
+   "fieldtype": "Data",
+   "label": "Account Name",
+   "read_only": 1
+  },
+  {
+   "fieldname": "oauth_details",
+   "fieldtype": "Section Break",
+   "label": "OAuth Credentials"
+  },
+  {
+   "fieldname": "consumer_key",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Consumer Key",
+   "reqd": 1
+  },
+  {
+   "fieldname": "consumer_secret",
+   "fieldtype": "Password",
+   "in_list_view": 1,
+   "label": "Consumer Secret",
+   "reqd": 1
+  },
+  {
+   "fieldname": "access_token",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Access Token",
+   "read_only": 1
+  },
+  {
+   "fieldname": "person_urn",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Person URN",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_5",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "user_details_section",
+   "fieldtype": "Section Break",
+   "label": "User Details"
+  },
+  {
+   "fieldname": "session_status",
+   "fieldtype": "Select",
+   "hidden": 1,
+   "label": "Session Status",
+   "options": "Expired\nActive",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company_id",
+   "fieldtype": "Data",
+   "label": "Company ID",
+   "reqd": 1
+  }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-04-16 23:22:51.966397",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "LinkedIn Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
new file mode 100644
index 0000000..5df35df
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -0,0 +1,166 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, requests, json
+from frappe import _
+from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
+from frappe.model.document import Document
+from frappe.utils.file_manager import get_file, get_file_path
+from six.moves.urllib.parse import urlencode
+
+class LinkedInSettings(Document):
+	def get_authorization_url(self):	
+		params = urlencode({
+			"response_type":"code",
+			"client_id": self.consumer_key,
+			"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
+			"scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social"
+		})
+
+		url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params)
+
+		return url
+
+	def get_access_token(self, code):
+		url = "https://www.linkedin.com/oauth/v2/accessToken"
+		body = {
+			"grant_type": "authorization_code",
+			"code": code,
+			"client_id": self.consumer_key,
+			"client_secret": self.get_password(fieldname="consumer_secret"),
+			"redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback",
+		}
+		headers = {
+			"Content-Type": "application/x-www-form-urlencoded"
+		}
+		
+		response = self.http_post(url=url, data=body, headers=headers)
+		response = frappe.parse_json(response.content.decode())
+		self.db_set("access_token", response["access_token"])
+
+	def get_member_profile(self):
+		headers = {
+			"Authorization": "Bearer {}".format(self.access_token)
+		}
+		url = "https://api.linkedin.com/v2/me"
+		response = requests.get(url=url, headers=headers)
+		response = frappe.parse_json(response.content.decode())
+
+		frappe.db.set_value(self.doctype, self.name, {
+			"person_urn": response["id"],
+			"account_name": response["vanityName"],
+			"session_status": "Active"
+		})
+		frappe.local.response["type"] = "redirect"
+		frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
+
+	def post(self, text, media=None):
+		if not media:
+			return self.post_text(text)
+		else:
+			media_id = self.upload_image(media)
+
+			if media_id:
+				return self.post_text(text, media_id=media_id)
+			else:
+				frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
+
+
+	def upload_image(self, media):
+		media = get_file_path(media)
+		register_url = "https://api.linkedin.com/v2/assets?action=registerUpload"
+		body = {
+			"registerUploadRequest": {
+				"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
+				"owner": "urn:li:organization:{0}".format(self.company_id),
+				"serviceRelationships": [{
+					"relationshipType": "OWNER",
+					"identifier": "urn:li:userGeneratedContent"
+				}]
+			}
+		}
+		headers = {
+			"Authorization": "Bearer {}".format(self.access_token)
+		}
+		response = self.http_post(url=register_url, body=body, headers=headers)
+
+		if response.status_code == 200:
+			response = response.json()
+			asset = response["value"]["asset"]
+			upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"]
+			headers['Content-Type']='image/jpeg'
+			response = self.http_post(upload_url, headers=headers, data=open(media,"rb"))
+			if response.status_code < 200 and response.status_code > 299:
+				frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason))
+				return None
+			return asset
+
+		return None
+
+	def post_text(self, text, media_id=None):
+		url = "https://api.linkedin.com/v2/shares"
+		headers = {
+			"X-Restli-Protocol-Version": "2.0.0",
+			"Authorization": "Bearer {}".format(self.access_token),
+			"Content-Type": "application/json; charset=UTF-8"
+		}
+		body = {
+			"distribution": {
+				"linkedInDistributionTarget": {}
+			},
+			"owner":"urn:li:organization:{0}".format(self.company_id),
+			"subject": "Test Share Subject",
+			"text": {
+				"text": text
+			}
+		}
+
+		if media_id:
+			body["content"]= {
+				"contentEntities": [{
+					"entity": media_id
+				}],
+				"shareMediaCategory": "IMAGE"
+			}
+
+		response = self.http_post(url=url, headers=headers, body=body)
+		return response
+
+	def http_post(self, url, headers=None, body=None, data=None):
+		try:
+			response = requests.post(
+				url = url,
+				json = body,
+				data = data,
+				headers = headers
+			)
+			if response.status_code not in [201,200]:
+				raise
+
+		except Exception as e:
+			content = json.loads(response.content)
+
+			if response.status_code == 401:
+				self.db_set("session_status", "Expired")
+				frappe.db.commit()
+				frappe.throw(content["message"], title="LinkedIn Error - Unauthorized")
+			elif response.status_code == 403:
+				frappe.msgprint(_("You Didn't have permission to access this API"))
+				frappe.throw(content["message"], title="LinkedIn Error - Access Denied")
+			else:
+				frappe.throw(response.reason, title=response.status_code)
+
+		return response
+
+@frappe.whitelist()
+def callback(code=None, error=None, error_description=None):
+	if not error:
+		linkedin_settings = frappe.get_doc("LinkedIn Settings")
+		linkedin_settings.get_access_token(code)
+		linkedin_settings.get_member_profile()
+		frappe.db.commit()
+	else:
+		frappe.local.response["type"] = "redirect"
+		frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
new file mode 100644
index 0000000..9c3ef3f
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestLinkedInSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/__init__.py
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
new file mode 100644
index 0000000..3a14f2d
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.js
@@ -0,0 +1,67 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.ui.form.on('Social Media Post', {
+    validate: function(frm){
+        if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){
+            frappe.throw(__("Select atleast one Social Media from Share on."))
+        }
+        if (frm.doc.scheduled_time) {
+            let scheduled_time = new Date(frm.doc.scheduled_time);
+            let date_time = new Date();
+            if (scheduled_time.getTime() < date_time.getTime()){
+                frappe.throw(__("Invalid Scheduled Time"));
+            }
+        }
+        if (frm.doc.text?.length > 280){
+            frappe.throw(__("Length Must be less than 280."))
+        }
+    },
+	refresh: function(frm){
+        if (frm.doc.docstatus === 1){
+            if (frm.doc.post_status != "Posted"){
+                add_post_btn(frm); 
+            }
+            else if (frm.doc.post_status == "Posted"){
+                frm.set_df_property('sheduled_time', 'read_only', 1);
+            }
+
+            let html='';
+            if (frm.doc.twitter){
+                let color = frm.doc.twitter_post_id ? "green" : "red";
+                let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
+                html += `<div class="col-xs-6">
+                            <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">Twitter : ${status} </span></span>
+                        </div>` ;
+            }
+            if (frm.doc.linkedin){
+                let color = frm.doc.linkedin_post_id ? "green" : "red";
+                let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
+                html += `<div class="col-xs-6">
+                            <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">LinkedIn : ${status} </span></span>
+                        </div>` ;
+            }
+            html = `<div class="row">${html}</div>`;
+            frm.dashboard.set_headline_alert(html);
+        }
+    }
+});
+var add_post_btn = function(frm){
+    frm.add_custom_button(('Post Now'), function(){
+        post(frm);
+    });
+}
+var post = function(frm){
+    frappe.dom.freeze();
+    frappe.call({
+        method: "erpnext.crm.doctype.social_media_post.social_media_post.publish",
+        args: {
+            doctype: frm.doc.doctype,
+            name: frm.doc.name
+        },
+        callback: function(r) {
+            frm.reload_doc();
+            frappe.dom.unfreeze();
+        }
+    })
+    
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json
new file mode 100644
index 0000000..2601c14
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.json
@@ -0,0 +1,166 @@
+{
+ "actions": [],
+ "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
+ "creation": "2020-01-30 11:53:13.872864",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "campaign_name",
+  "scheduled_time",
+  "post_status",
+  "column_break_6",
+  "twitter",
+  "linkedin",
+  "twitter_post_id",
+  "linkedin_post_id",
+  "content",
+  "text",
+  "column_break_14",
+  "tweet_preview",
+  "linkedin_section",
+  "linkedin_post",
+  "column_break_15",
+  "attachments_section",
+  "image",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "text",
+   "fieldtype": "Small Text",
+   "label": "Tweet",
+   "mandatory_depends_on": "eval:doc.twitter ==1"
+  },
+  {
+   "fieldname": "image",
+   "fieldtype": "Attach Image",
+   "label": "Image"
+  },
+  {
+   "default": "0",
+   "fieldname": "twitter",
+   "fieldtype": "Check",
+   "label": "Twitter"
+  },
+  {
+   "default": "0",
+   "fieldname": "linkedin",
+   "fieldtype": "Check",
+   "label": "LinkedIn"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Social Media Post",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "depends_on": "eval:doc.twitter ==1",
+   "fieldname": "content",
+   "fieldtype": "Section Break",
+   "label": "Twitter"
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "post_status",
+   "fieldtype": "Select",
+   "label": "Post Status",
+   "options": "\nScheduled\nPosted\nError",
+   "read_only": 1
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "twitter_post_id",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Twitter Post Id",
+   "read_only": 1
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "linkedin_post_id",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "LinkedIn Post Id",
+   "read_only": 1
+  },
+  {
+   "fieldname": "campaign_name",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Campaign",
+   "options": "Campaign"
+  },
+  {
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break",
+   "label": "Share On"
+  },
+  {
+   "fieldname": "column_break_14",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "tweet_preview",
+   "fieldtype": "HTML"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "eval:doc.linkedin==1",
+   "fieldname": "linkedin_section",
+   "fieldtype": "Section Break",
+   "label": "LinkedIn"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "attachments_section",
+   "fieldtype": "Section Break",
+   "label": "Attachments"
+  },
+  {
+   "fieldname": "linkedin_post",
+   "fieldtype": "Text",
+   "label": "Post",
+   "mandatory_depends_on": "eval:doc.linkedin ==1"
+  },
+  {
+   "fieldname": "column_break_15",
+   "fieldtype": "Column Break"
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "scheduled_time",
+   "fieldtype": "Datetime",
+   "label": "Scheduled Time",
+   "read_only_depends_on": "eval:doc.post_status == \"Posted\""
+  }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-04-21 15:10:04.953713",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Social Media Post",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py
new file mode 100644
index 0000000..ed1b583
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe import _
+import datetime
+
+class SocialMediaPost(Document):
+	def validate(self):
+		if self.scheduled_time:
+			current_time = frappe.utils.now_datetime()
+			scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
+			if scheduled_time < current_time:
+				frappe.throw(_("Invalid Scheduled Time"))
+
+	def submit(self):
+		if self.scheduled_time:
+			self.post_status = "Scheduled"
+		super(SocialMediaPost, self).submit()
+
+	def post(self):
+		try:
+			if self.twitter and not self.twitter_post_id:
+				twitter = frappe.get_doc("Twitter Settings")
+				twitter_post = twitter.post(self.text, self.image)
+				self.db_set("twitter_post_id", twitter_post.id)
+			if self.linkedin and not self.linkedin_post_id:
+				linkedin = frappe.get_doc("LinkedIn Settings")
+				linkedin_post = linkedin.post(self.linkedin_post, self.image)
+				self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1])
+			self.db_set("post_status", "Posted")
+
+		except:
+			self.db_set("post_status", "Error")
+			title = _("Error while POSTING {0}").format(self.name)
+			traceback = frappe.get_traceback()
+			frappe.log_error(message=traceback , title=title)
+
+def process_scheduled_social_media_posts():
+	posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"])
+	start = frappe.utils.now_datetime()
+	end = start + datetime.timedelta(minutes=10)
+	for post in posts:
+		if post.scheduled_time:
+			post_time = frappe.utils.get_datetime(post.scheduled_time)
+			if post_time > start and post_time <= end:
+				publish('Social Media Post', post.name)
+
+@frappe.whitelist()
+def publish(doctype, name):
+	sm_post = frappe.get_doc(doctype, name)
+	sm_post.post()
+	frappe.db.commit()
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js
new file mode 100644
index 0000000..c60b91a
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js
@@ -0,0 +1,10 @@
+frappe.listview_settings['Social Media Post'] = {
+    add_fields: ["status","post_status"],
+    get_indicator: function(doc) {
+        return [__(doc.post_status), {
+            "Scheduled": "orange",
+            "Posted": "green",
+            "Error": "red"
+            }[doc.post_status]];
+        }
+}
diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py
new file mode 100644
index 0000000..ec81ee5
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/test_social_media_post.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestSocialMediaPost(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/__init__.py
diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
new file mode 100644
index 0000000..3f999c1
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestTwitterSettings(unittest.TestCase):
+	pass
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
new file mode 100644
index 0000000..8f9c419
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Twitter Settings', {
+	onload: function(frm){
+		if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
+			frappe.confirm(
+				__('Session not valid, Do you want to login?'),
+				function(){
+					frm.trigger("login");
+				},
+				function(){
+					window.close();
+				}
+			);
+		}
+	},
+	refresh: function(frm){
+		let msg,color;
+		if (frm.doc.session_status == "Active"){
+			msg = __("Session Active");
+			color = 'green';
+		}
+		else {
+			msg = __("Session Not Active. Save doc to login.");
+			color = 'red';
+		}
+
+		frm.dashboard.set_headline_alert(
+			`<div class="row">
+				<div class="col-xs-12">
+					<span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
+				</div>
+			</div>`
+		);
+	},
+	login: function(frm){
+		if (frm.doc.consumer_key && frm.doc.consumer_secret){
+			frappe.dom.freeze();
+			frappe.call({
+				doc: frm.doc,
+				method: "get_authorize_url",
+				callback : function(r) {
+					window.location.href = r.message;
+				}
+			});
+		}
+	},
+	after_save: function(frm){
+		frm.trigger("login");
+	}
+});
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
new file mode 100644
index 0000000..f92e7f0
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
@@ -0,0 +1,101 @@
+{
+ "actions": [],
+ "creation": "2020-01-30 10:29:08.562108",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "account_name",
+  "profile_pic",
+  "oauth_details",
+  "consumer_key",
+  "column_break_5",
+  "consumer_secret",
+  "oauth_token",
+  "oauth_secret",
+  "session_status"
+ ],
+ "fields": [
+  {
+   "fieldname": "account_name",
+   "fieldtype": "Data",
+   "label": "Account Name",
+   "read_only": 1
+  },
+  {
+   "fieldname": "oauth_details",
+   "fieldtype": "Section Break",
+   "label": "OAuth Credentials"
+  },
+  {
+   "fieldname": "consumer_key",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "API Key",
+   "reqd": 1
+  },
+  {
+   "fieldname": "consumer_secret",
+   "fieldtype": "Password",
+   "in_list_view": 1,
+   "label": "API Secret Key",
+   "reqd": 1
+  },
+  {
+   "fieldname": "oauth_token",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "OAuth Token",
+   "read_only": 1
+  },
+  {
+   "fieldname": "oauth_secret",
+   "fieldtype": "Password",
+   "hidden": 1,
+   "label": "OAuth Token Secret",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_5",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "profile_pic",
+   "fieldtype": "Attach Image",
+   "hidden": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "session_status",
+   "fieldtype": "Select",
+   "hidden": 1,
+   "label": "Session Status",
+   "options": "Expired\nActive",
+   "read_only": 1
+  }
+ ],
+ "image_field": "profile_pic",
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-04-21 22:06:43.726798",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Twitter Settings",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
new file mode 100644
index 0000000..64f53b5
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, os, tweepy, json
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils.file_manager import get_file_path
+from frappe.utils import get_url_to_form, get_link_to_form
+from tweepy.error import TweepError
+
+class TwitterSettings(Document):
+	def get_authorize_url(self):
+		callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url())
+		auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
+
+		try:
+			redirect_url = auth.get_authorization_url()
+			return redirect_url
+		except:
+			frappe.msgprint(_("Error! Failed to get request token."))
+			frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
+
+	
+	def get_access_token(self, oauth_token, oauth_verifier):
+		auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+		auth.request_token = { 
+			'oauth_token' : oauth_token,
+			'oauth_token_secret' : oauth_verifier 
+		}
+
+		try:
+			auth.get_access_token(oauth_verifier)
+			api = self.get_api()
+			user = api.me()
+			profile_pic = (user._json["profile_image_url"]).replace("_normal","")
+
+			frappe.db.set_value(self.doctype, self.name, {
+				"oauth_token" : auth.access_token,
+				"oauth_secret" : auth.access_token_secret,
+				"account_name" : user._json["screen_name"],
+				"profile_pic" : profile_pic,
+				"session_status" : "Active"
+			})
+
+			frappe.local.response["type"] = "redirect"
+			frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings")
+		except TweepError as e:
+			frappe.msgprint(_("Error! Failed to get access token."))
+			frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
+
+	def get_api(self):
+		# authentication of consumer key and secret 
+		auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) 
+		# authentication of access token and secret 
+		auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) 
+
+		return tweepy.API(auth)
+
+	def post(self, text, media=None):
+		if not media:
+			return self.send_tweet(text)
+
+		if media:
+			media_id = self.upload_image(media)
+			return self.send_tweet(text, media_id)
+	
+	def upload_image(self, media):
+		media = get_file_path(media)
+		api = self.get_api()
+		media = api.media_upload(media)
+
+		return media.media_id
+
+	def send_tweet(self, text, media_id=None):
+		api = self.get_api()
+		try:
+			if media_id:
+				response = api.update_status(status = text, media_ids = [media_id])
+			else:
+				response = api.update_status(status = text)
+
+			return response
+
+		except TweepError as e:
+			content = json.loads(e.response.content)
+			content = content["errors"][0]
+			if e.response.status_code == 401:
+				self.db_set("session_status", "Expired")
+				frappe.db.commit()
+			frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
+
+@frappe.whitelist()
+def callback(oauth_token, oauth_verifier):
+	twitter_settings = frappe.get_single("Twitter Settings")
+	twitter_settings.get_access_token(oauth_token,oauth_verifier)
+	frappe.db.commit()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 447cc06..e6f6c8e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -270,7 +270,8 @@
 scheduler_events = {
 	"all": [
 		"erpnext.projects.doctype.project.project.project_status_update_reminder",
-		"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder"
+		"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
+		"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
 	],
 	"hourly": [
 		'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py
index a9d8eca..3cef560 100644
--- a/erpnext/selling/doctype/campaign/campaign_dashboard.py
+++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py
@@ -8,6 +8,10 @@
 			{
 				'label': _('Email Campaigns'),
 				'items': ['Email Campaign']
+			},
+			{
+				'label': _('Social Media Campaigns'),
+				'items': ['Social Media Post']
 			}
-		],
+		]
 	}
diff --git a/requirements.txt b/requirements.txt
index c277545..9da537e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@
 python-stdnum==1.12
 Unidecode==1.1.1
 WooCommerce==2.1.1
+tweepy==3.8.0
\ No newline at end of file