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

feat(issue): Create and view Tasks from Issues
This commit is contained in:
Rushabh Mehta 2019-03-15 08:55:20 +05:30 committed by GitHub
commit e5d79f3621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 175 additions and 81 deletions

View File

@ -35,19 +35,6 @@ frappe.ui.form.on("Task", {
} }
if(!doc.__islocal) { 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.perm[0].write) {
if(frm.doc.status!=="Completed" && frm.doc.status!=="Cancelled") { if(frm.doc.status!=="Completed" && frm.doc.status!=="Cancelled") {
frm.add_custom_button(__("Completed"), function() { frm.add_custom_button(__("Completed"), function() {

View File

@ -79,6 +79,39 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -213,6 +246,38 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -251,11 +316,11 @@
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 1,
"collapsible_depends_on": "", "collapsible_depends_on": "eval:doc.__islocal",
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"fieldname": "section_break_10", "fieldname": "sb_timeline",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -264,6 +329,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Timeline",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@ -513,38 +579,6 @@
"translatable": 0, "translatable": 0,
"unique": 0 "unique": 0
}, },
{
"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_bulk_edit": 0,
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
@ -554,7 +588,7 @@
"collapsible_depends_on": "", "collapsible_depends_on": "",
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"fieldname": "section_break0", "fieldname": "sb_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -563,10 +597,11 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Details",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"oldfieldtype": "Section Break", "oldfieldtype": "Section Break",
"options": "Simple", "options": "",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
@ -596,7 +631,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Details", "label": "Task Description",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"oldfieldname": "description", "oldfieldname": "description",
@ -624,7 +659,7 @@
"collapsible_depends_on": "", "collapsible_depends_on": "",
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"fieldname": "section_break", "fieldname": "sb_depends_on",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -726,7 +761,7 @@
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"description": "", "description": "",
"fieldname": "actual", "fieldname": "sb_actual",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -893,10 +928,10 @@
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 1,
"columns": 0, "columns": 0,
"depends_on": "", "depends_on": "",
"fieldname": "section_break_17", "fieldname": "sb_costing",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -905,6 +940,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Costing",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@ -1058,9 +1094,9 @@
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 1,
"columns": 0, "columns": 0,
"fieldname": "more_details", "fieldname": "sb_more_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -1069,7 +1105,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "", "label": "More Info",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,

View File

@ -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']
}
]
}

View File

@ -3,14 +3,21 @@ frappe.ui.form.on("Issue", {
frm.email_field = "raised_by"; frm.email_field = "raised_by";
}, },
refresh: function(frm) { refresh: function (frm) {
if(frm.doc.status!=="Closed") { if (frm.doc.status !== "Closed") {
frm.add_custom_button(__("Close"), function() { frm.add_custom_button(__("Close"), function () {
frm.set_value("status", "Closed"); frm.set_value("status", "Closed");
frm.save(); 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 { } else {
frm.add_custom_button(__("Reopen"), function() { frm.add_custom_button(__("Reopen"), function () {
frm.set_value("status", "Open"); frm.set_value("status", "Open");
frm.save(); frm.save();
}); });

View File

@ -1,5 +1,6 @@
{ {
"allow_copy": 0, "allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0, "allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
@ -349,8 +350,9 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "eval:doc.status!=\"Closed\"",
"columns": 0, "columns": 0,
"fieldname": "section_break_7", "fieldname": "sb_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -416,7 +418,7 @@
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fieldname": "response", "fieldname": "sb_response",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -511,7 +513,7 @@
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fieldname": "additional_info", "fieldname": "sb_additional_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -736,7 +738,7 @@
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fieldname": "section_break_19", "fieldname": "sb_resoution",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@ -1035,7 +1037,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-08-21 14:44:27.615004", "modified": "2019-02-14 02:55:47.562611",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Issue", "name": "Issue",

View File

@ -7,20 +7,24 @@ import json
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import now from frappe.utils import now
from frappe.utils.user import is_website_user from frappe.utils.user import is_website_user
sender_field = "raised_by" sender_field = "raised_by"
class Issue(Document): class Issue(Document):
def get_feed(self): def get_feed(self):
return "{0}: {1}".format(_(self.status), self.subject) return "{0}: {1}".format(_(self.status), self.subject)
def validate(self): 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 self.flags.create_communication = True
if not self.raised_by: if not self.raised_by:
self.raised_by = frappe.session.user self.raised_by = frappe.session.user
self.update_status() self.update_status()
self.set_lead_contact(self.raised_by) self.set_lead_contact(self.raised_by)
@ -29,17 +33,19 @@ class Issue(Document):
clear(self.doctype, self.name) clear(self.doctype, self.name)
def on_update(self): def on_update(self):
# create the communication email and remove the description # Add a communication in the issue timeline
if (self.flags.create_communication and self.via_customer_portal): if self.flags.create_communication and self.via_customer_portal:
self.create_communication() self.create_communication()
self.flags.communication_created = None self.flags.communication_created = None
def set_lead_contact(self, email_id): def set_lead_contact(self, email_id):
import email.utils import email.utils
email_id = email.utils.parseaddr(email_id)[1] email_id = email.utils.parseaddr(email_id)[1]
if email_id: if email_id:
if not self.lead: if not self.lead:
self.lead = frappe.db.get_value("Lead", {"email_id": email_id}) self.lead = frappe.db.get_value("Lead", {"email_id": email_id})
if not self.contact and not self.customer: if not self.contact and not self.customer:
self.contact = frappe.db.get_value("Contact", {"email_id": email_id}) self.contact = frappe.db.get_value("Contact", {"email_id": email_id})
@ -79,24 +85,30 @@ class Issue(Document):
communication.ignore_mandatory = True communication.ignore_mandatory = True
communication.save() communication.save()
self.db_set("description", "")
def split_issue(self, subject, communication_id): def split_issue(self, subject, communication_id):
# Bug: Pressing enter doesn't send subject # Bug: Pressing enter doesn't send subject
from copy import deepcopy from copy import deepcopy
replicated_issue = deepcopy(self) replicated_issue = deepcopy(self)
replicated_issue.subject = subject replicated_issue.subject = subject
frappe.get_doc(replicated_issue).insert() frappe.get_doc(replicated_issue).insert()
# Replicate linked Communications # 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) 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: for communication in communications:
doc = frappe.get_doc("Communication", communication.name) doc = frappe.get_doc("Communication", communication.name)
doc.reference_name = replicated_issue.name doc.reference_name = replicated_issue.name
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
return replicated_issue.name return replicated_issue.name
def get_list_context(context=None): def get_list_context(context=None):
return { return {
"title": _("Issues"), "title": _("Issues"),
@ -107,11 +119,14 @@ def get_list_context(context=None):
'no_breadcrumbs': True 'no_breadcrumbs': True
} }
def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None): def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
from frappe.www.list import get_list from frappe.www.list import get_list
user = frappe.session.user user = frappe.session.user
contact = frappe.db.get_value('Contact', {'user': user}, 'name') contact = frappe.db.get_value('Contact', {'user': user}, 'name')
customer = None customer = None
if contact: if contact:
contact_doc = frappe.get_doc('Contact', contact) contact_doc = frappe.get_doc('Contact', contact)
customer = contact_doc.get_link_for('Customer') customer = contact_doc.get_link_for('Customer')
@ -124,14 +139,23 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) 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() @frappe.whitelist()
def set_status(name, status): def set_status(name, status):
st = frappe.get_doc("Issue", name) st = frappe.get_doc("Issue", name)
st.status = status st.status = status
st.save() st.save()
def auto_close_tickets(): 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 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 issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and
@ -144,11 +168,6 @@ def auto_close_tickets():
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save() 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): def has_website_permission(doc, ptype, user, verbose=False):
from erpnext.controllers.website_list_for_contact import has_website_permission from erpnext.controllers.website_list_for_contact import has_website_permission
@ -160,3 +179,12 @@ def has_website_permission(doc, ptype, user, verbose=False):
def update_issue(contact, method): def update_issue(contact, method):
"""Called when Contact is deleted""" """Called when Contact is deleted"""
frappe.db.sql("""UPDATE `tabIssue` set contact='' where contact=%s""", contact.name) 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)

View File

@ -0,0 +1,15 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'issue',
'transactions': [
{
'label': _('Activity'),
'items': ['Task']
}
]
}