diff --git a/desk/src/App.vue b/desk/src/App.vue index 37c20a52a..a56b0e0e1 100644 --- a/desk/src/App.vue +++ b/desk/src/App.vue @@ -2,16 +2,17 @@ + diff --git a/desk/src/pages/TicketNew.vue b/desk/src/pages/TicketNew.vue index a8c11d8f5..a675f3070 100644 --- a/desk/src/pages/TicketNew.vue +++ b/desk/src/pages/TicketNew.vue @@ -28,12 +28,17 @@
- +
+ + Subject + * + + +

{ const preferKnowledgeBase = computed( () => !!parseInt(config.value.prefer_knowledge_base) ); + const isFeedbackMandatory = computed( + () => !!parseInt(config.value.is_feedback_mandatory) + ); socket.on("helpdesk:settings-updated", () => configRes.reload()); @@ -29,5 +32,6 @@ export const useConfigStore = defineStore("config", () => { preferKnowledgeBase, isSetupComplete, skipEmailWorkflow, + isFeedbackMandatory, }; }); diff --git a/desk/src/types.ts b/desk/src/types.ts index a4d9feb13..97d40f0fc 100644 --- a/desk/src/types.ts +++ b/desk/src/types.ts @@ -192,6 +192,7 @@ export interface EmailAccount { api_key?: string; api_secret?: string; password?: string; + frappe_mail_site?: string; enable_outgoing?: boolean; enable_incoming?: boolean; default_outgoing?: boolean; diff --git a/helpdesk/api/config.py b/helpdesk/api/config.py index 1772ccf88..a431ea1df 100644 --- a/helpdesk/api/config.py +++ b/helpdesk/api/config.py @@ -8,6 +8,7 @@ def get_config(): "prefer_knowledge_base", "setup_complete", "skip_email_workflow", + "is_feedback_mandatory", ] res = frappe.get_value(doctype="HD Settings", fieldname=fields, as_dict=True) return res diff --git a/helpdesk/api/settings.py b/helpdesk/api/settings.py index 66d9034af..f7eaf7d01 100644 --- a/helpdesk/api/settings.py +++ b/helpdesk/api/settings.py @@ -29,13 +29,15 @@ def create_email_account(data): **service_config, } ) - email_doc.append( - "imap_folder", {"append_to": "HD Ticket", "folder_name": "INBOX"} - ) if service == "Frappe Mail": email_doc.api_key = data.get("api_key") email_doc.api_secret = data.get("api_secret") + email_doc.frappe_mail_site = data.get("frappe_mail_site") + email_doc.append_to = "HD Ticket" else: + email_doc.append( + "imap_folder", {"append_to": "HD Ticket", "folder_name": "INBOX"} + ) email_doc.password = data.get("password") # validate whether the credentials are correct email_doc.get_incoming_server() diff --git a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json index d5f6d0724..f3425c6bc 100644 --- a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json +++ b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.json @@ -9,8 +9,7 @@ "user", "agent_name", "user_image", - "is_active", - "groups" + "is_active" ], "fields": [ { @@ -35,12 +34,6 @@ "in_list_view": 1, "label": "Is Active" }, - { - "fieldname": "groups", - "fieldtype": "Table", - "label": "Groups", - "options": "HD Team Item" - }, { "fetch_from": "user.user_image", "fieldname": "user_image", @@ -51,7 +44,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-24 22:31:23.178626", + "modified": "2024-11-11 17:30:22.859253", "modified_by": "Administrator", "module": "Helpdesk", "name": "HD Agent", @@ -95,6 +88,5 @@ "sort_field": "modified", "sort_order": "DESC", "states": [], - "title_field": "agent_name", - "track_changes": 1 + "title_field": "agent_name" } \ No newline at end of file diff --git a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py index 44a9936c0..4ecc6ccd2 100644 --- a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py +++ b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py @@ -21,156 +21,6 @@ def set_user_roles(self): user.save() - def on_update(self): - if self.has_value_changed("is_active"): - if not self.is_active: - self.remove_from_support_rotations() - else: - self.add_to_support_rotations() - - if self.has_value_changed("groups") and self.is_active: - previous = self.get_doc_before_save() - if previous: - for group in previous.groups: - if not next( - (g for g in self.groups if g.team == group.team), - None, - ): - self.remove_from_support_rotations(group.team) - - self.add_to_support_rotations() - - def on_trash(self): - self.remove_from_support_rotations() - - def add_to_support_rotations(self, group=None): - """ - Add the hd_agent to the support rotation for the given group or all groups - the hd_agent belongs to if hd_agent already added to the support roatation - for a group, skip - - :param str group: Team name, defaults to None. - """ - rule_docs = [] - if not group: - # Add the hd_agent to the base support rotation - - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Settings").get_base_support_rotation(), - ) - ) - - # Add the hd_agent to the support rotation for each group they belong to - if self.groups: - for group in self.groups: - try: - team_assignment_rule = frappe.get_doc( - "HD Team", group.team - ).get_assignment_rule() - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - team_assignment_rule, - ) - ) - except frappe.DoesNotExistError: - frappe.throw( - frappe._( - "Assignment Rule for HD Team {0} does not exist" - ).format(group.team) - ) - else: - # check if the group is in self.groups - if next( - (group for group in self.groups if group["group_name"] == group), None - ): - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Team", group).get_assignment_rule(), - ) - ) - else: - frappe.throw( - frappe._( - "Agent {0} does not belong to team {1}".format( - self.agent_name, group - ) - ) - ) - - for rule_doc in rule_docs: - skip = False - if rule_doc: - if rule_doc.users and len(rule_doc.users) > 0: - for user in rule_doc.users: - if ( - user.user == self.user - ): # if the user is already in the rule, skip - skip = True - break - if skip: - continue - - user_doc = frappe.get_doc( - {"doctype": "Assignment Rule User", "user": self.user} - ) - rule_doc.append("users", user_doc) - rule_doc.disabled = False # enable the rule if it is disabled - rule_doc.save(ignore_permissions=True) - - def remove_from_support_rotations(self, group=None): - rule_docs = [] - - if group: - # remove the hd_agent from the support rotation for the given group - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Team", group).get_assignment_rule(), - ) - ) - - else: - # Remove the hd_agent from the base support rotation - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Settings").get_base_support_rotation(), - ) - ) - - # Remove the hd_agent from the support rotation for each group they belong to - for group in self.groups: - rule_docs.append( - frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Team", group.team).get_assignment_rule(), - ) - ) - - for rule_doc in rule_docs: - if rule_doc.users and len(rule_doc.users) > 0: - for user in rule_doc.users: - if user.user == self.user: - if len(rule_doc.users) == 1: - rule_doc.disabled = ( - True # disable the rule if there are no users left - ) - rule_doc.remove(user) - rule_doc.save() - - def in_group(self, group): - """ - Check if agent is in the given group - """ - if self.groups: - return next((g for g in self.groups if g.team == group), False) - - return False - @frappe.whitelist() def create_hd_agent(first_name, last_name, email, signature, team): diff --git a/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json b/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json index a01dbb9c4..5bf9407ef 100644 --- a/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json +++ b/helpdesk/helpdesk/doctype/hd_settings/hd_settings.json @@ -28,6 +28,10 @@ "column_break_zxek", "ticket_restrictions_section", "allow_anyone_to_create_tickets", + "feedback_section", + "is_feedback_mandatory", + "section_break_duow", + "auto_update_status", "workflow_tab", "skip_email_workflow", "instantly_send_email", @@ -294,11 +298,34 @@ "fieldname": "allow_anyone_to_create_tickets", "fieldtype": "Check", "label": "Allow anyone to create tickets" + }, + { + "fieldname": "section_break_duow", + "fieldtype": "Section Break" + }, + { + "default": "0", + "description": "When enabled, the ticket status will automatically change to \"Replied\" whenever the agent responds to a ticket.\n", + "fieldname": "auto_update_status", + "fieldtype": "Check", + "label": "Auto Update Status" + }, + { + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "default": "0", + "description": "If enabled, the feedback dialog will be shown, when a user tries to close a ticket. \nNote: User can't close a ticket without giving a feedback.", + "fieldname": "is_feedback_mandatory", + "fieldtype": "Check", + "label": "Make Feedback Mandatory" } ], "issingle": 1, "links": [], - "modified": "2024-10-22 02:14:33.360809", + "modified": "2024-11-22 17:25:40.112881", "modified_by": "Administrator", "module": "Helpdesk", "name": "HD Settings", diff --git a/helpdesk/helpdesk/doctype/hd_team/hd_team.py b/helpdesk/helpdesk/doctype/hd_team/hd_team.py index 900c80cb1..75fb85e7c 100644 --- a/helpdesk/helpdesk/doctype/hd_team/hd_team.py +++ b/helpdesk/helpdesk/doctype/hd_team/hd_team.py @@ -2,6 +2,8 @@ # For license information, please see license.txt import frappe +from frappe import _ +from frappe.core.doctype.version.version import get_diff from frappe.exceptions import DoesNotExistError from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists @@ -12,8 +14,20 @@ class HDTeam(Document): def rename_self(self, new_name: str): self.rename(new_name) + # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting-other-method def after_insert(self): self.create_assignment_rule() + assignment_rule_doc = frappe.get_doc("Assignment Rule", self.assignment_rule) + + for user in self.users: + _user = user.get("user") + if not _user: + continue + assignment_rule_doc.append("users", {"user": _user}) + + if assignment_rule_doc.disabled and assignment_rule_doc.users: + assignment_rule_doc.disabled = False + assignment_rule_doc.save() def after_rename(self, olddn, newdn, merge=False): # Update the condition for the linked assignment rule @@ -22,18 +36,28 @@ def after_rename(self, olddn, newdn, merge=False): rule_doc.assign_condition = f"status == 'Open' and agent_group == '{newdn}'" rule_doc.save(ignore_permissions=True) - def on_trash(self): + def on_update(self): + self.update_support_rotations() + def on_trash(self): # Deletes the assignment rule for this group + rule = self.assignment_rule + if not rule: + return try: - rule = self.get_assignment_rule() - if rule: - self.assignment_rule = "" - self.save() - frappe.get_doc("Assignment Rule", rule).delete() + frappe.delete_doc( + "Assignment Rule", + rule, + ignore_permissions=True, + force=True, + ignore_on_trash=True, + ) + frappe.db.commit() except DoesNotExistError: - # TODO: Log this error - pass + frappe.log_error( + title="Assignment Rule not found", + message=f"Assignment Rule {rule} not found", + ) def create_assignment_rule(self): """Creates the assignment rule for this group""" @@ -72,3 +96,69 @@ def get_assignment_rule(self): self.create_assignment_rule() return self.assignment_rule + + def update_support_rotations(self): + """ + Update the support rotations for the hd_agent + # If agent removed, remove from the support rule of the team + # If agent added add to the support rule of the team and also, while adding remove from base Support Rotation + """ + assg_rule_doc = frappe.get_doc("Assignment Rule", self.assignment_rule) + if not assg_rule_doc: + return + + previous_doc = self.get_doc_before_save() + diff = get_diff(previous_doc, self) + if not diff: + return + + if not diff.get("removed") and not diff.get("added"): + return + + for user in diff.get("removed"): + self.update_assignment_rule_users(user, assg_rule_doc, action="remove") + + for user in diff.get("added"): + self.update_assignment_rule_users(user, assg_rule_doc) + + def update_assignment_rule_users(self, user, assignment_rule_doc, action="add"): + _user = user[1].get("user") + if not user: + frappe.throw(_("User Not found")) + return + + if action == "add": + assignment_rule_doc.append("users", {"user": _user}) + if assignment_rule_doc.disabled: + assignment_rule_doc.disabled = False + assignment_rule_doc.save() + + # remove the user from the base assignment rule + base_assignment_rule = frappe.get_value( + "HD Settings", "HD Settings", "base_support_rotation" + ) + base_assignment_rule = frappe.get_doc( + "Assignment Rule", base_assignment_rule + ) + user_id = frappe.get_value( + "Assignment Rule User", + {"user": _user, "parent": base_assignment_rule.name}, + ) + if user_id: + frappe.delete_doc("Assignment Rule User", user_id) + else: + user_id = frappe.get_value( + "Assignment Rule User", + {"user": _user, "parent": assignment_rule_doc.name}, + ) + if not user_id: + return + frappe.delete_doc("Assignment Rule User", user_id) + + # disable the assignment rule if there are no users + total_users_in_assignment_rule = frappe.db.count( + "Assignment Rule User", {"parent": assignment_rule_doc.name} + ) + if total_users_in_assignment_rule == 0: + assignment_rule_doc.disabled = True + assignment_rule_doc.save() diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py index da3acb747..daf302688 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py +++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py @@ -191,9 +191,6 @@ def after_insert(self): log_ticket_activity(self.name, "created this ticket") capture_event("ticket_created") publish_event("helpdesk:new-ticket", {"name": self.name}) - # create communication if we are not hitting the new ticket creation API - if not self.via_customer_portal: - self.create_communication_via_contact(self.description) def on_update(self): # flake8: noqa @@ -346,14 +343,16 @@ def remove_assignment_if_not_in_team(self): if not self.agent_group or (hasattr(self, "_assign") and not self._assign): return if self.has_value_changed("agent_group") and self.status == "Open": - current_assigned_agent_doc = self.get_assigned_agent() + current_assigned_agent = self.get_assigned_agent() + if not current_assigned_agent: + return + is_agent_in_assigned_team = self.agent_in_assigned_team( + current_assigned_agent, self.agent_group + ) + if ( - current_assigned_agent_doc - and not current_assigned_agent_doc.in_group(self.agent_group) - ) and frappe.get_doc( - "Assignment Rule", - frappe.get_doc("HD Team", self.agent_group).assignment_rule, - ).users: + not is_agent_in_assigned_team + ) and self.users_present_in_team_assignment_rule(): clear_all_assignments("HD Ticket", self.name) frappe.publish_realtime( "helpdesk:update-ticket-assignee", @@ -361,6 +360,39 @@ def remove_assignment_if_not_in_team(self): after_commit=True, ) + def agent_in_assigned_team(self, agent, team): + return frappe.db.exists( + "HD Team Member", + { + "parent": team, + "user": agent, + }, + ) + + def users_present_in_team_assignment_rule(self): + if not self.agent_group: + return False + + assignment_rule = frappe.db.get_value( + "HD Team", self.agent_group, "assignment_rule" + ) + if not assignment_rule: + return False + + is_disabled = frappe.db.get_value( + "Assignment Rule", assignment_rule, "disabled" + ) + if is_disabled: + return False + + users = frappe.get_all( + "Assignment Rule User", filters={"parent": assignment_rule} + ) + if not users: + return False + + return True + @frappe.whitelist() def assign_agent(self, agent): assign({"assign_to": [agent], "doctype": "HD Ticket", "name": self.name}) @@ -386,8 +418,7 @@ def get_assigned_agent(self): # TODO: temporary fix, remove this when only agents can be assigned to ticket exists = frappe.db.exists("HD Agent", assignees[0]) if exists: - agent_doc = frappe.get_doc("HD Agent", assignees[0]) - return agent_doc + return assignees[0] assignees = get_assignees({"doctype": "HD Ticket", "name": self.name}) if len(assignees) > 0: @@ -707,6 +738,10 @@ def on_communication_update(self, c): self.first_responded_on = ( self.first_responded_on or frappe.utils.now_datetime() ) + + if frappe.db.get_single_value("HD Settings", "auto_update_status"): + self.status = "Replied" + # Fetch description from communication if not set already. This might not be needed # anymore as a communication is created when a ticket is created. self.description = self.description or c.content diff --git a/helpdesk/helpdesk/doctype/hd_ticket_template/api.py b/helpdesk/helpdesk/doctype/hd_ticket_template/api.py index b46ac5e89..71da35d60 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket_template/api.py +++ b/helpdesk/helpdesk/doctype/hd_ticket_template/api.py @@ -60,5 +60,6 @@ def get_fields(template: str, fetch: Literal["Custom Field", "DocField"]): .join(QBFetch, JoinType.inner) .on(QBFetch.fieldname == fields.fieldname) .where(where_parent) + .orderby(fields.idx) .run(as_dict=True) ) diff --git a/helpdesk/patches.txt b/helpdesk/patches.txt index 28ff52d9a..c04b156ed 100644 --- a/helpdesk/patches.txt +++ b/helpdesk/patches.txt @@ -17,3 +17,4 @@ helpdesk.helpdesk.doctype.hd_ticket.patches.replace_overdue_failed helpdesk.patches.create_helpdesk_folder helpdesk.helpdesk.doctype.hd_ticket.patches.feedback_in_master helpdesk.helpdesk.doctype.hd_ticket.patches.first_responded_on +helpdesk.patches.update_hd_team_users diff --git a/helpdesk/patches/update_hd_team_users.py b/helpdesk/patches/update_hd_team_users.py new file mode 100644 index 000000000..e162cb065 --- /dev/null +++ b/helpdesk/patches/update_hd_team_users.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + teams = frappe.get_all("HD Team", pluck="name") + for team in teams: + existing_agents = frappe.get_all( + "HD Team Item", filters={"team": team}, pluck="parent" + ) # agents in HD Agent doctype + team_users = frappe.get_all( + "HD Team Member", filters={"parent": team}, pluck="user" + ) # agents in HD Team doctype + + for agent in existing_agents: + is_agent_active = frappe.get_value("HD Agent", agent, "is_active") + if is_agent_active and agent not in team_users: + team_doc = ( + frappe.get_doc("HD Team", team) + .append("users", {"user": agent}) + .save() + ) + print("Agent Added")