Merge pull request #16673 from Alchez/feat-task-from-issue

feat(issue): Create and view Tasks from Issues
diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js
index 93423db..9a8af69 100644
--- a/erpnext/projects/doctype/task/task.js
+++ b/erpnext/projects/doctype/task/task.js
@@ -35,19 +35,6 @@
 			}
 
 			if(!doc.__islocal) {
-				if(frappe.model.can_read("Timesheet")) {
-					frm.add_custom_button(__("Timesheet"), function() {
-						frappe.route_options = {"project": doc.project, "task": doc.name}
-						frappe.set_route("List", "Timesheet");
-					}, __("View"), true);
-				}
-				if(frappe.model.can_read("Expense Claim")) {
-					frm.add_custom_button(__("Expense Claims"), function() {
-						frappe.route_options = {"project": doc.project, "task": doc.name}
-						frappe.set_route("List", "Expense Claim");
-					}, __("View"), true);
-				}
-
 				if(frm.perm[0].write) {
 					if(frm.doc.status!=="Completed" && frm.doc.status!=="Cancelled") {
 						frm.add_custom_button(__("Completed"), function() {
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index d904d70..2602aef 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -83,6 +83,39 @@
    "allow_bulk_edit": 0,
    "allow_in_quick_entry": 0,
    "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "issue",
+   "fieldtype": "Link",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Issue",
+   "length": 0,
+   "no_copy": 0,
+   "options": "Issue",
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
    "bold": 1,
    "collapsible": 0,
    "columns": 0,
@@ -217,6 +250,38 @@
    "allow_bulk_edit": 0,
    "allow_in_quick_entry": 0,
    "allow_on_submit": 0,
+   "bold": 0,
+   "collapsible": 0,
+   "columns": 0,
+   "fieldname": "color",
+   "fieldtype": "Color",
+   "hidden": 0,
+   "ignore_user_permissions": 0,
+   "ignore_xss_filter": 0,
+   "in_filter": 0,
+   "in_global_search": 0,
+   "in_list_view": 0,
+   "in_standard_filter": 0,
+   "label": "Color",
+   "length": 0,
+   "no_copy": 0,
+   "permlevel": 0,
+   "precision": "",
+   "print_hide": 0,
+   "print_hide_if_no_value": 0,
+   "read_only": 0,
+   "remember_last_selected_value": 0,
+   "report_hide": 0,
+   "reqd": 0,
+   "search_index": 0,
+   "set_only_once": 0,
+   "translatable": 0,
+   "unique": 0
+  },
+  {
+   "allow_bulk_edit": 0,
+   "allow_in_quick_entry": 0,
+   "allow_on_submit": 0,
    "bold": 1,
    "collapsible": 0,
    "columns": 0,
@@ -251,11 +316,11 @@
    "allow_in_quick_entry": 0,
    "allow_on_submit": 0,
    "bold": 0,
-   "collapsible": 0,
-   "collapsible_depends_on": "",
+   "collapsible": 1,
+   "collapsible_depends_on": "eval:doc.__islocal",
    "columns": 0,
    "depends_on": "",
-   "fieldname": "section_break_10",
+   "fieldname": "sb_timeline",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -264,6 +329,7 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
+   "label": "Timeline",
    "length": 0,
    "no_copy": 0,
    "permlevel": 0,
@@ -519,42 +585,10 @@
    "allow_on_submit": 0,
    "bold": 0,
    "collapsible": 0,
-   "columns": 0,
-   "fieldname": "color",
-   "fieldtype": "Color",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
-   "in_list_view": 0,
-   "in_standard_filter": 0,
-   "label": "Color",
-   "length": 0,
-   "no_copy": 0,
-   "permlevel": 0,
-   "precision": "",
-   "print_hide": 0,
-   "print_hide_if_no_value": 0,
-   "read_only": 0,
-   "remember_last_selected_value": 0,
-   "report_hide": 0,
-   "reqd": 0,
-   "search_index": 0,
-   "set_only_once": 0,
-   "translatable": 0,
-   "unique": 0
-  },
-  {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
    "collapsible_depends_on": "",
    "columns": 0,
    "depends_on": "",
-   "fieldname": "section_break0",
+   "fieldname": "sb_details",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -563,10 +597,11 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
+   "label": "Details",
    "length": 0,
    "no_copy": 0,
    "oldfieldtype": "Section Break",
-   "options": "Simple",
+   "options": "",
    "permlevel": 0,
    "print_hide": 0,
    "print_hide_if_no_value": 0,
@@ -596,7 +631,7 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
-   "label": "Details",
+   "label": "Task Description",
    "length": 0,
    "no_copy": 0,
    "oldfieldname": "description",
@@ -624,7 +659,7 @@
    "collapsible_depends_on": "",
    "columns": 0,
    "depends_on": "",
-   "fieldname": "section_break",
+   "fieldname": "sb_depends_on",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -726,7 +761,7 @@
    "columns": 0,
    "depends_on": "",
    "description": "",
-   "fieldname": "actual",
+   "fieldname": "sb_actual",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -893,10 +928,10 @@
    "allow_in_quick_entry": 0,
    "allow_on_submit": 0,
    "bold": 0,
-   "collapsible": 0,
+   "collapsible": 1,
    "columns": 0,
    "depends_on": "",
-   "fieldname": "section_break_17",
+   "fieldname": "sb_costing",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -905,6 +940,7 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
+   "label": "Costing",
    "length": 0,
    "no_copy": 0,
    "permlevel": 0,
@@ -1058,9 +1094,9 @@
    "allow_in_quick_entry": 0,
    "allow_on_submit": 0,
    "bold": 0,
-   "collapsible": 0,
+   "collapsible": 1,
    "columns": 0,
-   "fieldname": "more_details",
+   "fieldname": "sb_more_info",
    "fieldtype": "Section Break",
    "hidden": 0,
    "ignore_user_permissions": 0,
@@ -1069,7 +1105,7 @@
    "in_global_search": 0,
    "in_list_view": 0,
    "in_standard_filter": 0,
-   "label": "",
+   "label": "More Info",
    "length": 0,
    "no_copy": 0,
    "permlevel": 0,
diff --git a/erpnext/projects/doctype/task/task_dashboard.py b/erpnext/projects/doctype/task/task_dashboard.py
new file mode 100644
index 0000000..b776b98
--- /dev/null
+++ b/erpnext/projects/doctype/task/task_dashboard.py
@@ -0,0 +1,19 @@
+from __future__ import unicode_literals
+
+from frappe import _
+
+
+def get_data():
+	return {
+		'fieldname': 'task',
+		'transactions': [
+			{
+				'label': _('Activity'),
+				'items': ['Timesheet']
+			},
+			{
+				'label': _('Accounting'),
+				'items': ['Expense Claim']
+			}
+		]
+	}
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index d0a9bf3..ce75304 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -3,14 +3,21 @@
 		frm.email_field = "raised_by";
 	},
 
-	refresh: function(frm) {
-		if(frm.doc.status!=="Closed") {
-			frm.add_custom_button(__("Close"), function() {
+	refresh: function (frm) {
+		if (frm.doc.status !== "Closed") {
+			frm.add_custom_button(__("Close"), function () {
 				frm.set_value("status", "Closed");
 				frm.save();
 			});
+
+			frm.add_custom_button(__("Task"), function () {
+				frappe.model.open_mapped_doc({
+					method: "erpnext.support.doctype.issue.issue.make_task",
+					frm: frm
+				});
+			}, __("Make"));
 		} else {
-			frm.add_custom_button(__("Reopen"), function() {
+			frm.add_custom_button(__("Reopen"), function () {
 				frm.set_value("status", "Open");
 				frm.save();
 			});
@@ -37,7 +44,7 @@
 		if (!frm.timeline.wrapper.find('.btn-split-issue').length) {
 			let split_issue = __("Split Issue")
 			$(`<button class="btn btn-xs btn-link btn-add-to-kb text-muted hidden-xs btn-split-issue pull-right" style="display:inline-block; margin-right: 15px">
-				${split_issue} 
+				${split_issue}
 			</button>`)
 				.appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])'))
 			if (!frm.timeline.wrapper.data("split-issue-event-attached")){
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index 21cf2f7..7cb0df2 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -1,5 +1,6 @@
 {
  "allow_copy": 0, 
+ "allow_events_in_timeline": 0, 
  "allow_guest_to_view": 0, 
  "allow_import": 1, 
  "allow_rename": 1, 
@@ -349,8 +350,9 @@
    "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 1, 
+   "collapsible_depends_on": "eval:doc.status!=\"Closed\"", 
    "columns": 0, 
-   "fieldname": "section_break_7", 
+   "fieldname": "sb_details", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -416,7 +418,7 @@
    "bold": 0, 
    "collapsible": 1, 
    "columns": 0, 
-   "fieldname": "response", 
+   "fieldname": "sb_response", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -511,7 +513,7 @@
    "bold": 0, 
    "collapsible": 1, 
    "columns": 0, 
-   "fieldname": "additional_info", 
+   "fieldname": "sb_additional_info", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -736,7 +738,7 @@
    "bold": 0, 
    "collapsible": 1, 
    "columns": 0, 
-   "fieldname": "section_break_19", 
+   "fieldname": "sb_resoution", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -1035,7 +1037,7 @@
  "issingle": 0, 
  "istable": 0, 
  "max_attachments": 0, 
- "modified": "2018-08-21 14:44:27.615004", 
+ "modified": "2019-02-14 02:55:47.562611", 
  "modified_by": "Administrator", 
  "module": "Support", 
  "name": "Issue", 
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 0b5eb53..de3d144 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -7,20 +7,24 @@
 from frappe import _
 
 from frappe.model.document import Document
+from frappe.model.mapper import get_mapped_doc
 from frappe.utils import now
 from frappe.utils.user import is_website_user
 
 sender_field = "raised_by"
 
+
 class Issue(Document):
 	def get_feed(self):
 		return "{0}: {1}".format(_(self.status), self.subject)
 
 	def validate(self):
-		if (self.get("__islocal") and self.via_customer_portal):
+		if self.is_new() and self.via_customer_portal:
 			self.flags.create_communication = True
+
 		if not self.raised_by:
 			self.raised_by = frappe.session.user
+
 		self.update_status()
 		self.set_lead_contact(self.raised_by)
 
@@ -29,17 +33,19 @@
 			clear(self.doctype, self.name)
 
 	def on_update(self):
-		# create the communication email and remove the description
-		if (self.flags.create_communication and self.via_customer_portal):
+		# Add a communication in the issue timeline
+		if self.flags.create_communication and self.via_customer_portal:
 			self.create_communication()
 			self.flags.communication_created = None
 
 	def set_lead_contact(self, email_id):
 		import email.utils
+
 		email_id = email.utils.parseaddr(email_id)[1]
 		if email_id:
 			if not self.lead:
 				self.lead = frappe.db.get_value("Lead", {"email_id": email_id})
+
 			if not self.contact and not self.customer:
 				self.contact = frappe.db.get_value("Contact", {"email_id": email_id})
 
@@ -79,24 +85,30 @@
 		communication.ignore_mandatory = True
 		communication.save()
 
-		self.db_set("description", "")
-
 	def split_issue(self, subject, communication_id):
 		# Bug: Pressing enter doesn't send subject
 		from copy import deepcopy
+
 		replicated_issue = deepcopy(self)
 		replicated_issue.subject = subject
 		frappe.get_doc(replicated_issue).insert()
+
 		# Replicate linked Communications
-		# todo get all communications in timeline before this, and modify them to append them to new doc
+		# TODO: get all communications in timeline before this, and modify them to append them to new doc
 		comm_to_split_from = frappe.get_doc("Communication", communication_id)
-		communications = frappe.get_all("Communication", filters={"reference_name": comm_to_split_from.reference_name, "reference_doctype": "Issue", "creation": ('>=', comm_to_split_from.creation)})
+		communications = frappe.get_all("Communication",
+			filters={"reference_doctype": "Issue",
+				"reference_name": comm_to_split_from.reference_name,
+				"creation": ('>=', comm_to_split_from.creation)})
+
 		for communication in communications:
 			doc = frappe.get_doc("Communication", communication.name)
 			doc.reference_name = replicated_issue.name
 			doc.save(ignore_permissions=True)
+
 		return replicated_issue.name
 
+
 def get_list_context(context=None):
 	return {
 		"title": _("Issues"),
@@ -107,11 +119,14 @@
 		'no_breadcrumbs': True
 	}
 
+
 def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
 	from frappe.www.list import get_list
+
 	user = frappe.session.user
 	contact = frappe.db.get_value('Contact', {'user': user}, 'name')
 	customer = None
+
 	if contact:
 		contact_doc = frappe.get_doc('Contact', contact)
 		customer = contact_doc.get_link_for('Customer')
@@ -124,14 +139,23 @@
 
 	return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)
 
+
+@frappe.whitelist()
+def set_multiple_status(names, status):
+	names = json.loads(names)
+	for name in names:
+		set_status(name, status)
+
+
 @frappe.whitelist()
 def set_status(name, status):
 	st = frappe.get_doc("Issue", name)
 	st.status = status
 	st.save()
 
+
 def auto_close_tickets():
-	""" auto close the replied support tickets after 7 days """
+	"""Auto-close replied support tickets after 7 days"""
 	auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
 
 	issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and
@@ -144,11 +168,6 @@
 		doc.flags.ignore_mandatory = True
 		doc.save()
 
-@frappe.whitelist()
-def set_multiple_status(names, status):
-	names = json.loads(names)
-	for name in names:
-		set_status(name, status)
 
 def has_website_permission(doc, ptype, user, verbose=False):
 	from erpnext.controllers.website_list_for_contact import has_website_permission
@@ -160,3 +179,12 @@
 def update_issue(contact, method):
 	"""Called when Contact is deleted"""
 	frappe.db.sql("""UPDATE `tabIssue` set contact='' where contact=%s""", contact.name)
+
+
+@frappe.whitelist()
+def make_task(source_name, target_doc=None):
+	return get_mapped_doc("Issue", source_name, {
+		"Issue": {
+			"doctype": "Task"
+		}
+	}, target_doc)
diff --git a/erpnext/support/doctype/issue/issue_dashboard.py b/erpnext/support/doctype/issue/issue_dashboard.py
new file mode 100644
index 0000000..2ac7c81
--- /dev/null
+++ b/erpnext/support/doctype/issue/issue_dashboard.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from frappe import _
+
+
+def get_data():
+	return {
+		'fieldname': 'issue',
+		'transactions': [
+			{
+				'label': _('Activity'),
+				'items': ['Task']
+			}
+		]
+	}