Add odex25_helpdesk
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Helpdesk',
|
||||
'version': '1.3',
|
||||
'sequence': 110,
|
||||
'summary': 'Track, prioritize, and solve customer tickets',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': [
|
||||
'base_setup',
|
||||
'mail',
|
||||
'utm',
|
||||
'rating',
|
||||
'web_tour',
|
||||
'resource',
|
||||
'portal',
|
||||
'digest',
|
||||
],
|
||||
'description': """
|
||||
Odex25 helpdesk - Ticket Management App
|
||||
================================
|
||||
|
||||
Features:
|
||||
|
||||
- Process tickets through different stages to solve them.
|
||||
- Add priorities, types, descriptions and tags to define your tickets.
|
||||
- Use the chatter to communicate additional information and ping co-workers on tickets.
|
||||
- Enjoy the use of an adapted dashboard, and an easy-to-use kanban view to handle your tickets.
|
||||
- Make an in-depth analysis of your tickets through the pivot view in the reports menu.
|
||||
- Create a team and define its members, use an automatic assignment method if you wish.
|
||||
- Use a mail alias to automatically create tickets and communicate with your customers.
|
||||
- Add Service Level Agreement deadlines automatically to your tickets.
|
||||
- Get customer feedback by using ratings.
|
||||
- Install additional features easily using your team form view.
|
||||
|
||||
""",
|
||||
'data': [
|
||||
'security/odex25_helpdesk_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/digest_data.xml',
|
||||
'data/mail_data.xml',
|
||||
# 'data/odex25_helpdesk_data.xml',
|
||||
'views/odex25_helpdesk_views.xml',
|
||||
'views/odex25_helpdesk_views.xml',
|
||||
'views/zfp_config_setting.xml',
|
||||
'views/odex25_helpdesk_team_views.xml',
|
||||
'views/assets.xml',
|
||||
'views/digest_views.xml',
|
||||
'views/odex25_helpdesk_portal_templates.xml',
|
||||
# 'views/res_partner_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'report/odex25_helpdesk_sla_report_analysis_views.xml',
|
||||
],
|
||||
'qweb': [
|
||||
"static/src/xml/odex25_helpdesk_team_templates.xml",
|
||||
],
|
||||
# 'demo': ['data/odex25_helpdesk_demo.xml'],
|
||||
'application': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import portal
|
||||
from . import rating
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessError, MissingError, UserError
|
||||
from odoo.http import request
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools import groupby as groupbyelem
|
||||
from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal
|
||||
from odoo.osv.expression import OR
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(CustomerPortal, self)._prepare_portal_layout_values()
|
||||
if values.get('sales_user', False):
|
||||
values['title'] = _("Salesperson")
|
||||
return values
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super()._prepare_home_portal_values(counters)
|
||||
if 'ticket_count' in counters:
|
||||
values['ticket_count'] = request.env['odex25_helpdesk.ticket'].search_count([])
|
||||
return values
|
||||
|
||||
def _ticket_get_page_view_values(self, ticket, access_token, **kwargs):
|
||||
values = {
|
||||
'page_name': 'ticket',
|
||||
'ticket': ticket,
|
||||
}
|
||||
return self._get_page_view_values(ticket, access_token, values, 'my_tickets_history', False, **kwargs)
|
||||
|
||||
@http.route(['/my/tickets', '/my/tickets/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def my_odex25_helpdesk_tickets(self, page=1, date_begin=None, date_end=None, sortby=None, filterby='all', search=None, groupby='none', search_in='content', **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
|
||||
searchbar_sortings = {
|
||||
'date': {'label': _('Newest'), 'order': 'create_date desc'},
|
||||
'name': {'label': _('Subject'), 'order': 'name'},
|
||||
'stage': {'label': _('Stage'), 'order': 'stage_id'},
|
||||
'reference': {'label': _('Reference'), 'order': 'id'},
|
||||
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'},
|
||||
}
|
||||
searchbar_filters = {
|
||||
'all': {'label': _('All'), 'domain': []},
|
||||
'assigned': {'label': _('Assigned'), 'domain': [('user_id', '!=', False)]},
|
||||
'unassigned': {'label': _('Unassigned'), 'domain': [('user_id', '=', False)]},
|
||||
'open': {'label': _('Open'), 'domain': [('close_date', '=', False)]},
|
||||
'closed': {'label': _('Closed'), 'domain': [('close_date', '!=', False)]},
|
||||
'last_message_sup': {'label': _('Last message is from support')},
|
||||
'last_message_cust': {'label': _('Last message is from customer')},
|
||||
}
|
||||
searchbar_inputs = {
|
||||
'content': {'input': 'content', 'label': _('Search <span class="nolabel"> (in Content)</span>')},
|
||||
'message': {'input': 'message', 'label': _('Search in Messages')},
|
||||
'customer': {'input': 'customer', 'label': _('Search in Customer')},
|
||||
'id': {'input': 'id', 'label': _('Search in Reference')},
|
||||
'status': {'input': 'status', 'label': _('Search in Stage')},
|
||||
'all': {'input': 'all', 'label': _('Search in All')},
|
||||
}
|
||||
searchbar_groupby = {
|
||||
'none': {'input': 'none', 'label': _('None')},
|
||||
'stage': {'input': 'stage_id', 'label': _('Stage')},
|
||||
}
|
||||
|
||||
# default sort by value
|
||||
if not sortby:
|
||||
sortby = 'date'
|
||||
order = searchbar_sortings[sortby]['order']
|
||||
|
||||
if filterby in ['last_message_sup', 'last_message_cust']:
|
||||
discussion_subtype_id = request.env.ref('mail.mt_comment').id
|
||||
messages = request.env['mail.message'].search_read([('model', '=', 'odex25_helpdesk.ticket'), ('subtype_id', '=', discussion_subtype_id)], fields=['res_id', 'author_id'], order='date desc')
|
||||
last_author_dict = {}
|
||||
for message in messages:
|
||||
if message['res_id'] not in last_author_dict:
|
||||
last_author_dict[message['res_id']] = message['author_id'][0]
|
||||
|
||||
ticket_author_list = request.env['odex25_helpdesk.ticket'].search_read(fields=['id', 'partner_id'])
|
||||
ticket_author_dict = dict([(ticket_author['id'], ticket_author['partner_id'][0] if ticket_author['partner_id'] else False) for ticket_author in ticket_author_list])
|
||||
|
||||
last_message_cust = []
|
||||
last_message_sup = []
|
||||
for ticket_id in last_author_dict.keys():
|
||||
if last_author_dict[ticket_id] == ticket_author_dict[ticket_id]:
|
||||
last_message_cust.append(ticket_id)
|
||||
else:
|
||||
last_message_sup.append(ticket_id)
|
||||
|
||||
if filterby == 'last_message_cust':
|
||||
domain = [('id', 'in', last_message_cust)]
|
||||
else:
|
||||
domain = [('id', 'in', last_message_sup)]
|
||||
|
||||
else:
|
||||
domain = searchbar_filters[filterby]['domain']
|
||||
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||
|
||||
# search
|
||||
if search and search_in:
|
||||
search_domain = []
|
||||
if search_in in ('id', 'all'):
|
||||
search_domain = OR([search_domain, [('id', 'ilike', search)]])
|
||||
if search_in in ('content', 'all'):
|
||||
search_domain = OR([search_domain, ['|', ('name', 'ilike', search), ('description', 'ilike', search)]])
|
||||
if search_in in ('customer', 'all'):
|
||||
search_domain = OR([search_domain, [('partner_id', 'ilike', search)]])
|
||||
if search_in in ('message', 'all'):
|
||||
discussion_subtype_id = request.env.ref('mail.mt_comment').id
|
||||
search_domain = OR([search_domain, [('message_ids.body', 'ilike', search), ('message_ids.subtype_id', '=', discussion_subtype_id)]])
|
||||
if search_in in ('status', 'all'):
|
||||
search_domain = OR([search_domain, [('stage_id', 'ilike', search)]])
|
||||
domain += search_domain
|
||||
|
||||
# pager
|
||||
tickets_count = len(request.env['odex25_helpdesk.ticket'].search(domain))
|
||||
pager = portal_pager(
|
||||
url="/my/tickets",
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'search_in': search_in, 'search': search},
|
||||
total=tickets_count,
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
|
||||
tickets = request.env['odex25_helpdesk.ticket'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
|
||||
request.session['my_tickets_history'] = tickets.ids[:100]
|
||||
|
||||
if groupby == 'stage':
|
||||
grouped_tickets = [request.env['odex25_helpdesk.ticket'].concat(*g) for k, g in groupbyelem(tickets, itemgetter('stage_id'))]
|
||||
else:
|
||||
grouped_tickets = [tickets]
|
||||
|
||||
values.update({
|
||||
'date': date_begin,
|
||||
'grouped_tickets': grouped_tickets,
|
||||
'page_name': 'ticket',
|
||||
'default_url': '/my/tickets',
|
||||
'pager': pager,
|
||||
'searchbar_sortings': searchbar_sortings,
|
||||
'searchbar_filters': searchbar_filters,
|
||||
'searchbar_inputs': searchbar_inputs,
|
||||
'searchbar_groupby': searchbar_groupby,
|
||||
'sortby': sortby,
|
||||
'groupby': groupby,
|
||||
'search_in': search_in,
|
||||
'search': search,
|
||||
'filterby': filterby,
|
||||
})
|
||||
return request.render("odex25_helpdesk.portal_odex25_helpdesk_ticket", values)
|
||||
|
||||
@http.route([
|
||||
"/odex25_helpdesk/ticket/<int:ticket_id>",
|
||||
"/odex25_helpdesk/ticket/<int:ticket_id>/<access_token>",
|
||||
'/my/ticket/<int:ticket_id>',
|
||||
'/my/ticket/<int:ticket_id>/<access_token>'
|
||||
], type='http', auth="public", website=True)
|
||||
def tickets_followup(self, ticket_id=None, access_token=None, **kw):
|
||||
try:
|
||||
ticket_sudo = self._document_check_access('odex25_helpdesk.ticket', ticket_id, access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
values = self._ticket_get_page_view_values(ticket_sudo, access_token, **kw)
|
||||
return request.render("odex25_helpdesk.tickets_followup", values)
|
||||
|
||||
@http.route([
|
||||
'/my/ticket/close/<int:ticket_id>',
|
||||
'/my/ticket/close/<int:ticket_id>/<access_token>',
|
||||
], type='http', auth="public", website=True)
|
||||
def ticket_close(self, ticket_id=None, access_token=None, **kw):
|
||||
try:
|
||||
ticket_sudo = self._document_check_access('odex25_helpdesk.ticket', ticket_id, access_token)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
if not ticket_sudo.team_id.allow_portal_ticket_closing:
|
||||
raise UserError(_("The team does not allow ticket closing through portal"))
|
||||
|
||||
if not ticket_sudo.closed_by_partner:
|
||||
closing_stage = ticket_sudo.team_id._get_closing_stage()
|
||||
if ticket_sudo.stage_id != closing_stage:
|
||||
ticket_sudo.write({'stage_id': closing_stage[0].id, 'closed_by_partner': True})
|
||||
else:
|
||||
ticket_sudo.write({'closed_by_partner': True})
|
||||
body = _('Ticket closed by the customer')
|
||||
ticket_sudo.with_context(mail_create_nosubscribe=True).message_post(body=body, message_type='comment', subtype_xmlid='mail.mt_note')
|
||||
|
||||
return request.redirect('/my/ticket/%s/%s' % (ticket_id, access_token or ''))
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
class Websiteodex25_helpdesk(http.Controller):
|
||||
|
||||
@http.route(['/odex25_helpdesk/rating', '/odex25_helpdesk/rating/<model("odex25_helpdesk.team"):team>'], type='http', auth="public", website=True, sitemap=True)
|
||||
def page(self, team=False, **kw):
|
||||
# to avoid giving any access rights on Helpdesk team to the public user, let's use sudo
|
||||
# and check if the user should be able to view the team (team managers only if it's not published or has no rating)
|
||||
user = request.env.user
|
||||
team_domain = [('id', '=', team.id)] if team else []
|
||||
if user.has_group('odex25_helpdesk.group_heldpesk_manager'):
|
||||
domain = AND([[('use_rating', '=', True)], team_domain])
|
||||
else:
|
||||
domain = AND([[('use_rating', '=', True), ('portal_show_rating', '=', True)], team_domain])
|
||||
teams = request.env['odex25_helpdesk.team'].search(domain)
|
||||
team_values = []
|
||||
for team in teams:
|
||||
tickets = request.env['odex25_helpdesk.ticket'].sudo().search([('team_id', '=', team.id)])
|
||||
domain = [
|
||||
('res_model', '=', 'odex25_helpdesk.ticket'), ('res_id', 'in', tickets.ids),
|
||||
('consumed', '=', True), ('rating', '>=', 1),
|
||||
]
|
||||
ratings = request.env['rating.rating'].sudo().search(domain, order="id desc", limit=100)
|
||||
|
||||
yesterday = (datetime.date.today()-datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')
|
||||
stats = {}
|
||||
any_rating = False
|
||||
for x in (7, 30, 90):
|
||||
todate = (datetime.date.today()-datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00')
|
||||
domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)]
|
||||
stats[x] = {1: 0, 3: 0, 5: 0}
|
||||
rating_stats = request.env['rating.rating'].sudo().read_group(domdate, [], ['rating'])
|
||||
total = sum(st['rating_count'] for st in rating_stats)
|
||||
for rate in rating_stats:
|
||||
any_rating = True
|
||||
stats[x][rate['rating']] = (rate['rating_count'] * 100) / total
|
||||
values = {
|
||||
'team': team,
|
||||
'ratings': ratings if any_rating else False,
|
||||
'stats': stats,
|
||||
}
|
||||
team_values.append(values)
|
||||
return request.render('odex25_helpdesk.team_rating_page', {'page_name': 'rating', 'teams': team_values})
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="digest.digest_digest_default" model="digest.digest">
|
||||
<field name="kpi_odex25_helpdesk_tickets_closed">True</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data>
|
||||
<record id="digest_tip_odex25_helpdesk_0" model="digest.tip">
|
||||
<field name="name">Tip: Create tickets from incoming emails</field>
|
||||
<field name="sequence">1800</field>
|
||||
<field name="group_id" ref="odex25_helpdesk.group_odex25_helpdesk_manager" />
|
||||
<field name="tip_description" type="html">
|
||||
<div>
|
||||
% set record = object.env['odex25_helpdesk.team'].search([('alias_name', '!=', False)],limit=1)
|
||||
<b class="tip_title">Tip: Create tickets from incoming emails</b>
|
||||
% if record and record.alias_domain
|
||||
<p class="tip_content">Emails sent to <a href="mailto:${record.alias_id.display_name}" target="_blank" style="color: #875a7b; text-decoration: none;">${record.alias_id.display_name}</a> generate tickets in your pipeline.</p>
|
||||
% else
|
||||
<p class="tip_content">Emails sent to a Helpdesk Team alias generate tickets in your pipeline.</p>
|
||||
% endif
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
|
||||
<!-- odex25_helpdesk-specific activities, for automatic generation mainly -->
|
||||
<record id="mail_act_odex25_helpdesk_handle" model="mail.activity.type">
|
||||
<field name="name">Handle Ticket</field>
|
||||
<field name="icon">fa-ticket</field>
|
||||
<field name="res_model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
</record>
|
||||
|
||||
<!-- Ticket related subtypes for messaging / Chatter -->
|
||||
<record id="mt_ticket_new" model="mail.message.subtype">
|
||||
<field name="name">Ticket Created</field>
|
||||
<field name="sequence">0</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="hidden" eval="True"/>
|
||||
<field name="description">Ticket created</field>
|
||||
</record>
|
||||
<record id="mt_ticket_rated" model="mail.message.subtype">
|
||||
<field name="name">Ticket Rated</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
<field name="description">Ticket rated</field>
|
||||
</record>
|
||||
<record id="mt_ticket_stage" model="mail.message.subtype">
|
||||
<field name="name">Stage Changed</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="description">Stage Changed</field>
|
||||
</record>
|
||||
|
||||
<!-- Team related subtypes for messaging / Chatter -->
|
||||
<record id="mt_team_ticket_new" model="mail.message.subtype">
|
||||
<field name="name">Ticket Created</field>
|
||||
<field name="sequence">0</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="parent_id" ref="mt_ticket_new"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
<record id="mt_team_ticket_rated" model="mail.message.subtype">
|
||||
<field name="name">Ticket Rated</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="parent_id" ref="mt_ticket_rated"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
<record id="mt_team_ticket_stage" model="mail.message.subtype">
|
||||
<field name="name">Ticket Stage Changed</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="parent_id" ref="mt_ticket_stage"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
|
||||
<record id="new_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Ticket: Reception Acknowledgment</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="subject">${object.display_name}</field>
|
||||
<field name="email_from">${(object.user_id.email_formatted or user.email_formatted) | safe}</field>
|
||||
<field name="email_to">${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div>
|
||||
Dear ${object.sudo().partner_id.name or 'Madam/Sir'},<br /><br />
|
||||
Your request
|
||||
% if object.get_portal_url():
|
||||
<a href="/my/ticket/${object.id}/${object.access_token}">${object.name}</a>
|
||||
% endif
|
||||
has been received and is being reviewed by our ${object.team_id.name or ''} team.
|
||||
The reference of your ticket is ${object.id}.<br /><br />
|
||||
|
||||
<div style="text-align: center; margin: 16px 0px 16px 0px;">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;" href="${object.get_portal_url()}">View the ticket</a><br/>
|
||||
</div>
|
||||
|
||||
To add additional comments, reply to this email.<br/><br/>
|
||||
|
||||
Thank you,<br/><br/>
|
||||
${object.team_id.name or 'Helpdesk'} Team.
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">${object.partner_id.lang or object.user_id.lang or user.lang}</field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="solved_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Ticket: Solved</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="subject">${object.display_name}</field>
|
||||
<field name="email_from">${(object.user_id.email_formatted or user.email_formatted) | safe}</field>
|
||||
<field name="email_to">${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div>
|
||||
Dear ${object.sudo().partner_id.name or 'Madam/Sir'},<br /><br />
|
||||
This automatic message informs you that we have closed your ticket (reference ${object.id}).
|
||||
We hope that the services provided have met your expectations.
|
||||
If you have any more questions or comments, don't hesitate to reply to this e-mail to re-open your ticket.<br /><br />
|
||||
Thank you for your cooperation.<br />
|
||||
Kind regards,<br /><br />
|
||||
${object.team_id.name or 'Helpdesk'} Team.
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">${object.partner_id.lang or object.user_id.lang or user.lang}</field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="rating_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Ticket: Rating Request (requires rating enabled on team)</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="subject">${object.company_id.name or object.user_id.company_id.name or 'Helpdesk'}: Service Rating Request</field>
|
||||
<field name="email_from">${object.rating_get_rated_partner_id().email_formatted | safe}</field>
|
||||
<field name="email_to">${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div>
|
||||
% set access_token = object.rating_get_access_token()
|
||||
% set partner = object.rating_get_partner_id()
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0;">
|
||||
<tbody>
|
||||
<tr><td valign="top" style="font-size: 14px;">
|
||||
% if partner.name:
|
||||
Hello ${partner.name},<br/>
|
||||
% else:
|
||||
Hello,<br/>
|
||||
% endif
|
||||
Please take a moment to rate our services related to the ticket "<strong>${object.name}</strong>"
|
||||
% if object.rating_get_rated_partner_id().name:
|
||||
assigned to <strong>${object.rating_get_rated_partner_id().name}</strong>.<br/>
|
||||
% else:
|
||||
.<br/>
|
||||
% endif
|
||||
</td></tr>
|
||||
<tr><td style="text-align: center;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" summary="o_mail_notification" style="width:100%; margin: 32px 0px 32px 0px;">
|
||||
<tr><td style="font-size: 14px;">
|
||||
<strong>Tell us how you feel about our service</strong><br/>
|
||||
<span style="text-color: #888888">(click on one of these smileys)</span>
|
||||
</td></tr>
|
||||
<tr><td style="font-size: 14px;">
|
||||
<table style="width:100%;text-align:center;">
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/rate/${access_token}/5">
|
||||
<img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rate/${access_token}/3">
|
||||
<img alt="Not satisfied" src="/rating/static/src/img/rating_3.png" title="Not satisfied"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/rate/${access_token}/1">
|
||||
<img alt="Highly Dissatisfied" src="/rating/static/src/img/rating_1.png" title="Highly Dissatisfied"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td valign="top" style="font-size: 14px;">
|
||||
We appreciate your feedback. It helps us to improve continuously.
|
||||
<br/><span style="margin: 0px 0px 0px 0px; font-size: 12px; opacity: 0.5; color: #454748;">This customer survey has been sent because your ticket has been moved to the stage <b>${object.stage_id.name}</b></span>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</field>
|
||||
<field name="lang">${object.partner_id.lang or object.user_id.lang or user.lang}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="odex25_helpdesk_team1" model="odex25_helpdesk.team">
|
||||
<field name="name">Customer Care</field>
|
||||
<field name="alias_name">support</field>
|
||||
<field name="stage_ids" eval="False"/> <!-- eval=False to don't get the default stage. New stages are setted below-->
|
||||
<field name="use_sla" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- stage "New" gets created by default with sequence 0-->
|
||||
<record id="stage_new" model="odex25_helpdesk.stage">
|
||||
<field name="name">New</field>
|
||||
<field name="sequence">0</field>
|
||||
<field name="team_ids" eval="[(4, ref('odex25_helpdesk_team1'))]"/>
|
||||
<field name="is_close" eval="False"/>
|
||||
<field name="template_id" ref="odex25_helpdesk.new_ticket_request_email_template"/>
|
||||
</record>
|
||||
<record id="stage_in_progress" model="odex25_helpdesk.stage">
|
||||
<field name="name">In Progress</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="team_ids" eval="[(4, ref('odex25_helpdesk_team1'))]"/>
|
||||
<field name="is_close" eval="False"/>
|
||||
</record>
|
||||
<record id="stage_solved" model="odex25_helpdesk.stage">
|
||||
<field name="name">Solved</field>
|
||||
<field name="team_ids" eval="[(4, ref('odex25_helpdesk_team1'))]"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="is_close" eval="True"/>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
<record id="stage_cancelled" model="odex25_helpdesk.stage">
|
||||
<field name="name">Cancelled</field>
|
||||
<field name="sequence">3</field>
|
||||
<field name="team_ids" eval="[(4, ref('odex25_helpdesk_team1'))]"/>
|
||||
<field name="is_close" eval="True"/>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="type_question" model="odex25_helpdesk.ticket.type">
|
||||
<field name="name">Question</field>
|
||||
</record>
|
||||
<record id="type_incident" model="odex25_helpdesk.ticket.type">
|
||||
<field name="name">Issue</field>
|
||||
</record>
|
||||
|
||||
<!-- Share Button in action menu -->
|
||||
<record id="model_odex25_helpdesk_ticket_action_share" model="ir.actions.server">
|
||||
<field name="name">Share</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="binding_model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.action_share()</field>
|
||||
</record>
|
||||
|
||||
<!-- Report Filter -->
|
||||
<record id="odex25_helpdesk_sla_report_analysis_filter_status_per_deadline" model="ir.filters">
|
||||
<field name="name">Status Per Deadline</field>
|
||||
<field name="model_id">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="context">{
|
||||
'pivot_column_groupby': ['sla_deadline:day'],
|
||||
'pivot_row_groupby': ['team_id', 'ticket_id', 'sla_id']
|
||||
}</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_report_analysis_filter_stage_failed" model="ir.filters">
|
||||
<field name="name">Failed SLA Stage per Month</field>
|
||||
<field name="model_id">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="context">{
|
||||
'pivot_measures': ['__count'],
|
||||
'pivot_column_groupby': ['create_date:month'],
|
||||
'pivot_row_groupby': ['team_id', 'sla_stage_id']
|
||||
}</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="user_id" eval="False"/>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="tag_crm" model="odex25_helpdesk.tag">
|
||||
<field name="name">CRM</field>
|
||||
</record>
|
||||
<record id="tag_website" model="odex25_helpdesk.tag">
|
||||
<field name="name">Website</field>
|
||||
</record>
|
||||
<record id="tag_service" model="odex25_helpdesk.tag">
|
||||
<field name="name">Service</field>
|
||||
</record>
|
||||
<record id="tag_repair" model="odex25_helpdesk.tag">
|
||||
<field name="name">Repair</field>
|
||||
</record>
|
||||
|
||||
<!-- Set the demo user as a Helpdesk user -->
|
||||
<record id="group_odex25_helpdesk_user" model="res.groups">
|
||||
<field name="users" eval="[(4,ref('base.user_demo'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Helpdesk stages -->
|
||||
<record id="stage_done" model="odex25_helpdesk.stage">
|
||||
<field name="name">Done</field>
|
||||
<field name="sequence">2</field>
|
||||
<field name="template_id" ref="odex25_helpdesk.rating_ticket_request_email_template"/>
|
||||
<field name="is_close" eval="True"/>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Helpdesk team -->
|
||||
<record id="odex25_helpdesk_team3" model="odex25_helpdesk.team">
|
||||
<field name="name">VIP Support</field>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('odex25_helpdesk.stage_new'), ref('odex25_helpdesk.stage_in_progress'), ref('odex25_helpdesk.stage_done'), ref('odex25_helpdesk.stage_cancelled')])]"/>
|
||||
<field name="use_sla" eval="True"/>
|
||||
<field name="use_rating" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- SLA's -->
|
||||
<record id="odex25_helpdesk_sla_1" model="odex25_helpdesk.sla">
|
||||
<field name="name">2 days to start</field>
|
||||
<field name="team_id" ref="odex25_helpdesk_team1"/>
|
||||
<field name="stage_id" ref="stage_in_progress"/>
|
||||
<field name="time_days">2</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_2" model="odex25_helpdesk.sla">
|
||||
<field name="name">7 days to finish</field>
|
||||
<field name="team_id" ref="odex25_helpdesk_team1"/>
|
||||
<field name="stage_id" ref="stage_solved"/>
|
||||
<field name="time_days">7</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_3" model="odex25_helpdesk.sla">
|
||||
<field name="name">Assigned within 24 hrs</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="time_days">1</field>
|
||||
<field name="priority">3</field>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('odex25_helpdesk.tag_service'),ref('odex25_helpdesk.tag_repair')])]"/>
|
||||
<field name="target_type">assigning</field>
|
||||
</record>
|
||||
|
||||
<!-- Tickets -->
|
||||
<record id="odex25_helpdesk_ticket_1" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Kitchen collapsing</field>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_2" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Where can I download a catalog ?</field>
|
||||
<field name="priority">0</field>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_3" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Warranty</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="partner_id" ref="base.res_partner_main1" />
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I would like to know what kind of warranties you are offering for your products.
|
||||
|
||||
Here is my contact number: 123456789
|
||||
|
||||
Thank you,
|
||||
Chester Reed
|
||||
</field>
|
||||
</record>
|
||||
<!-- fail the sla status -->
|
||||
<function model="odex25_helpdesk.sla.status" name="write">
|
||||
<value model="odex25_helpdesk.ticket" eval="obj().search([('id', '=', ref('odex25_helpdesk.odex25_helpdesk_ticket_3'))]).sla_status_ids.ids"/>
|
||||
<value eval="{'deadline': DateTime.now() - relativedelta(days=2)}"/>
|
||||
</function>
|
||||
|
||||
<record id="odex25_helpdesk_ticket_4" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Wood Treatment</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_4" />
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
Is the wood from your furniture treated with a particular product? What would you recommend to maintain the quality of a dining table?
|
||||
|
||||
Your assistance would be greatly appreciated.
|
||||
|
||||
Thanks in Advance,
|
||||
Azure Interior
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_5" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Chair dimensions</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_solved"/>
|
||||
<field name="description">
|
||||
Can you please tell me the dimensions of your “Office chair Black”? Also I am unable to find the information on your official site.
|
||||
|
||||
I look forward to your kind response.
|
||||
|
||||
Thank you!
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_6" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Lost key</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I bought a locker a few years ago and I, unfortunately, lost the key. I cannot retrieve the documents I had left in there without damaging the furniture item. What solution do you offer?
|
||||
|
||||
Thanks in advance for your help.
|
||||
Kind regards,
|
||||
Gemini Furniture
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_7" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Furniture delivery</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team1"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_cancelled"/>
|
||||
<field name="description">
|
||||
Hi,
|
||||
|
||||
I was wondering if you were delivering the furniture or if we needed to pick it up at your warehouse?
|
||||
If you do take care of the delivery, are there any extra costs?
|
||||
|
||||
Regards,
|
||||
Deco Addict
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_8" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Cabinets in kit</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I would like to know if your cabinets come in a kit? They seem quite large and I am not sure they will fit through my front door.
|
||||
|
||||
Thank you for your help.
|
||||
Best regards,
|
||||
Jackson Group
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_9" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Missing user manual</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I recently purchased one of your wardrobes in a kit. Unfortunately, I didn’t receive the user manual, so I cannot assemble the item. Could you send me this document?
|
||||
|
||||
Thank you.
|
||||
Kind regards,
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_10" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Ugly Chair</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_done"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I purchased a chair from you last week. I now realize it doesn’t go well with the rest of my furniture, so I would like to return it and to get a refund.
|
||||
|
||||
Regards,
|
||||
Deco Addict
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_11" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Couch</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
The couch I ordered was scratched during the delivery. Would it be possible to have a gesture of goodwill?
|
||||
|
||||
Thank you for considering my request.
|
||||
Best regards,
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_12" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Chair wheels aren’t working</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="partner_id" ref="base.res_partner_main1"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('odex25_helpdesk.tag_repair'),ref('odex25_helpdesk.tag_service')])]"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=1)"/>
|
||||
<field name="description">
|
||||
The chair I bought last year isn't turning correctly anymore. Are you selling spare parts for the wheels?
|
||||
|
||||
Thank you in advance for your help.
|
||||
Chester Reed
|
||||
</field>
|
||||
</record>
|
||||
<!-- Fail the sla on ticket -->
|
||||
<function model="odex25_helpdesk.sla.status" name="write">
|
||||
<value model="odex25_helpdesk.ticket" eval="obj().search([('id', '=', ref('odex25_helpdesk.odex25_helpdesk_ticket_12'))]).sla_status_ids.ids"/>
|
||||
<value eval="{'deadline': DateTime.now()}"/>
|
||||
</function>
|
||||
|
||||
<record id="odex25_helpdesk_ticket_13" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Cabinet Colour and Lock aren't proper</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('odex25_helpdesk.tag_repair'),ref('odex25_helpdesk.tag_service')])]"/>
|
||||
<field name="description">
|
||||
Hi,
|
||||
|
||||
I purchased a "Cabinet With Doors" from your store a few days ago. The lock is not working properly and the color is wrong. This is unacceptable! I am asking for a product that corresponds to my order and that matches the quality you are advertising.
|
||||
|
||||
Regards,
|
||||
The Jackson Group
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_14" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Lamp Stand is bent</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_new"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
Yesterday I purchased a lamp stand from your site but the product I received is bent.
|
||||
|
||||
Would it be possible to get a replacement?
|
||||
|
||||
Regards,
|
||||
Ready Mat
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_15" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Table legs are unbalanced</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('odex25_helpdesk.tag_repair'),ref('odex25_helpdesk.tag_service')])]"/>
|
||||
<field name="description">
|
||||
Hi,
|
||||
|
||||
A few days ago, I bought a Four Persons Desk. While assembling it in my office, I found that the legs of the table were not properly balanced. Could you please come and fix this?
|
||||
|
||||
Kindly do this as early as possible.
|
||||
|
||||
Best,
|
||||
Azure Interior
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_16" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Drawer’s slides and handle have a defect</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="description">
|
||||
Hi,
|
||||
|
||||
I have purchased a "Drawer" from your store but the slides and the handle seem to have a defect.
|
||||
|
||||
Would it be possible for you to fix it?
|
||||
|
||||
Regards,
|
||||
Deco
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_17" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Want to change the place of the dining area</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_question"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_in_progress"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I want to change the location of the dining area and would like your advice.
|
||||
|
||||
Hope to hear from you soon.
|
||||
|
||||
Best,
|
||||
Gemini Furniture
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_18" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Received Product is damaged</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_done"/>
|
||||
<field name="description">
|
||||
Hi,
|
||||
|
||||
I ordered a "Table Kit" from your store but the delivered product is damaged. I demand a refund as soon as possible.
|
||||
|
||||
Regards,
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_19" model="odex25_helpdesk.ticket">
|
||||
<field name="name">Delivered wood panel is not what I ordered</field>
|
||||
<field name="team_id" ref="odex25_helpdesk.odex25_helpdesk_team3"/>
|
||||
<field name="ticket_type_id" ref="odex25_helpdesk.type_incident"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="stage_id" ref="odex25_helpdesk.stage_done"/>
|
||||
<field name="description">
|
||||
Hello,
|
||||
|
||||
I ordered a wood panel from your online store, but the delivered product is not what I had ordered.
|
||||
|
||||
Could you please replace it with the right product?
|
||||
Waiting for your response.
|
||||
|
||||
Best,
|
||||
Wood Corner
|
||||
</field>
|
||||
</record>
|
||||
<function model="odex25_helpdesk.ticket" name="rating_apply" eval="([ref('odex25_helpdesk_ticket_18')], 3, None,'Good Service')"/>
|
||||
<function model="odex25_helpdesk.ticket" name="rating_apply" eval="([ref('odex25_helpdesk_ticket_19')], 5, None,'Awesome Service.\nLove to use your product')"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def migrate(cr, version):
|
||||
cr.execute("""
|
||||
UPDATE odex25_helpdesk_sla
|
||||
SET time_days = COALESCE(time_days, 0),
|
||||
time_hours = COALESCE(time_hours, 0),
|
||||
time_minutes = COALESCE(time_minutes, 0)
|
||||
""")
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import digest
|
||||
from . import odex25_helpdesk
|
||||
from . import odex25_helpdesk_ticket
|
||||
from . import res_users
|
||||
from . import res_partner
|
||||
from . import res_setting
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_odex25_helpdesk_tickets_closed = fields.Boolean('Tickets Closed')
|
||||
kpi_odex25_helpdesk_tickets_closed_value = fields.Integer(compute='_compute_kpi_odex25_helpdesk_tickets_closed_value')
|
||||
|
||||
def _compute_kpi_odex25_helpdesk_tickets_closed_value(self):
|
||||
if not self.env.user.has_group('odex25_helpdesk.group_odex25_helpdesk_user'):
|
||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||
for record in self:
|
||||
start, end, company = record._get_kpi_compute_parameters()
|
||||
closed_ticket = self.env['odex25_helpdesk.ticket'].search_count([
|
||||
('close_date', '>=', start),
|
||||
('close_date', '<', end),
|
||||
('company_id', '=', company.id)
|
||||
])
|
||||
record.kpi_odex25_helpdesk_tickets_closed_value = closed_ticket
|
||||
|
||||
def _compute_kpis_actions(self, company, user):
|
||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
||||
res['kpi_odex25_helpdesk_tickets_closed'] = 'odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main&menu_id=%s' % self.env.ref('odex25_helpdesk.menu_odex25_helpdesk_root').id
|
||||
return res
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import ast
|
||||
import datetime
|
||||
|
||||
from dateutil import relativedelta
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.odex25_helpdesk.models.odex25_helpdesk_ticket import TICKET_PRIORITY
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class odex25_helpdeskTeam(models.Model):
|
||||
_name = "odex25_helpdesk.team"
|
||||
_inherit = ['mail.alias.mixin', 'mail.thread', 'rating.parent.mixin']
|
||||
_description = "Helpdesk Team"
|
||||
_order = 'sequence,name'
|
||||
_rating_satisfaction_days = False # takes all existing ratings
|
||||
|
||||
_sql_constraints = [('not_portal_show_rating_if_not_use_rating',
|
||||
'check (portal_show_rating = FALSE OR use_rating = TRUE)',
|
||||
'Cannot show ratings in portal if not using them'), ]
|
||||
|
||||
def _default_stage_ids(self):
|
||||
default_stage = self.env['odex25_helpdesk.stage'].search([('name', '=', _('New'))], limit=1)
|
||||
if default_stage:
|
||||
return [(4, default_stage.id)]
|
||||
return [(0, 0, {'name': _('New'), 'sequence': 0, 'template_id': self.env.ref('odex25_helpdesk.new_ticket_request_email_template', raise_if_not_found=False) or None})]
|
||||
|
||||
def _default_domain_member_ids(self):
|
||||
return [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]
|
||||
|
||||
name = fields.Char('Helpdesk Team', required=True, translate=True)
|
||||
description = fields.Text('About Team', translate=True)
|
||||
active = fields.Boolean(default=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
|
||||
sequence = fields.Integer("Sequence", default=10)
|
||||
color = fields.Integer('Color Index', default=1)
|
||||
stage_ids = fields.Many2many(
|
||||
'odex25_helpdesk.stage', relation='team_stage_rel', string='Stages',
|
||||
default=_default_stage_ids,
|
||||
help="Stages the team will use. This team's tickets will only be able to be in these stages.")
|
||||
assign_method = fields.Selection([
|
||||
('manual', 'Manually'),
|
||||
('randomly', 'Random'),
|
||||
('balanced', 'Balanced')], string='Assignment Method', default='manual',
|
||||
compute='_compute_assign_method', store=True, readonly=False, required=True,
|
||||
help='Automatic assignment method for new tickets:\n'
|
||||
'\tManually: manual\n'
|
||||
'\tRandomly: randomly but everyone gets the same amount\n'
|
||||
'\tBalanced: to the person with the least amount of open tickets')
|
||||
member_ids = fields.Many2many('res.users', string='Team Members', domain=lambda self: self._default_domain_member_ids())
|
||||
visibility_member_ids = fields.Many2many('res.users', 'odex25_helpdesk_visibility_team', string='Team Visibility', domain=lambda self: self._default_domain_member_ids(),
|
||||
help="Team Members to whom this team will be visible. Keep empty for everyone to see this team.")
|
||||
ticket_ids = fields.One2many('odex25_helpdesk.ticket', 'team_id', string='Tickets')
|
||||
|
||||
use_alias = fields.Boolean('Email alias', default=True)
|
||||
has_external_mail_server = fields.Boolean(compute='_compute_has_external_mail_server')
|
||||
allow_portal_ticket_closing = fields.Boolean('Ticket closing', help="Allow customers to close their tickets")
|
||||
use_website_helpdesk_form = fields.Boolean('Website Form')
|
||||
use_website_helpdesk_livechat = fields.Boolean('Live chat',
|
||||
help="In Channel: You can create a new ticket by typing /Helpdesk [ticket title]. You can search ticket by typing /odex25_helpdesk_search [Keyword1],[Keyword2],.")
|
||||
use_website_helpdesk_forum = fields.Boolean('Help Center')
|
||||
use_website_helpdesk_slides = fields.Boolean('Enable eLearning')
|
||||
use_odex25_helpdesk_timesheet = fields.Boolean('Timesheet on Ticket', help="This required to have project module installed.")
|
||||
use_odex25_helpdesk_sale_timesheet = fields.Boolean(
|
||||
'Time Reinvoicing', compute='_compute_use_odex25_helpdesk_sale_timesheet', store=True,
|
||||
readonly=False, help="Reinvoice the time spent on ticket through tasks.")
|
||||
use_credit_notes = fields.Boolean('Refunds')
|
||||
use_coupons = fields.Boolean('Coupons')
|
||||
use_product_returns = fields.Boolean('Returns')
|
||||
use_product_repairs = fields.Boolean('Repairs')
|
||||
use_twitter = fields.Boolean('Twitter')
|
||||
use_api = fields.Boolean('API')
|
||||
use_rating = fields.Boolean('Ratings on tickets')
|
||||
portal_show_rating = fields.Boolean(
|
||||
'Display Rating on Customer Portal', compute='_compute_portal_show_rating', store=True,
|
||||
readonly=False)
|
||||
portal_rating_url = fields.Char('URL to Submit an Issue', readonly=True, compute='_compute_portal_rating_url')
|
||||
use_sla = fields.Boolean('SLA Policies')
|
||||
upcoming_sla_fail_tickets = fields.Integer(string='Upcoming SLA Fail Tickets', compute='_compute_upcoming_sla_fail_tickets')
|
||||
unassigned_tickets = fields.Integer(string='Unassigned Tickets', compute='_compute_unassigned_tickets')
|
||||
resource_calendar_id = fields.Many2one('resource.calendar', 'Working Hours',
|
||||
default=lambda self: self.env.company.resource_calendar_id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
|
||||
is_internal_team = fields.Boolean('Internal Team', default=False)
|
||||
is_vip_team = fields.Boolean('VIP Team', default=False)
|
||||
|
||||
@api.depends('name', 'portal_show_rating')
|
||||
def _compute_portal_rating_url(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for team in self:
|
||||
if team.name and team.portal_show_rating and team.id:
|
||||
team.portal_rating_url = '%s/odex25_helpdesk/rating/%s' % (base_url, slug(team))
|
||||
else:
|
||||
team.portal_rating_url = False
|
||||
|
||||
def _compute_has_external_mail_server(self):
|
||||
self.has_external_mail_server = self.env['ir.config_parameter'].sudo().get_param('base_setup.default_external_email_server')
|
||||
|
||||
def _compute_upcoming_sla_fail_tickets(self):
|
||||
ticket_data = self.env['odex25_helpdesk.ticket'].read_group([
|
||||
('team_id', 'in', self.ids),
|
||||
('sla_deadline', '!=', False),
|
||||
('sla_deadline', '<=', fields.Datetime.to_string((datetime.date.today() + relativedelta.relativedelta(days=1)))),
|
||||
], ['team_id'], ['team_id'])
|
||||
mapped_data = dict((data['team_id'][0], data['team_id_count']) for data in ticket_data)
|
||||
for team in self:
|
||||
team.upcoming_sla_fail_tickets = mapped_data.get(team.id, 0)
|
||||
|
||||
def _compute_unassigned_tickets(self):
|
||||
ticket_data = self.env['odex25_helpdesk.ticket'].read_group([('user_id', '=', False), ('team_id', 'in', self.ids), ('stage_id.is_close', '!=', True)], ['team_id'], ['team_id'])
|
||||
mapped_data = dict((data['team_id'][0], data['team_id_count']) for data in ticket_data)
|
||||
for team in self:
|
||||
team.unassigned_tickets = mapped_data.get(team.id, 0)
|
||||
|
||||
@api.depends('use_rating')
|
||||
def _compute_portal_show_rating(self):
|
||||
without_rating = self.filtered(lambda t: not t.use_rating)
|
||||
without_rating.update({'portal_show_rating': False})
|
||||
|
||||
@api.depends('member_ids', 'visibility_member_ids')
|
||||
def _compute_assign_method(self):
|
||||
with_manual = self.filtered(lambda t: not t.member_ids and not t.visibility_member_ids)
|
||||
with_manual.update({'assign_method': 'manual'})
|
||||
|
||||
@api.onchange('use_alias', 'name')
|
||||
def _onchange_use_alias(self):
|
||||
if not self.use_alias:
|
||||
self.alias_name = False
|
||||
|
||||
@api.depends('use_odex25_helpdesk_timesheet')
|
||||
def _compute_use_odex25_helpdesk_sale_timesheet(self):
|
||||
without_timesheet = self.filtered(lambda t: not t.use_odex25_helpdesk_timesheet)
|
||||
without_timesheet.update({'use_odex25_helpdesk_sale_timesheet': False})
|
||||
|
||||
@api.constrains('assign_method', 'member_ids', 'visibility_member_ids')
|
||||
def _check_member_assignation(self):
|
||||
if not self.member_ids and not self.visibility_member_ids and self.assign_method != 'manual':
|
||||
raise ValidationError(_("You must have team members assigned to change the assignment method."))
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM overrides
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
team = super(odex25_helpdeskTeam, self.with_context(mail_create_nosubscribe=True)).create(vals)
|
||||
team.sudo()._check_sla_group()
|
||||
team.sudo()._check_modules_to_install()
|
||||
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
|
||||
return team
|
||||
|
||||
def write(self, vals):
|
||||
result = super(odex25_helpdeskTeam, self).write(vals)
|
||||
if 'active' in vals:
|
||||
self.with_context(active_test=False).mapped('ticket_ids').write({'active': vals['active']})
|
||||
self.sudo()._check_sla_group()
|
||||
self.sudo()._check_modules_to_install()
|
||||
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
|
||||
return result
|
||||
|
||||
def unlink(self):
|
||||
stages = self.mapped('stage_ids').filtered(lambda stage: stage.team_ids <= self) # remove stages that only belong to team in self
|
||||
stages.unlink()
|
||||
return super(odex25_helpdeskTeam, self).unlink()
|
||||
|
||||
def _check_sla_group(self):
|
||||
for team in self:
|
||||
if team.use_sla and not self.user_has_groups('odex25_helpdesk.group_use_sla'):
|
||||
self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').write({'implied_ids': [(4, self.env.ref('odex25_helpdesk.group_use_sla').id)]})
|
||||
if team.use_sla:
|
||||
self.env['odex25_helpdesk.sla'].with_context(active_test=False).search([('team_id', '=', team.id), ('active', '=', False)]).write({'active': True})
|
||||
else:
|
||||
self.env['odex25_helpdesk.sla'].search([('team_id', '=', team.id)]).write({'active': False})
|
||||
if not self.search_count([('use_sla', '=', True)]):
|
||||
self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').write({'implied_ids': [(3, self.env.ref('odex25_helpdesk.group_use_sla').id)]})
|
||||
self.env.ref('odex25_helpdesk.group_use_sla').write({'users': [(5, 0, 0)]})
|
||||
|
||||
def _check_modules_to_install(self):
|
||||
# mapping of field names to module names
|
||||
FIELD_MODULE = {
|
||||
'use_website_helpdesk_form': 'odex25_website_helpdesk_form',
|
||||
'use_website_helpdesk_livechat': 'odex25_website_helpdesk_livechat',
|
||||
'use_website_helpdesk_forum': 'odex25_website_helpdesk_forum',
|
||||
'use_website_helpdesk_slides': 'odex25_website_helpdesk_slides',
|
||||
'use_odex25_helpdesk_timesheet': 'odex25_helpdesk_timesheet',
|
||||
'use_odex25_helpdesk_sale_timesheet': 'odex25_helpdesk_sale_timesheet',
|
||||
'use_credit_notes': 'odex25_helpdesk_account',
|
||||
'use_product_returns': 'odex25_helpdesk_stock',
|
||||
'use_product_repairs': 'odex25_helpdesk_repair',
|
||||
'use_coupons': 'odex25_helpdesk_sale_coupon',
|
||||
}
|
||||
|
||||
# determine the modules to be installed
|
||||
expected = [
|
||||
mname
|
||||
for fname, mname in FIELD_MODULE.items()
|
||||
if any(team[fname] for team in self)
|
||||
]
|
||||
modules = self.env['ir.module.module']
|
||||
if expected:
|
||||
STATES = ('installed', 'to install', 'to upgrade')
|
||||
modules = modules.search([('name', 'in', expected)])
|
||||
modules = modules.filtered(lambda module: module.state not in STATES)
|
||||
|
||||
# other stuff
|
||||
for team in self:
|
||||
if team.use_rating:
|
||||
for stage in team.stage_ids:
|
||||
if stage.is_close and not stage.fold:
|
||||
stage.template_id = self.env.ref('odex25_helpdesk.rating_ticket_request_email_template', raise_if_not_found= False)
|
||||
|
||||
if modules:
|
||||
modules.button_immediate_install()
|
||||
|
||||
# just in case we want to do something if we install a module. (like a refresh ...)
|
||||
return bool(modules)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Mail Alias Mixin
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super(odex25_helpdeskTeam, self)._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get('odex25_helpdesk.ticket').id
|
||||
if self.id:
|
||||
values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
|
||||
defaults['team_id'] = self.id
|
||||
return values
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Business Methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def retrieve_dashboard(self):
|
||||
domain = [('user_id', '=', self.env.uid)]
|
||||
group_fields = ['priority', 'create_date', 'stage_id', 'close_hours']
|
||||
list_fields = ['priority', 'create_date', 'stage_id', 'close_hours']
|
||||
#TODO: remove SLA calculations if user_uses_sla is false.
|
||||
user_uses_sla = self.user_has_groups('odex25_helpdesk.group_use_sla') and\
|
||||
bool(self.env['odex25_helpdesk.team'].search([('use_sla', '=', True), '|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)]))
|
||||
|
||||
if user_uses_sla:
|
||||
group_fields.insert(1, 'sla_deadline:year')
|
||||
group_fields.insert(2, 'sla_deadline:hour')
|
||||
group_fields.insert(3, 'sla_reached_late')
|
||||
list_fields.insert(1, 'sla_deadline')
|
||||
list_fields.insert(2, 'sla_reached_late')
|
||||
|
||||
odex25_helpdeskTicket = self.env['odex25_helpdesk.ticket']
|
||||
tickets = odex25_helpdeskTicket.search_read(expression.AND([domain, [('stage_id.is_close', '=', False)]]), ['sla_deadline', 'open_hours', 'sla_reached_late', 'priority'])
|
||||
|
||||
result = {
|
||||
'odex25_helpdesk_target_closed': self.env.user.odex25_helpdesk_target_closed,
|
||||
'odex25_helpdesk_target_rating': self.env.user.odex25_helpdesk_target_rating,
|
||||
'odex25_helpdesk_target_success': self.env.user.odex25_helpdesk_target_success,
|
||||
'today': {'count': 0, 'rating': 0, 'success': 0},
|
||||
'7days': {'count': 0, 'rating': 0, 'success': 0},
|
||||
'my_all': {'count': 0, 'hours': 0, 'failed': 0},
|
||||
'my_high': {'count': 0, 'hours': 0, 'failed': 0},
|
||||
'my_urgent': {'count': 0, 'hours': 0, 'failed': 0},
|
||||
'show_demo': not bool(odex25_helpdeskTicket.search([], limit=1)),
|
||||
'rating_enable': False,
|
||||
'success_rate_enable': user_uses_sla
|
||||
}
|
||||
|
||||
def _is_sla_failed(data):
|
||||
deadline = data.get('sla_deadline')
|
||||
sla_deadline = fields.Datetime.now() > deadline if deadline else False
|
||||
return sla_deadline or data.get('sla_reached_late')
|
||||
|
||||
def add_to(ticket, key="my_all"):
|
||||
result[key]['count'] += 1
|
||||
result[key]['hours'] += ticket['open_hours']
|
||||
if _is_sla_failed(ticket):
|
||||
result[key]['failed'] += 1
|
||||
|
||||
for ticket in tickets:
|
||||
add_to(ticket, 'my_all')
|
||||
if ticket['priority'] == '2':
|
||||
add_to(ticket, 'my_high')
|
||||
if ticket['priority'] == '3':
|
||||
add_to(ticket, 'my_urgent')
|
||||
|
||||
dt = fields.Date.today()
|
||||
tickets = odex25_helpdeskTicket.read_group(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)], list_fields, group_fields, lazy=False)
|
||||
for ticket in tickets:
|
||||
result['today']['count'] += ticket['__count']
|
||||
if not _is_sla_failed(ticket):
|
||||
result['today']['success'] += ticket['__count']
|
||||
|
||||
dt = fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6)))
|
||||
tickets = odex25_helpdeskTicket.read_group(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)], list_fields, group_fields, lazy=False)
|
||||
for ticket in tickets:
|
||||
result['7days']['count'] += ticket['__count']
|
||||
if not _is_sla_failed(ticket):
|
||||
result['7days']['success'] += ticket['__count']
|
||||
|
||||
result['today']['success'] = (result['today']['success'] * 100) / (result['today']['count'] or 1)
|
||||
result['7days']['success'] = (result['7days']['success'] * 100) / (result['7days']['count'] or 1)
|
||||
result['my_all']['hours'] = round(result['my_all']['hours'] / (result['my_all']['count'] or 1), 2)
|
||||
result['my_high']['hours'] = round(result['my_high']['hours'] / (result['my_high']['count'] or 1), 2)
|
||||
result['my_urgent']['hours'] = round(result['my_urgent']['hours'] / (result['my_urgent']['count'] or 1), 2)
|
||||
|
||||
if self.env['odex25_helpdesk.team'].search([('use_rating', '=', True), '|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)]):
|
||||
result['rating_enable'] = True
|
||||
# rating of today
|
||||
domain = [('user_id', '=', self.env.uid)]
|
||||
dt = fields.Date.today()
|
||||
tickets = self.env['odex25_helpdesk.ticket'].search(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)])
|
||||
activity = tickets.rating_get_grades()
|
||||
total_rating = self._compute_activity_avg(activity)
|
||||
total_activity_values = sum(activity.values())
|
||||
team_satisfaction = round((total_rating / total_activity_values if total_activity_values else 0), 2) * 5
|
||||
if team_satisfaction:
|
||||
result['today']['rating'] = team_satisfaction
|
||||
|
||||
# rating of last 7 days (6 days + today)
|
||||
dt = fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6)))
|
||||
tickets = self.env['odex25_helpdesk.ticket'].search(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)])
|
||||
activity = tickets.rating_get_grades()
|
||||
total_rating = self._compute_activity_avg(activity)
|
||||
total_activity_values = sum(activity.values())
|
||||
team_satisfaction_7days = round((total_rating / total_activity_values if total_activity_values else 0), 2) * 5
|
||||
if team_satisfaction_7days:
|
||||
result['7days']['rating'] = team_satisfaction_7days
|
||||
return result
|
||||
|
||||
def _action_view_rating(self, period=False, only_my_closed=False):
|
||||
""" return the action to see all the rating about the tickets of the Team
|
||||
:param period: either 'today' or 'seven_days' to include (or not) the tickets closed in this period
|
||||
:param only_my_closed: True will include only the ticket of the current user in a closed stage
|
||||
"""
|
||||
domain = [('team_id', 'in', self.ids)]
|
||||
|
||||
if period == 'seven_days':
|
||||
domain += [('close_date', '>=', fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6))))]
|
||||
elif period == 'today':
|
||||
domain += [('close_date', '>=', fields.Datetime.to_string(datetime.date.today()))]
|
||||
|
||||
if only_my_closed:
|
||||
domain += [('user_id', '=', self._uid), ('stage_id.is_close', '=', True)]
|
||||
|
||||
ticket_ids = self.env['odex25_helpdesk.ticket'].search(domain).ids
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("rating.rating_rating_view")
|
||||
action['domain'] = [('res_id', 'in', ticket_ids), ('rating', '!=', -1), ('res_model', '=', 'odex25_helpdesk.ticket'), ('consumed', '=', True)]
|
||||
action['help'] = '<p class="o_view_nocontent_empty_folder">No data yet !</p><p>Create tickets to get statistics.</p>'
|
||||
return action
|
||||
|
||||
def action_view_ticket(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("odex25_helpdesk.odex25_helpdesk_ticket_action_team")
|
||||
action['display_name'] = self.name
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def action_view_rating_today(self):
|
||||
# call this method of on click "Customer Rating" button on dashbord for today rating of teams tickets
|
||||
return self.search(['|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)])._action_view_rating(period='today', only_my_closed=True)
|
||||
|
||||
@api.model
|
||||
def action_view_rating_7days(self):
|
||||
# call this method of on click "Customer Rating" button on dashbord for last 7days rating of teams tickets
|
||||
return self.search(['|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)])._action_view_rating(period='seven_days', only_my_closed=True)
|
||||
|
||||
def action_view_all_rating(self):
|
||||
""" return the action to see all the rating about the all sort of activity of the team (tickets) """
|
||||
return self._action_view_rating()
|
||||
|
||||
@api.model
|
||||
def _compute_activity_avg(self, activity):
|
||||
# compute average base on all rating value
|
||||
# like: 5 great, 3 okey, 1 bad
|
||||
# great = 5, okey = 3, bad = 0
|
||||
# (5*5) + (2*3) + (1*0) = 60 / 8 (nuber of activity for rating)
|
||||
great = activity['great'] * 5.00
|
||||
okey = activity['okay'] * 3.00
|
||||
bad = activity['bad'] * 0.00
|
||||
return great + okey + bad
|
||||
|
||||
def _determine_user_to_assign(self):
|
||||
""" Get a dict with the user (per team) that should be assign to the nearly created ticket according to the team policy
|
||||
:returns a mapping of team identifier with the "to assign" user (maybe an empty record).
|
||||
:rtype : dict (key=team_id, value=record of res.users)
|
||||
"""
|
||||
result = dict.fromkeys(self.ids, self.env['res.users'])
|
||||
for team in self:
|
||||
member_ids = sorted(team.member_ids.ids) if team.member_ids else sorted(team.visibility_member_ids.ids)
|
||||
if member_ids:
|
||||
if team.assign_method == 'randomly': # randomly means new tickets get uniformly distributed
|
||||
last_assigned_user = self.env['odex25_helpdesk.ticket'].search([('team_id', '=', team.id)], order='create_date desc, id desc', limit=1).user_id
|
||||
index = 0
|
||||
if last_assigned_user and last_assigned_user.id in member_ids:
|
||||
previous_index = member_ids.index(last_assigned_user.id)
|
||||
index = (previous_index + 1) % len(member_ids)
|
||||
result[team.id] = self.env['res.users'].browse(member_ids[index])
|
||||
elif team.assign_method == 'balanced': # find the member with the least open ticket
|
||||
ticket_count_data = self.env['odex25_helpdesk.ticket'].read_group([('stage_id.is_close', '=', False), ('user_id', 'in', member_ids), ('team_id', '=', team.id)], ['user_id'], ['user_id'])
|
||||
open_ticket_per_user_map = dict.fromkeys(member_ids, 0) # dict: user_id -> open ticket count
|
||||
open_ticket_per_user_map.update((item['user_id'][0], item['user_id_count']) for item in ticket_count_data)
|
||||
result[team.id] = self.env['res.users'].browse(min(open_ticket_per_user_map, key=open_ticket_per_user_map.get))
|
||||
return result
|
||||
|
||||
def _determine_stage(self):
|
||||
""" Get a dict with the stage (per team) that should be set as first to a created ticket
|
||||
:returns a mapping of team identifier with the stage (maybe an empty record).
|
||||
:rtype : dict (key=team_id, value=record of odex25_helpdesk.stage)
|
||||
"""
|
||||
result = dict.fromkeys(self.ids, self.env['odex25_helpdesk.stage'])
|
||||
for team in self:
|
||||
result[team.id] = self.env['odex25_helpdesk.stage'].search([('team_ids', 'in', team.id)], order='sequence', limit=1)
|
||||
return result
|
||||
|
||||
def _get_closing_stage(self):
|
||||
"""
|
||||
Return the first closing kanban stage or the last stage of the pipe if none
|
||||
"""
|
||||
closed_stage = self.stage_ids.filtered(lambda stage: stage.is_close)
|
||||
if not closed_stage:
|
||||
closed_stage = self.stage_ids[-1]
|
||||
return closed_stage
|
||||
|
||||
|
||||
|
||||
class odex25_helpdeskStage(models.Model):
|
||||
_name = 'odex25_helpdesk.stage'
|
||||
_description = 'Helpdesk Stage'
|
||||
_order = 'sequence, id'
|
||||
|
||||
def _default_team_ids(self):
|
||||
team_id = self.env.context.get('default_team_id')
|
||||
if team_id:
|
||||
return [(4, team_id, 0)]
|
||||
|
||||
name = fields.Char('Stage Name', required=True, translate=True)
|
||||
description = fields.Text(translate=True)
|
||||
sequence = fields.Integer('Sequence', default=10)
|
||||
is_close = fields.Boolean(
|
||||
'Closing Stage',
|
||||
help='Tickets in this stage are considered as done. This is used notably when '
|
||||
'computing SLAs and KPIs on tickets.')
|
||||
fold = fields.Boolean(
|
||||
'Folded in Kanban',
|
||||
help='This stage is folded in the kanban view when there are no records in that stage to display.')
|
||||
team_ids = fields.Many2many(
|
||||
'odex25_helpdesk.team', relation='team_stage_rel', string='Team',
|
||||
default=_default_team_ids,
|
||||
help='Specific team that uses this stage. Other teams will not be able to see or use this stage.')
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Email Template',
|
||||
domain="[('model', '=', 'odex25_helpdesk.ticket')]",
|
||||
help="Automated email sent to the ticket's customer when the ticket reaches this stage.")
|
||||
legend_blocked = fields.Char(
|
||||
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True,
|
||||
help='Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage.')
|
||||
legend_done = fields.Char(
|
||||
'Green Kanban Label', default=lambda s: _('Ready'), translate=True, required=True,
|
||||
help='Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage.')
|
||||
legend_normal = fields.Char(
|
||||
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True,
|
||||
help='Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage.')
|
||||
|
||||
def unlink(self):
|
||||
stages = self
|
||||
default_team_id = self.env.context.get('default_team_id')
|
||||
if default_team_id:
|
||||
shared_stages = self.filtered(lambda x: len(x.team_ids) > 1 and default_team_id in x.team_ids.ids)
|
||||
tickets = self.env['odex25_helpdesk.ticket'].with_context(active_test=False).search([('team_id', '=', default_team_id), ('stage_id', 'in', self.ids)])
|
||||
if shared_stages and not tickets:
|
||||
shared_stages.write({'team_ids': [(3, default_team_id)]})
|
||||
stages = self.filtered(lambda x: x not in shared_stages)
|
||||
return super(odex25_helpdeskStage, stages).unlink()
|
||||
|
||||
|
||||
class odex25_helpdeskSLA(models.Model):
|
||||
_name = "odex25_helpdesk.sla"
|
||||
_order = "name"
|
||||
_description = "Helpdesk SLA Policies"
|
||||
|
||||
name = fields.Char('SLA Policy Name', required=True, index=True)
|
||||
description = fields.Text('SLA Policy Description')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
team_id = fields.Many2one('odex25_helpdesk.team', 'Team', required=True)
|
||||
target_type = fields.Selection([('stage', 'Reaching Stage'), ('assigning', 'Assigning Ticket')], default='stage', required=True)
|
||||
ticket_type_id = fields.Many2one(
|
||||
'odex25_helpdesk.ticket.type', "Ticket Type",
|
||||
help="Only apply the SLA to a specific ticket type. If left empty it will apply to all types.")
|
||||
tag_ids = fields.Many2many(
|
||||
'odex25_helpdesk.tag', string='Tags',
|
||||
help="Only apply the SLA to tickets with specific tags. If left empty it will apply to all tags.")
|
||||
stage_id = fields.Many2one(
|
||||
'odex25_helpdesk.stage', 'Target Stage',
|
||||
help='Minimum stage a ticket needs to reach in order to satisfy this SLA.')
|
||||
exclude_stage_ids = fields.Many2many(
|
||||
'odex25_helpdesk.stage', string='Exclude Stages',
|
||||
compute='_compute_exclude_stage_ids', store=True, readonly=False, copy=True,
|
||||
domain="[('id', '!=', stage_id.id)]",
|
||||
help='The amount of time the ticket spends in this stage will not be taken into account when evaluating the status of the SLA Policy.')
|
||||
priority = fields.Selection(
|
||||
TICKET_PRIORITY, string='Minimum Priority',
|
||||
default='0', required=True,
|
||||
help='Tickets under this priority will not be taken into account.')
|
||||
company_id = fields.Many2one('res.company', 'Company', related='team_id.company_id', readonly=True, store=True)
|
||||
time_days = fields.Integer(
|
||||
'Days', default=0, required=True,
|
||||
help="Days to reach given stage based on ticket creation date")
|
||||
time_hours = fields.Integer(
|
||||
'Hours', default=0, inverse='_inverse_time_hours', required=True,
|
||||
help="Hours to reach given stage based on ticket creation date")
|
||||
time_minutes = fields.Integer(
|
||||
'Minutes', default=0, inverse='_inverse_time_minutes', required=True,
|
||||
help="Minutes to reach given stage based on ticket creation date")
|
||||
category_id = fields.Many2one('service.category')
|
||||
service_id = fields.Many2one('helpdesk.service')
|
||||
|
||||
@api.depends('target_type')
|
||||
def _compute_exclude_stage_ids(self):
|
||||
self.update({'exclude_stage_ids': False})
|
||||
|
||||
def _inverse_time_hours(self):
|
||||
for sla in self:
|
||||
sla.time_hours = max(0, sla.time_hours)
|
||||
if sla.time_hours >= 24:
|
||||
sla.time_days += sla.time_hours / 24
|
||||
sla.time_hours = sla.time_hours % 24
|
||||
|
||||
def _inverse_time_minutes(self):
|
||||
for sla in self:
|
||||
sla.time_minutes = max(0, sla.time_minutes)
|
||||
if sla.time_minutes >= 60:
|
||||
sla.time_hours += sla.time_minutes / 60
|
||||
sla.time_minutes = sla.time_minutes % 60
|
||||
|
|
@ -0,0 +1,970 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import timedelta
|
||||
from random import randint
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
TICKET_PRIORITY = [
|
||||
('0', 'All'),
|
||||
('1', 'Low priority'),
|
||||
('2', 'High priority'),
|
||||
('3', 'Urgent'),
|
||||
]
|
||||
|
||||
|
||||
class odex25_helpdeskTag(models.Model):
|
||||
_name = 'odex25_helpdesk.tag'
|
||||
_description = 'Helpdesk Tags'
|
||||
_order = 'name'
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char('Tag Name', required=True)
|
||||
color = fields.Integer('Color', default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class odex25_helpdeskTicketType(models.Model):
|
||||
_name = 'odex25_helpdesk.ticket.type'
|
||||
_description = 'Helpdesk Ticket Type'
|
||||
_order = 'sequence'
|
||||
|
||||
name = fields.Char('Type', required=True, translate=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Type name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class odex25_helpdeskSLAStatus(models.Model):
|
||||
_name = 'odex25_helpdesk.sla.status'
|
||||
_description = "Ticket SLA Status"
|
||||
_table = 'odex25_helpdesk_sla_status'
|
||||
_order = 'deadline ASC, sla_stage_id'
|
||||
_rec_name = 'sla_id'
|
||||
|
||||
ticket_id = fields.Many2one('odex25_helpdesk.ticket', string='Ticket', required=True, ondelete='cascade',
|
||||
index=True)
|
||||
sla_id = fields.Many2one('odex25_helpdesk.sla', required=True, ondelete='cascade')
|
||||
sla_stage_id = fields.Many2one('odex25_helpdesk.stage', related='sla_id.stage_id',
|
||||
store=True) # need to be stored for the search in `_sla_reach`
|
||||
target_type = fields.Selection(related='sla_id.target_type', store=True)
|
||||
deadline = fields.Datetime("Deadline", compute='_compute_deadline', compute_sudo=True, store=True)
|
||||
reached_datetime = fields.Datetime("Reached Date",
|
||||
help="Datetime at which the SLA stage was reached for the first time")
|
||||
status = fields.Selection([('failed', 'Failed'), ('reached', 'Reached'), ('ongoing', 'Ongoing')], string="Status",
|
||||
compute='_compute_status', compute_sudo=True, search='_search_status')
|
||||
color = fields.Integer("Color Index", compute='_compute_color')
|
||||
exceeded_days = fields.Float("Excedeed Working Days", compute='_compute_exceeded_days', compute_sudo=True,
|
||||
store=True,
|
||||
help="Working days exceeded for reached SLAs compared with deadline. Positive number means the SLA was eached after the deadline.")
|
||||
|
||||
@api.depends('ticket_id.create_date', 'sla_id', 'ticket_id.stage_id')
|
||||
def _compute_deadline(self):
|
||||
for status in self:
|
||||
if (status.deadline and status.reached_datetime) or (
|
||||
status.deadline and status.target_type == 'stage' and not status.sla_id.exclude_stage_ids) or (
|
||||
status.status == 'failed'):
|
||||
continue
|
||||
if status.target_type == 'assigning' and status.sla_stage_id == status.ticket_id.stage_id:
|
||||
deadline = fields.Datetime.now()
|
||||
elif status.target_type == 'assigning' and status.sla_stage_id:
|
||||
status.deadline = False
|
||||
continue
|
||||
else:
|
||||
deadline = status.ticket_id.create_date
|
||||
working_calendar = status.ticket_id.team_id.resource_calendar_id
|
||||
if not working_calendar:
|
||||
# Normally, having a working_calendar is mandatory
|
||||
status.deadline = deadline
|
||||
continue
|
||||
|
||||
if status.target_type == 'stage' and status.sla_id.exclude_stage_ids:
|
||||
if status.ticket_id.stage_id in status.sla_id.exclude_stage_ids:
|
||||
# We are in the freezed time stage: No deadline
|
||||
status.deadline = False
|
||||
continue
|
||||
|
||||
time_days = status.sla_id.time_days
|
||||
if time_days and (
|
||||
status.sla_id.target_type == 'stage' or status.sla_id.target_type == 'assigning' and not status.sla_id.stage_id):
|
||||
deadline = working_calendar.plan_days(time_days + 1, deadline, compute_leaves=True)
|
||||
# We should also depend on ticket creation time, otherwise for 1 day SLA, all tickets
|
||||
# created on monday will have their deadline filled with tuesday 8:00
|
||||
create_dt = status.ticket_id.create_date
|
||||
deadline = deadline.replace(hour=create_dt.hour, minute=create_dt.minute, second=create_dt.second,
|
||||
microsecond=create_dt.microsecond)
|
||||
elif time_days and status.target_type == 'assigning' and status.sla_stage_id == status.ticket_id.stage_id:
|
||||
deadline = working_calendar.plan_days(time_days + 1, deadline, compute_leaves=True)
|
||||
reached_stage_dt = fields.Datetime.now()
|
||||
deadline = deadline.replace(hour=reached_stage_dt.hour, minute=reached_stage_dt.minute,
|
||||
second=reached_stage_dt.second, microsecond=reached_stage_dt.microsecond)
|
||||
|
||||
sla_hours = status.sla_id.time_hours + (status.sla_id.time_minutes / 60)
|
||||
|
||||
if status.target_type == 'stage' and status.sla_id.exclude_stage_ids:
|
||||
sla_hours += status._get_freezed_hours(working_calendar)
|
||||
|
||||
# Except if ticket creation time is later than the end time of the working day
|
||||
deadline_for_working_cal = working_calendar.plan_hours(0, deadline)
|
||||
if deadline_for_working_cal and deadline.day < deadline_for_working_cal.day:
|
||||
deadline = deadline.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
# We should execute the function plan_hours in any case because, in a 1 day SLA environment,
|
||||
# if I create a ticket knowing that I'm not working the day after at the same time, ticket
|
||||
# deadline will be set at time I don't work (ticket creation time might not be in working calendar).
|
||||
status.deadline = working_calendar.plan_hours(sla_hours, deadline, compute_leaves=True)
|
||||
|
||||
@api.depends('deadline', 'reached_datetime')
|
||||
def _compute_status(self):
|
||||
""" Note: this computed field depending on 'now()' is stored, but refreshed by a cron """
|
||||
for status in self:
|
||||
if status.reached_datetime and status.deadline: # if reached_datetime, SLA is finished: either failed or succeeded
|
||||
status.status = 'reached' if status.reached_datetime < status.deadline else 'failed'
|
||||
else: # if not finished, deadline should be compared to now()
|
||||
status.status = 'ongoing' if not status.deadline or status.deadline > fields.Datetime.now() else 'failed'
|
||||
|
||||
@api.model
|
||||
def _search_status(self, operator, value):
|
||||
""" Supported operators: '=', 'in' and their negative form. """
|
||||
# constants
|
||||
datetime_now = fields.Datetime.now()
|
||||
positive_domain = {
|
||||
'failed': ['|', '&', ('reached_datetime', '=', True), ('deadline', '<=', 'reached_datetime'), '&',
|
||||
('reached_datetime', '=', False), ('deadline', '<=', fields.Datetime.to_string(datetime_now))],
|
||||
'reached': ['&', ('reached_datetime', '=', True), ('reached_datetime', '<', 'deadline')],
|
||||
'ongoing': ['&', ('reached_datetime', '=', False),
|
||||
('deadline', '<=', fields.Datetime.to_string(datetime_now))]
|
||||
}
|
||||
# in/not in case: we treat value as a list of selection item
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
# transform domains
|
||||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||||
# "('status', 'not in', [A, B])" tranformed into "('status', '=', C) OR ('status', '=', D)"
|
||||
domains_to_keep = [dom for key, dom in positive_domain if key not in value]
|
||||
return expression.OR(domains_to_keep)
|
||||
else:
|
||||
return expression.OR(positive_domain[value_item] for value_item in value)
|
||||
|
||||
@api.depends('status')
|
||||
def _compute_color(self):
|
||||
for status in self:
|
||||
if status.status == 'failed':
|
||||
status.color = 1
|
||||
elif status.status == 'reached':
|
||||
status.color = 10
|
||||
else:
|
||||
status.color = 0
|
||||
|
||||
@api.depends('deadline', 'reached_datetime')
|
||||
def _compute_exceeded_days(self):
|
||||
for status in self:
|
||||
if status.reached_datetime and status.deadline and status.ticket_id.team_id.resource_calendar_id:
|
||||
if status.reached_datetime <= status.deadline:
|
||||
start_dt = status.reached_datetime
|
||||
end_dt = status.deadline
|
||||
factor = -1
|
||||
else:
|
||||
start_dt = status.deadline
|
||||
end_dt = status.reached_datetime
|
||||
factor = 1
|
||||
duration_data = status.ticket_id.team_id.resource_calendar_id.get_work_duration_data(start_dt, end_dt,
|
||||
compute_leaves=True)
|
||||
status.exceeded_days = duration_data['days'] * factor
|
||||
else:
|
||||
status.exceeded_days = False
|
||||
|
||||
def _get_freezed_hours(self, working_calendar):
|
||||
self.ensure_one()
|
||||
hours_freezed = 0
|
||||
|
||||
field_stage = self.env['ir.model.fields']._get(self.ticket_id._name, "stage_id")
|
||||
freeze_stages = self.sla_id.exclude_stage_ids.ids
|
||||
tracking_lines = self.ticket_id.message_ids.tracking_value_ids.filtered(
|
||||
lambda tv: tv.field == field_stage).sorted(key="create_date")
|
||||
|
||||
if not tracking_lines:
|
||||
return 0
|
||||
|
||||
old_time = self.ticket_id.create_date
|
||||
for tracking_line in tracking_lines:
|
||||
if tracking_line.old_value_integer in freeze_stages:
|
||||
# We must use get_work_hours_count to compute real waiting hours (as the deadline computation is also based on calendar)
|
||||
hours_freezed += working_calendar.get_work_hours_count(old_time, tracking_line.create_date)
|
||||
old_time = tracking_line.create_date
|
||||
if tracking_lines[-1].new_value_integer in freeze_stages:
|
||||
# the last tracking line is not yet created
|
||||
hours_freezed += working_calendar.get_work_hours_count(old_time, fields.Datetime.now())
|
||||
return hours_freezed
|
||||
|
||||
|
||||
class odex25_helpdeskTicket(models.Model):
|
||||
_name = 'odex25_helpdesk.ticket'
|
||||
_description = 'Helpdesk Ticket'
|
||||
_order = 'priority desc, id desc'
|
||||
_inherit = ['portal.mixin', 'mail.thread.cc', 'utm.mixin', 'rating.mixin', 'mail.activity.mixin']
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(odex25_helpdeskTicket, self).default_get(fields)
|
||||
if result.get('team_id') and fields:
|
||||
team = self.env['odex25_helpdesk.team'].browse(result['team_id'])
|
||||
if 'user_id' in fields and 'user_id' not in result: # if no user given, deduce it from the team
|
||||
result['user_id'] = team._determine_user_to_assign()[team.id].id
|
||||
if 'stage_id' in fields and 'stage_id' not in result: # if no stage given, deduce it from the team
|
||||
result['stage_id'] = team._determine_stage()[team.id].id
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _read_group_stage_ids(self, stages, domain, order):
|
||||
# write the domain
|
||||
# - ('id', 'in', stages.ids): add columns that should be present
|
||||
# - OR ('team_ids', '=', team_id) if team_id: add team columns
|
||||
search_domain = [('id', 'in', stages.ids)]
|
||||
if self.env.context.get('default_team_id'):
|
||||
search_domain = ['|', ('team_ids', 'in', self.env.context['default_team_id'])] + search_domain
|
||||
|
||||
return stages.search(search_domain, order=order)
|
||||
|
||||
name = fields.Char(string='Subject', required=True, index=True)
|
||||
team_id = fields.Many2one('odex25_helpdesk.team', string='Helpdesk Team', index=True)
|
||||
description = fields.Text(required=True, tracking=True)
|
||||
active = fields.Boolean(default=True)
|
||||
ticket_type_id = fields.Many2one('odex25_helpdesk.ticket.type', string="Ticket Type")
|
||||
tag_ids = fields.Many2many('odex25_helpdesk.tag', string='Tags')
|
||||
company_id = fields.Many2one(related='team_id.company_id', string='Company', store=True, readonly=True)
|
||||
color = fields.Integer(string='Color Index')
|
||||
kanban_state = fields.Selection([
|
||||
('normal', 'Grey'),
|
||||
('done', 'Green'),
|
||||
('blocked', 'Red')], string='Kanban State',
|
||||
default='normal', required=True)
|
||||
kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Column Status', tracking=True)
|
||||
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True,
|
||||
related_sudo=False)
|
||||
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True,
|
||||
related_sudo=False)
|
||||
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True,
|
||||
related_sudo=False)
|
||||
domain_user_ids = fields.Many2many('res.users', compute='_compute_domain_user_ids')
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='Assigned to', compute='_compute_user_and_stage_ids', store=True,
|
||||
readonly=False, tracking=True,
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)])
|
||||
partner_id = fields.Many2one('res.partner', string='Customer')
|
||||
partner_ticket_count = fields.Integer('Number of closed tickets from the same partner',
|
||||
compute='_compute_partner_ticket_count')
|
||||
attachment_number = fields.Integer(compute='_compute_attachment_number', string="Number of Attachments")
|
||||
is_self_assigned = fields.Boolean("Am I assigned", compute='_compute_is_self_assigned')
|
||||
# Used to submit tickets from a contact form
|
||||
partner_name = fields.Char(string='Customer Name', compute='_compute_partner_info', store=True, readonly=False)
|
||||
partner_email = fields.Char(string='Customer Email', compute='_compute_partner_info', store=True, readonly=False)
|
||||
closed_by_partner = fields.Boolean('Closed by Partner', readonly=True,
|
||||
help="If checked, this means the ticket was closed through the customer portal by the customer.")
|
||||
# Used in message_get_default_recipients, so if no partner is created, email is sent anyway
|
||||
email = fields.Char(related='partner_email', string='Email on Customer', readonly=False)
|
||||
# priority = fields.Selection(related="service_id.priority", string='Priority', default='0')
|
||||
priority = fields.Selection(TICKET_PRIORITY, string='Priority', default='0')
|
||||
stage_id = fields.Many2one(
|
||||
'odex25_helpdesk.stage', string='Stage', compute='_compute_user_and_stage_ids', store=True,
|
||||
readonly=False, ondelete='restrict', tracking=True, group_expand='_read_group_stage_ids',
|
||||
copy=False, index=True, domain="[('team_ids', '=', team_id)]")
|
||||
date_last_stage_update = fields.Datetime("Last Stage Update", copy=False, readonly=True)
|
||||
# next 4 fields are computed in write (or create)
|
||||
assign_date = fields.Datetime("First assignment date")
|
||||
assign_hours = fields.Integer("Time to first assignment (hours)", compute='_compute_assign_hours', store=True,
|
||||
help="This duration is based on the working calendar of the team")
|
||||
close_date = fields.Datetime("Close date", copy=False)
|
||||
close_hours = fields.Integer("Time to close (hours)", compute='_compute_close_hours', store=True,
|
||||
help="This duration is based on the working calendar of the team")
|
||||
open_hours = fields.Integer("Open Time (hours)", compute='_compute_open_hours', search='_search_open_hours',
|
||||
help="This duration is not based on the working calendar of the team")
|
||||
# SLA relative
|
||||
sla_ids = fields.Many2many('odex25_helpdesk.sla', 'odex25_helpdesk_sla_status', 'ticket_id', 'sla_id',
|
||||
string="SLAs", copy=False)
|
||||
sla_status_ids = fields.One2many('odex25_helpdesk.sla.status', 'ticket_id', string="SLA Status")
|
||||
sla_reached_late = fields.Boolean("Has SLA reached late", compute='_compute_sla_reached_late', compute_sudo=True,
|
||||
store=True)
|
||||
sla_deadline = fields.Datetime("SLA Deadline", compute='_compute_sla_deadline', compute_sudo=True, store=True,
|
||||
help="The closest deadline of all SLA applied on this ticket")
|
||||
sla_fail = fields.Boolean("Failed SLA Policy", compute='_compute_sla_fail', search='_search_sla_fail')
|
||||
sla_success = fields.Boolean("Success SLA Policy", compute='_compute_sla_success', search='_search_sla_success')
|
||||
|
||||
use_credit_notes = fields.Boolean(related='team_id.use_credit_notes', string='Use Credit Notes')
|
||||
use_coupons = fields.Boolean(related='team_id.use_coupons', string='Use Coupons')
|
||||
use_product_returns = fields.Boolean(related='team_id.use_product_returns', string='Use Returns')
|
||||
use_product_repairs = fields.Boolean(related='team_id.use_product_repairs', string='Use Repairs')
|
||||
|
||||
# customer portal: include comment and incoming emails in communication history
|
||||
website_message_ids = fields.One2many(
|
||||
domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
|
||||
|
||||
category_id = fields.Many2one('service.category')
|
||||
service_id = fields.Many2one('helpdesk.service')
|
||||
assistance_user_id = fields.Many2one('res.users', string="Assistance", domain=lambda self: [("groups_id", "=",
|
||||
self.env.ref("odex25_helpdesk.group_odex25_helpdesk_user").id)])
|
||||
# work_email = fields.Char(related='employee_id.work_email', string="Work Email")
|
||||
department_id = fields.Many2one('hr.department', string="Administrative Structure")
|
||||
phone_no = fields.Char(string="Phone Number")
|
||||
on_behalf = fields.Many2one('hr.employee', string="On behalf of")
|
||||
emp_req = fields.Boolean(default=False)
|
||||
schedule_date = fields.Datetime("Scheduled Date")
|
||||
|
||||
@api.onchange('service_id')
|
||||
def _onchange_invoice_date(self):
|
||||
if self.service_id:
|
||||
self.priority = self.service_id.priority
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def onchange_partner_id(self):
|
||||
"""
|
||||
get the partner of the user
|
||||
"""
|
||||
user = self.env['res.users'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
employee_id = self.env['hr.employee'].search([('user_id', '=', user.id)], limit=1)
|
||||
self.department_id = employee_id.department_id
|
||||
|
||||
def activity_update(self):
|
||||
for ticket in self.filtered(lambda request: request.schedule_date):
|
||||
ticket.activity_schedule('odex25_helpdesk.mail_act_odex25_helpdesk_assistance',
|
||||
fields.Datetime.from_string(ticket.schedule_date).date(),
|
||||
note=ticket.name, user_id=ticket.assistance_user_id.id or self.env.uid)
|
||||
|
||||
@api.depends('stage_id', 'kanban_state')
|
||||
def _compute_kanban_state_label(self):
|
||||
for task in self:
|
||||
if task.kanban_state == 'normal':
|
||||
task.kanban_state_label = task.legend_normal
|
||||
elif task.kanban_state == 'blocked':
|
||||
task.kanban_state_label = task.legend_blocked
|
||||
else:
|
||||
task.kanban_state_label = task.legend_done
|
||||
|
||||
@api.depends('team_id')
|
||||
def _compute_domain_user_ids(self):
|
||||
for task in self:
|
||||
if task.team_id and task.team_id.visibility_member_ids:
|
||||
odex25_helpdesk_manager = self.env['res.users'].search(
|
||||
[('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id)])
|
||||
task.domain_user_ids = [(6, 0, (odex25_helpdesk_manager + task.team_id.visibility_member_ids).ids)]
|
||||
else:
|
||||
odex25_helpdesk_users = self.env['res.users'].search(
|
||||
[('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]).ids
|
||||
task.domain_user_ids = [(6, 0, odex25_helpdesk_users)]
|
||||
|
||||
def _compute_access_url(self):
|
||||
super(odex25_helpdeskTicket, self)._compute_access_url()
|
||||
for ticket in self:
|
||||
ticket.access_url = '/my/ticket/%s' % ticket.id
|
||||
|
||||
def _compute_attachment_number(self):
|
||||
read_group_res = self.env['ir.attachment'].read_group(
|
||||
[('res_model', '=', 'odex25_helpdesk.ticket'), ('res_id', 'in', self.ids)],
|
||||
['res_id'], ['res_id'])
|
||||
attach_data = {res['res_id']: res['res_id_count'] for res in read_group_res}
|
||||
for record in self:
|
||||
record.attachment_number = attach_data.get(record.id, 0)
|
||||
|
||||
@api.depends('sla_status_ids.deadline', 'sla_status_ids.reached_datetime')
|
||||
def _compute_sla_reached_late(self):
|
||||
""" Required to do it in SQL since we need to compare 2 columns value """
|
||||
mapping = {}
|
||||
if self.ids:
|
||||
self.env.cr.execute("""
|
||||
SELECT ticket_id, COUNT(id) AS reached_late_count
|
||||
FROM odex25_helpdesk_sla_status
|
||||
WHERE ticket_id IN %s AND deadline < reached_datetime
|
||||
GROUP BY ticket_id
|
||||
""", (tuple(self.ids),))
|
||||
mapping = dict(self.env.cr.fetchall())
|
||||
|
||||
for ticket in self:
|
||||
ticket.sla_reached_late = mapping.get(ticket.id, 0) > 0
|
||||
|
||||
@api.depends('sla_status_ids.deadline', 'sla_status_ids.reached_datetime')
|
||||
def _compute_sla_deadline(self):
|
||||
""" Keep the deadline for the last stage (closed one), so a closed ticket can have a status failed.
|
||||
Note: a ticket in a closed stage will probably have no deadline
|
||||
"""
|
||||
for ticket in self:
|
||||
deadline = False
|
||||
status_not_reached = ticket.sla_status_ids.filtered(
|
||||
lambda status: not status.reached_datetime and status.deadline)
|
||||
ticket.sla_deadline = min(status_not_reached.mapped('deadline')) if status_not_reached else deadline
|
||||
|
||||
@api.depends('sla_deadline', 'sla_reached_late')
|
||||
def _compute_sla_fail(self):
|
||||
now = fields.Datetime.now()
|
||||
for ticket in self:
|
||||
if ticket.sla_deadline:
|
||||
ticket.sla_fail = (ticket.sla_deadline < now) or ticket.sla_reached_late
|
||||
else:
|
||||
ticket.sla_fail = ticket.sla_reached_late
|
||||
|
||||
@api.model
|
||||
def _search_sla_fail(self, operator, value):
|
||||
datetime_now = fields.Datetime.now()
|
||||
if (value and operator in expression.NEGATIVE_TERM_OPERATORS) or (
|
||||
not value and operator not in expression.NEGATIVE_TERM_OPERATORS): # is not failed
|
||||
return ['&', ('sla_reached_late', '=', False), '|', ('sla_deadline', '=', False),
|
||||
('sla_deadline', '>=', datetime_now)]
|
||||
return ['|', ('sla_reached_late', '=', True), ('sla_deadline', '<', datetime_now)] # is failed
|
||||
|
||||
@api.depends('sla_deadline', 'sla_reached_late')
|
||||
def _compute_sla_success(self):
|
||||
now = fields.Datetime.now()
|
||||
for ticket in self:
|
||||
ticket.sla_success = (ticket.sla_deadline and ticket.sla_deadline > now)
|
||||
|
||||
@api.model
|
||||
def _search_sla_success(self, operator, value):
|
||||
datetime_now = fields.Datetime.now()
|
||||
if (value and operator in expression.NEGATIVE_TERM_OPERATORS) or (
|
||||
not value and operator not in expression.NEGATIVE_TERM_OPERATORS): # is failed
|
||||
return [[('sla_status_ids.reached_datetime', '>', datetime_now), ('sla_reached_late', '!=', False)]]
|
||||
return [('sla_status_ids.reached_datetime', '<', datetime_now), ('sla_reached_late', '=', False)] # is success
|
||||
|
||||
@api.depends('user_id')
|
||||
def _compute_is_self_assigned(self):
|
||||
for ticket in self:
|
||||
ticket.is_self_assigned = self.env.user == ticket.user_id
|
||||
|
||||
@api.depends('team_id')
|
||||
def _compute_user_and_stage_ids(self):
|
||||
for ticket in self.filtered(lambda ticket: ticket.team_id):
|
||||
if not ticket.user_id:
|
||||
ticket.user_id = ticket.team_id._determine_user_to_assign()[ticket.team_id.id]
|
||||
if not ticket.stage_id or ticket.stage_id not in ticket.team_id.stage_ids:
|
||||
ticket.stage_id = ticket.team_id._determine_stage()[ticket.team_id.id]
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_info(self):
|
||||
for ticket in self:
|
||||
if ticket.partner_id:
|
||||
ticket.partner_name = ticket.partner_id.name
|
||||
ticket.partner_email = ticket.partner_id.email
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_ticket_count(self):
|
||||
data = self.env['odex25_helpdesk.ticket'].read_group([
|
||||
('partner_id', 'in', self.mapped('partner_id').ids),
|
||||
('stage_id.is_close', '=', False)
|
||||
], ['partner_id'], ['partner_id'], lazy=False)
|
||||
ticket_per_partner_map = dict((item['partner_id'][0], item['__count']) for item in data)
|
||||
for ticket in self:
|
||||
ticket.partner_ticket_count = ticket_per_partner_map.get(ticket.partner_id.id, 0)
|
||||
|
||||
@api.depends('assign_date')
|
||||
def _compute_assign_hours(self):
|
||||
for ticket in self:
|
||||
create_date = fields.Datetime.from_string(ticket.create_date)
|
||||
if create_date and ticket.assign_date and ticket.team_id.resource_calendar_id:
|
||||
duration_data = ticket.team_id.resource_calendar_id.get_work_duration_data(create_date,
|
||||
fields.Datetime.from_string(
|
||||
ticket.assign_date),
|
||||
compute_leaves=True)
|
||||
ticket.assign_hours = duration_data['hours']
|
||||
else:
|
||||
ticket.assign_hours = False
|
||||
|
||||
@api.depends('create_date', 'close_date')
|
||||
def _compute_close_hours(self):
|
||||
for ticket in self:
|
||||
create_date = fields.Datetime.from_string(ticket.create_date)
|
||||
if create_date and ticket.close_date:
|
||||
duration_data = ticket.team_id.resource_calendar_id.get_work_duration_data(create_date,
|
||||
fields.Datetime.from_string(
|
||||
ticket.close_date),
|
||||
compute_leaves=True)
|
||||
ticket.close_hours = duration_data['hours']
|
||||
else:
|
||||
ticket.close_hours = False
|
||||
|
||||
@api.depends('close_hours')
|
||||
def _compute_open_hours(self):
|
||||
for ticket in self:
|
||||
if ticket.create_date: # fix from https://github.com/odoo/enterprise/commit/928fbd1a16e9837190e9c172fa50828fae2a44f7
|
||||
if ticket.close_date:
|
||||
time_difference = ticket.close_date - fields.Datetime.from_string(ticket.create_date)
|
||||
else:
|
||||
time_difference = fields.Datetime.now() - fields.Datetime.from_string(ticket.create_date)
|
||||
ticket.open_hours = (time_difference.seconds) / 3600 + time_difference.days * 24
|
||||
else:
|
||||
ticket.open_hours = 0
|
||||
|
||||
@api.model
|
||||
def _search_open_hours(self, operator, value):
|
||||
dt = fields.Datetime.now() - relativedelta.relativedelta(hours=value)
|
||||
|
||||
d1, d2 = False, False
|
||||
if operator in ['<', '<=', '>', '>=']:
|
||||
d1 = ['&', ('close_date', '=', False), ('create_date', expression.TERM_OPERATORS_NEGATION[operator], dt)]
|
||||
d2 = ['&', ('close_date', '!=', False), ('close_hours', operator, value)]
|
||||
elif operator in ['=', '!=']:
|
||||
subdomain = ['&', ('create_date', '>=', dt.replace(minute=0, second=0, microsecond=0)),
|
||||
('create_date', '<=', dt.replace(minute=59, second=59, microsecond=99))]
|
||||
if operator in expression.NEGATIVE_TERM_OPERATORS:
|
||||
subdomain = expression.distribute_not(subdomain)
|
||||
d1 = expression.AND([[('close_date', '=', False)], subdomain])
|
||||
d2 = ['&', ('close_date', '!=', False), ('close_hours', operator, value)]
|
||||
return expression.OR([d1, d2])
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM overrides
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for ticket in self:
|
||||
result.append((ticket.id, "%s (#%d)" % (ticket.name, ticket._origin.id)))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def create_action(self, action_ref, title, search_view_ref):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(action_ref)
|
||||
if title:
|
||||
action['display_name'] = title
|
||||
if search_view_ref:
|
||||
action['search_view_id'] = self.env.ref(search_view_ref).read()[0]
|
||||
action['views'] = [(False, view) for view in action['view_mode'].split(",")]
|
||||
|
||||
return {'action': action}
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, list_value):
|
||||
now = fields.Datetime.now()
|
||||
# determine user_id and stage_id if not given. Done in batch.
|
||||
teams = self.env['odex25_helpdesk.team'].browse([vals['team_id'] for vals in list_value if vals.get('team_id')])
|
||||
team_default_map = dict.fromkeys(teams.ids, dict())
|
||||
for team in teams:
|
||||
team_default_map[team.id] = {
|
||||
'stage_id': team._determine_stage()[team.id].id,
|
||||
'user_id': team._determine_user_to_assign()[team.id].id
|
||||
}
|
||||
|
||||
# Manually create a partner now since 'generate_recipients' doesn't keep the name. This is
|
||||
# to avoid intrusive changes in the 'mail' module
|
||||
for vals in list_value:
|
||||
partner_id = vals.get('partner_id', False)
|
||||
partner_name = vals.get('partner_name', False)
|
||||
partner_email = vals.get('partner_email', False)
|
||||
if partner_name and partner_email and not partner_id:
|
||||
try:
|
||||
vals['partner_id'] = self.env['res.partner'].find_or_create(
|
||||
tools.formataddr((partner_name, partner_email))
|
||||
).id
|
||||
except UnicodeEncodeError:
|
||||
# 'formataddr' doesn't support non-ascii characters in email. Therefore, we fall
|
||||
# back on a simple partner creation.
|
||||
vals['partner_id'] = self.env['res.partner'].create({
|
||||
'name': partner_name,
|
||||
'email': partner_email,
|
||||
}).id
|
||||
|
||||
# determine partner email for ticket with partner but no email given
|
||||
partners = self.env['res.partner'].browse([vals['partner_id'] for vals in list_value if
|
||||
'partner_id' in vals and vals.get(
|
||||
'partner_id') and 'partner_email' not in vals])
|
||||
partner_email_map = {partner.id: partner.email for partner in partners}
|
||||
partner_name_map = {partner.id: partner.name for partner in partners}
|
||||
|
||||
for vals in list_value:
|
||||
if vals.get('team_id'):
|
||||
team_default = team_default_map[vals['team_id']]
|
||||
if 'stage_id' not in vals:
|
||||
vals['stage_id'] = team_default['stage_id']
|
||||
# Note: this will break the randomly distributed user assignment. Indeed, it will be too difficult to
|
||||
# equally assigned user when creating ticket in batch, as it requires to search after the last assigned
|
||||
# after every ticket creation, which is not very performant. We decided to not cover this user case.
|
||||
if 'user_id' not in vals:
|
||||
vals['user_id'] = team_default['user_id']
|
||||
if vals.get(
|
||||
'user_id'): # if a user is finally assigned, force ticket assign_date and reset assign_hours
|
||||
vals['assign_date'] = fields.Datetime.now()
|
||||
vals['assign_hours'] = 0
|
||||
|
||||
# set partner email if in map of not given
|
||||
if vals.get('partner_id') in partner_email_map:
|
||||
vals['partner_email'] = partner_email_map.get(vals['partner_id'])
|
||||
# set partner name if in map of not given
|
||||
if vals.get('partner_id') in partner_name_map:
|
||||
vals['partner_name'] = partner_name_map.get(vals['partner_id'])
|
||||
|
||||
if vals.get('stage_id'):
|
||||
vals['date_last_stage_update'] = now
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
tickets = super(odex25_helpdeskTicket, self).create(list_value)
|
||||
tickets.activity_update()
|
||||
tickets.emp_req = True
|
||||
|
||||
# make customer follower
|
||||
for ticket in tickets:
|
||||
if ticket.partner_id:
|
||||
ticket.message_subscribe(partner_ids=ticket.partner_id.ids)
|
||||
|
||||
ticket._portal_ensure_token()
|
||||
|
||||
# apply SLA
|
||||
tickets.sudo()._sla_apply()
|
||||
|
||||
return tickets
|
||||
|
||||
def write(self, vals):
|
||||
# we set the assignation date (assign_date) to now for tickets that are being assigned for the first time
|
||||
# same thing for the closing date
|
||||
assigned_tickets = closed_tickets = self.browse()
|
||||
if vals.get('user_id'):
|
||||
assigned_tickets = self.filtered(lambda ticket: not ticket.assign_date)
|
||||
|
||||
if vals.get('stage_id'):
|
||||
if self.env['odex25_helpdesk.stage'].browse(vals.get('stage_id')).is_close:
|
||||
closed_tickets = self.filtered(lambda ticket: not ticket.close_date)
|
||||
else: # auto reset the 'closed_by_partner' flag
|
||||
vals['closed_by_partner'] = False
|
||||
|
||||
now = fields.Datetime.now()
|
||||
|
||||
# update last stage date when changing stage
|
||||
if 'stage_id' in vals:
|
||||
vals['date_last_stage_update'] = now
|
||||
|
||||
res = super(odex25_helpdeskTicket, self - assigned_tickets - closed_tickets).write(vals)
|
||||
res &= super(odex25_helpdeskTicket, assigned_tickets - closed_tickets).write(dict(vals, **{
|
||||
'assign_date': now,
|
||||
}))
|
||||
res &= super(odex25_helpdeskTicket, closed_tickets - assigned_tickets).write(dict(vals, **{
|
||||
'close_date': now,
|
||||
}))
|
||||
res &= super(odex25_helpdeskTicket, assigned_tickets & closed_tickets).write(dict(vals, **{
|
||||
'assign_date': now,
|
||||
'close_date': now,
|
||||
}))
|
||||
|
||||
if vals.get('partner_id'):
|
||||
self.message_subscribe([vals['partner_id']])
|
||||
if vals.get('assistance_user_id'):
|
||||
# need to change description of activity also so unlink old and create new activity
|
||||
self.activity_unlink(['odex25_helpdesk.mail_act_odex25_helpdesk_assistance'])
|
||||
self.activity_update()
|
||||
|
||||
# SLA business
|
||||
sla_triggers = self._sla_reset_trigger()
|
||||
if any(field_name in sla_triggers for field_name in vals.keys()):
|
||||
self.sudo()._sla_apply(keep_reached=True)
|
||||
if 'stage_id' in vals:
|
||||
self.sudo()._sla_reach(vals['stage_id'])
|
||||
|
||||
if 'stage_id' in vals or 'user_id' in vals:
|
||||
self.filtered(lambda ticket: ticket.user_id).sudo()._sla_assigning_reach()
|
||||
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Actions and Business methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _sla_reset_trigger(self):
|
||||
""" Get the list of field for which we have to reset the SLAs (regenerate) """
|
||||
return ['team_id', 'priority', 'service_id', 'tag_ids']
|
||||
|
||||
def _sla_apply(self, keep_reached=False):
|
||||
""" Apply SLA to current tickets: erase the current SLAs, then find and link the new SLAs to each ticket.
|
||||
Note: transferring ticket to a team "not using SLA" (but with SLAs defined), SLA status of the ticket will be
|
||||
erased but nothing will be recreated.
|
||||
:returns recordset of new odex25_helpdesk.sla.status applied on current tickets
|
||||
"""
|
||||
# get SLA to apply
|
||||
sla_per_tickets = self._sla_find()
|
||||
|
||||
# generate values of new sla status
|
||||
sla_status_value_list = []
|
||||
for tickets, slas in sla_per_tickets.items():
|
||||
sla_status_value_list += tickets._sla_generate_status_values(slas, keep_reached=keep_reached)
|
||||
|
||||
sla_status_to_remove = self.mapped('sla_status_ids')
|
||||
if keep_reached: # keep only the reached one to avoid losing reached_date info
|
||||
sla_status_to_remove = sla_status_to_remove.filtered(lambda status: not status.reached_datetime)
|
||||
|
||||
# if we are going to recreate many sla.status, then add norecompute to avoid 2 recomputation (unlink + recreate). Here,
|
||||
# `norecompute` will not trigger recomputation. It will be done on the create multi (if value list is not empty).
|
||||
if sla_status_value_list:
|
||||
sla_status_to_remove.with_context(norecompute=True)
|
||||
|
||||
# unlink status and create the new ones in 2 operations (recomputation optimized)
|
||||
sla_status_to_remove.unlink()
|
||||
return self.env['odex25_helpdesk.sla.status'].create(sla_status_value_list)
|
||||
|
||||
def _sla_find(self):
|
||||
""" Find the SLA to apply on the current tickets
|
||||
:returns a map with the tickets linked to the SLA to apply on them
|
||||
:rtype : dict {<odex25_helpdesk.ticket>: <odex25_helpdesk.sla>}
|
||||
"""
|
||||
tickets_map = {}
|
||||
sla_domain_map = {}
|
||||
|
||||
def _generate_key(ticket):
|
||||
""" Return a tuple identifying the combinaison of field determining the SLA to apply on the ticket """
|
||||
fields_list = self._sla_reset_trigger()
|
||||
key = list()
|
||||
for field_name in fields_list:
|
||||
if ticket._fields[field_name].type == 'many2one':
|
||||
key.append(ticket[field_name].id)
|
||||
else:
|
||||
key.append(ticket[field_name])
|
||||
return tuple(key)
|
||||
|
||||
for ticket in self:
|
||||
if ticket.team_id.use_sla: # limit to the team using SLA
|
||||
key = _generate_key(ticket)
|
||||
# group the ticket per key
|
||||
tickets_map.setdefault(key, self.env['odex25_helpdesk.ticket'])
|
||||
tickets_map[key] |= ticket
|
||||
# group the SLA to apply, by key
|
||||
if key not in sla_domain_map:
|
||||
sla_domain_map[key] = [
|
||||
('team_id', '=', ticket.team_id.id), ('priority', '<=', ticket.priority),
|
||||
'|',
|
||||
'&', ('stage_id.sequence', '>=', ticket.stage_id.sequence), ('target_type', '=', 'stage'),
|
||||
('target_type', '=', 'assigning'),
|
||||
'|', ('service_id', '=', ticket.service_id.id), ('service_id', '=', False)]
|
||||
|
||||
result = {}
|
||||
for key, tickets in tickets_map.items(): # only one search per ticket group
|
||||
domain = sla_domain_map[key]
|
||||
slas = self.env['odex25_helpdesk.sla'].search(domain)
|
||||
result[tickets] = slas.filtered(lambda s: s.tag_ids <= tickets.tag_ids) # SLA to apply on ticket subset
|
||||
return result
|
||||
|
||||
def _sla_generate_status_values(self, slas, keep_reached=False):
|
||||
""" Return the list of values for given SLA to be applied on current ticket """
|
||||
status_to_keep = dict.fromkeys(self.ids, list())
|
||||
|
||||
# generate the map of status to keep by ticket only if requested
|
||||
if keep_reached:
|
||||
for ticket in self:
|
||||
for status in ticket.sla_status_ids:
|
||||
if status.reached_datetime:
|
||||
status_to_keep[ticket.id].append(status.sla_id.id)
|
||||
|
||||
# create the list of value, and maybe exclude the existing ones
|
||||
result = []
|
||||
for ticket in self:
|
||||
for sla in slas:
|
||||
if not (keep_reached and sla.id in status_to_keep[ticket.id]):
|
||||
if sla.target_type == 'stage' and ticket.stage_id == sla.stage_id:
|
||||
# in case of SLA of type stage and on first stage
|
||||
reached_datetime = fields.Datetime.now()
|
||||
elif sla.target_type == 'assigning' and (
|
||||
not sla.stage_id or ticket.stage_id == sla.stage_id) and ticket.user_id:
|
||||
# in case of SLA of type assigning and ticket is already assigned
|
||||
reached_datetime = fields.Datetime.now()
|
||||
else:
|
||||
reached_datetime = False
|
||||
result.append({
|
||||
'ticket_id': ticket.id,
|
||||
'sla_id': sla.id,
|
||||
'reached_datetime': reached_datetime
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def _sla_assigning_reach(self):
|
||||
""" Flag the SLA status of current ticket for the given stage_id as reached, and even the unreached SLA applied
|
||||
on stage having a sequence lower than the given one.
|
||||
"""
|
||||
self.env['odex25_helpdesk.sla.status'].search([
|
||||
('ticket_id', 'in', self.ids),
|
||||
('reached_datetime', '=', False),
|
||||
('deadline', '!=', False),
|
||||
('target_type', '=', 'assigning')
|
||||
]).write({'reached_datetime': fields.Datetime.now()})
|
||||
|
||||
def _sla_reach(self, stage_id):
|
||||
""" Flag the SLA status of current ticket for the given stage_id as reached, and even the unreached SLA applied
|
||||
on stage having a sequence lower than the given one.
|
||||
"""
|
||||
stage = self.env['odex25_helpdesk.stage'].browse(stage_id)
|
||||
stages = self.env['odex25_helpdesk.stage'].search([('sequence', '<=', stage.sequence), (
|
||||
'team_ids', 'in', self.mapped('team_id').ids)]) # take previous stages
|
||||
self.env['odex25_helpdesk.sla.status'].search([
|
||||
('ticket_id', 'in', self.ids),
|
||||
('sla_stage_id', 'in', stages.ids),
|
||||
('reached_datetime', '=', False),
|
||||
('target_type', '=', 'stage')
|
||||
]).write({'reached_datetime': fields.Datetime.now()})
|
||||
|
||||
# For all SLA of type assigning, we compute deadline if they are not succeded (is succeded = has a reach_datetime)
|
||||
# and if they are linked to a specific stage.
|
||||
self.env['odex25_helpdesk.sla.status'].search([
|
||||
('ticket_id', 'in', self.ids),
|
||||
('sla_stage_id', '!=', False),
|
||||
('reached_datetime', '=', False),
|
||||
('target_type', '=', 'assigning')
|
||||
])._compute_deadline()
|
||||
|
||||
def assign_ticket_to_self(self):
|
||||
self.ensure_one()
|
||||
self.user_id = self.env.user
|
||||
|
||||
def open_customer_tickets(self):
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Customer Tickets'),
|
||||
'res_model': 'odex25_helpdesk.ticket',
|
||||
'view_mode': 'kanban,tree,form,pivot,graph',
|
||||
'context': {'search_default_is_open': True, 'search_default_partner_id': self.partner_id.id}
|
||||
}
|
||||
|
||||
def action_get_attachment_tree_view(self):
|
||||
attachment_action = self.env.ref('base.action_attachment')
|
||||
action = attachment_action.read()[0]
|
||||
action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
|
||||
return action
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Messaging API
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# DVE FIXME: if partner gets created when sending the message it should be set as partner_id of the ticket.
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(odex25_helpdeskTicket, self)._message_get_suggested_recipients()
|
||||
try:
|
||||
for ticket in self:
|
||||
if ticket.partner_id and ticket.partner_id.email:
|
||||
ticket._message_add_suggested_recipient(recipients, partner=ticket.partner_id, reason=_('Customer'))
|
||||
elif ticket.partner_email:
|
||||
ticket._message_add_suggested_recipient(recipients, email=ticket.partner_email,
|
||||
reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> just ignore suggested recipients because this implies modifying followers
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def _ticket_email_split(self, msg):
|
||||
email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or ''))
|
||||
# check left-part is not already an alias
|
||||
return [
|
||||
x for x in email_list
|
||||
if x.split('@')[0] not in self.mapped('team_id.alias_name')
|
||||
]
|
||||
|
||||
@api.model
|
||||
def message_new(self, msg, custom_values=None):
|
||||
values = dict(custom_values or {}, partner_email=msg.get('from'), partner_id=msg.get('author_id'))
|
||||
ticket = super(odex25_helpdeskTicket, self.with_context(mail_notify_author=True)).message_new(msg,
|
||||
custom_values=values)
|
||||
partner_ids = [x.id for x in
|
||||
self.env['mail.thread']._mail_find_partner_from_emails(self._ticket_email_split(msg),
|
||||
records=ticket) if x]
|
||||
customer_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(
|
||||
tools.email_split(values['partner_email']), records=ticket) if p]
|
||||
partner_ids += customer_ids
|
||||
if customer_ids and not values.get('partner_id'):
|
||||
ticket.partner_id = customer_ids[0]
|
||||
if partner_ids:
|
||||
ticket.message_subscribe(partner_ids)
|
||||
return ticket
|
||||
|
||||
def message_update(self, msg, update_vals=None):
|
||||
partner_ids = [x.id for x in
|
||||
self.env['mail.thread']._mail_find_partner_from_emails(self._ticket_email_split(msg),
|
||||
records=self) if x]
|
||||
if partner_ids:
|
||||
self.message_subscribe(partner_ids)
|
||||
return super(odex25_helpdeskTicket, self).message_update(msg, update_vals=update_vals)
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.partner_email and self.partner_id and not self.partner_id.email:
|
||||
self.partner_id.email = self.partner_email
|
||||
|
||||
if self.partner_email and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email)
|
||||
if new_partner:
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
('partner_email', '=', new_partner.email),
|
||||
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
|
||||
return super(odex25_helpdeskTicket, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super(odex25_helpdeskTicket, self)._track_template(changes)
|
||||
ticket = self[0]
|
||||
if 'stage_id' in changes and ticket.stage_id.template_id:
|
||||
res['stage_id'] = (ticket.stage_id.template_id, {
|
||||
'auto_delete_message': True,
|
||||
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
|
||||
'email_layout_xmlid': 'mail.mail_notification_light'
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def _creation_subtype(self):
|
||||
return self.env.ref('odex25_helpdesk.mt_ticket_new')
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'stage_id' in init_values:
|
||||
return self.env.ref('odex25_helpdesk.mt_ticket_stage')
|
||||
return super(odex25_helpdeskTicket, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_groups(self, msg_vals=None):
|
||||
""" Handle Helpdesk users and managers recipients that can assign
|
||||
tickets directly from notification emails. Also give access button
|
||||
to portal and portal customers. If they are notified they should
|
||||
probably have access to the document. """
|
||||
groups = super(odex25_helpdeskTicket, self)._notify_get_groups()
|
||||
msg_vals = msg_vals or {}
|
||||
|
||||
self.ensure_one()
|
||||
for group_name, group_method, group_data in groups:
|
||||
if group_name != 'customer':
|
||||
group_data['has_button_access'] = True
|
||||
|
||||
if self.user_id:
|
||||
return groups
|
||||
|
||||
take_action = self._notify_get_action_link('assign', **msg_vals)
|
||||
odex25_helpdesk_actions = [{'url': take_action, 'title': _('Assign to me')}]
|
||||
odex25_helpdesk_user_group_id = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id
|
||||
new_groups = [(
|
||||
'group_odex25_helpdesk_user',
|
||||
lambda pdata: pdata['type'] == 'user' and odex25_helpdesk_user_group_id in pdata['groups'],
|
||||
{'actions': odex25_helpdesk_actions}
|
||||
)]
|
||||
return new_groups + groups
|
||||
|
||||
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
|
||||
""" Override to set alias of tickets to their team if any. """
|
||||
aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company,
|
||||
doc_names=None)
|
||||
res = {ticket.id: aliases.get(ticket.team_id.id) for ticket in self}
|
||||
leftover = self.filtered(lambda rec: not rec.team_id)
|
||||
if leftover:
|
||||
res.update(super(odex25_helpdeskTicket, leftover)._notify_get_reply_to(default=default, records=None,
|
||||
company=company,
|
||||
doc_names=doc_names))
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Rating Mixin
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None):
|
||||
return super(odex25_helpdeskTicket, self).rating_apply(rate, token=token, feedback=feedback,
|
||||
subtype_xmlid="odex25_helpdesk.mt_ticket_rated")
|
||||
|
||||
def _rating_get_parent_field_name(self):
|
||||
return 'team_id'
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
ticket_count = fields.Integer("Tickets", compute='_compute_ticket_count')
|
||||
|
||||
def _compute_ticket_count(self):
|
||||
# retrieve all children partners and prefetch 'parent_id' on them
|
||||
all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
all_partners.read(['parent_id'])
|
||||
|
||||
# group tickets by partner, and account for each partner in self
|
||||
groups = self.env['odex25_helpdesk.ticket'].read_group(
|
||||
[('partner_id', 'in', all_partners.ids)],
|
||||
fields=['partner_id'], groupby=['partner_id'],
|
||||
)
|
||||
self.ticket_count = 0
|
||||
for group in groups:
|
||||
partner = self.browse(group['partner_id'][0])
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.ticket_count += group['partner_id_count']
|
||||
partner = partner.parent_id
|
||||
|
||||
def action_open_odex25_helpdesk_ticket(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree")
|
||||
action['context'] = {}
|
||||
action['domain'] = [('partner_id', 'child_of', self.ids)]
|
||||
return action
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
class odex25_helpdeskSLA(models.Model):
|
||||
_inherit = "odex25_helpdesk.sla"
|
||||
|
||||
category_id = fields.Many2one('service.category')
|
||||
service_id = fields.Many2one('helpdesk.service')
|
||||
|
||||
|
||||
|
||||
class ServiceCategory(models.Model):
|
||||
_name = 'service.category'
|
||||
_description = 'Service Category'
|
||||
|
||||
name = fields.Char('Service Category', required=True)
|
||||
|
||||
@api.constrains('name')
|
||||
def unique_service_category_constrains(self):
|
||||
if self.name:
|
||||
parties_party = self.env['service.category'].search([('name', '=', self.name), ('id', '!=', self.id)])
|
||||
if parties_party:
|
||||
raise ValidationError(_('The Service Category Must Be Unique'))
|
||||
|
||||
|
||||
class HelpdeskService(models.Model):
|
||||
_name = 'helpdesk.service'
|
||||
_description = 'Helpdesk Service'
|
||||
|
||||
name = fields.Char('Service', required=True)
|
||||
category_id = fields.Many2one('service.category')
|
||||
priority = fields.Selection([('0', 'All'), ('1', 'Low priority'), ('2', 'High priority'), ('3', 'Urgent')],
|
||||
string='Priority', default='0')
|
||||
|
||||
@api.constrains('name')
|
||||
def unique_helpdesk_service_constrains(self):
|
||||
if self.name:
|
||||
parties_party = self.env['helpdesk.service'].search([('name', '=', self.name), ('id', '!=', self.id)])
|
||||
if parties_party:
|
||||
raise ValidationError(_('The Helpdesk Service Must Be Unique'))
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
odex25_helpdesk_target_closed = fields.Float(string='Target Tickets to Close', default=1)
|
||||
odex25_helpdesk_target_rating = fields.Float(string='Target Customer Rating', default=100)
|
||||
odex25_helpdesk_target_success = fields.Float(string='Target Success Rate', default=100)
|
||||
|
||||
_sql_constraints = [
|
||||
('target_closed_not_zero', 'CHECK(odex25_helpdesk_target_closed > 0)', 'You cannot have negative targets'),
|
||||
('target_rating_not_zero', 'CHECK(odex25_helpdesk_target_rating > 0)', 'You cannot have negative targets'),
|
||||
('target_success_not_zero', 'CHECK(odex25_helpdesk_target_success > 0)', 'You cannot have negative targets'),
|
||||
]
|
||||
|
||||
def __init__(self, pool, cr):
|
||||
""" Override of __init__ to add access rights.
|
||||
Access rights are disabled by default, but allowed
|
||||
on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
|
||||
"""
|
||||
init_res = super(ResUsers, self).__init__(pool, cr)
|
||||
odex25_helpdesk_fields = [
|
||||
'odex25_helpdesk_target_closed',
|
||||
'odex25_helpdesk_target_rating',
|
||||
'odex25_helpdesk_target_success',
|
||||
]
|
||||
# duplicate list to avoid modifying the original reference
|
||||
type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
|
||||
type(self).SELF_WRITEABLE_FIELDS.extend(odex25_helpdesk_fields)
|
||||
# duplicate list to avoid modifying the original reference
|
||||
type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
|
||||
type(self).SELF_READABLE_FIELDS.extend(odex25_helpdesk_fields)
|
||||
return init_res
|
||||
|
||||
|
||||
class GroupsView(models.Model):
|
||||
_inherit = 'res.groups'
|
||||
|
||||
def get_application_groups(self, domain):
|
||||
group_helpdesk_user = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user', raise_if_not_found=False)
|
||||
group_helpdesk_admin = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager', raise_if_not_found=False)
|
||||
if group_helpdesk_admin and group_helpdesk_user:
|
||||
return super().get_application_groups(
|
||||
domain + [('id', 'not in', (group_helpdesk_user.id, group_helpdesk_admin.id))])
|
||||
return super(GroupsView, self).get_application_groups(domain)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import odex25_helpdesk_sla_report_analysis
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.addons.odex25_helpdesk.models.odex25_helpdesk_ticket import TICKET_PRIORITY
|
||||
|
||||
|
||||
class odex25_helpdeskSLAReport(models.Model):
|
||||
_name = 'odex25_helpdesk.sla.report.analysis'
|
||||
_description = "SLA Status Analysis"
|
||||
_auto = False
|
||||
_order = 'create_date DESC'
|
||||
|
||||
ticket_id = fields.Many2one('odex25_helpdesk.ticket', string='Ticket', readonly=True)
|
||||
create_date = fields.Date("Ticket Create Date", readonly=True)
|
||||
priority = fields.Selection(TICKET_PRIORITY, string='Minimum Priority', readonly=True)
|
||||
user_id = fields.Many2one('res.users', string="Assigned To", readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', string="Customer", readonly=True)
|
||||
ticket_type_id = fields.Many2one('odex25_helpdesk.ticket.type', string="Ticket Type", readonly=True)
|
||||
ticket_stage_id = fields.Many2one('odex25_helpdesk.stage', string="Ticket Stage", readonly=True)
|
||||
ticket_deadline = fields.Datetime("Ticket Deadline", readonly=True)
|
||||
ticket_failed = fields.Boolean("Ticket Failed", group_operator="bool_or", readonly=True)
|
||||
ticket_closed = fields.Boolean("Ticket Closed", readonly=True)
|
||||
ticket_close_hours = fields.Integer("Time to close (hours)", group_operator="avg", readonly=True)
|
||||
ticket_open_hours = fields.Integer("Open Time (hours)", group_operator="avg", readonly=True)
|
||||
ticket_assignation_hours = fields.Integer("Time to first assignment (hours)", group_operator="avg", readonly=True)
|
||||
|
||||
sla_id = fields.Many2one('odex25_helpdesk.sla', string="SLA", readonly=True)
|
||||
sla_stage_id = fields.Many2one('odex25_helpdesk.stage', string="SLA Stage", readonly=True)
|
||||
sla_deadline = fields.Datetime("SLA Deadline", group_operator='min', readonly=True)
|
||||
sla_reached_datetime = fields.Datetime("SLA Reached Date", group_operator='min', readonly=True)
|
||||
sla_status = fields.Selection([('failed', 'Failed'), ('reached', 'Reached'), ('ongoing', 'Ongoing')], string="Status", readonly=True)
|
||||
sla_status_failed = fields.Boolean("SLA Status Failed", group_operator='bool_or', readonly=True)
|
||||
sla_exceeded_days = fields.Integer("Day to reach SLA", group_operator='avg', readonly=True, help="Day to reach the stage of the SLA, without taking the working calendar into account")
|
||||
|
||||
team_id = fields.Many2one('odex25_helpdesk.team', string='Team', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, 'odex25_helpdesk_sla_report_analysis')
|
||||
self.env.cr.execute("""
|
||||
CREATE VIEW odex25_helpdesk_sla_report_analysis AS (
|
||||
SELECT
|
||||
ST.id as id,
|
||||
T.create_date AS create_date,
|
||||
T.id AS ticket_id,
|
||||
T.team_id,
|
||||
T.stage_id AS ticket_stage_id,
|
||||
T.ticket_type_id,
|
||||
T.user_id,
|
||||
T.partner_id,
|
||||
T.company_id,
|
||||
T.priority AS priority,
|
||||
T.sla_reached_late OR T.sla_deadline < NOW() AT TIME ZONE 'UTC' AS ticket_failed,
|
||||
T.sla_deadline AS ticket_deadline,
|
||||
T.close_hours AS ticket_close_hours,
|
||||
EXTRACT(HOUR FROM (COALESCE(T.assign_date, NOW()) - T.create_date)) AS ticket_open_hours,
|
||||
T.assign_hours AS ticket_assignation_hours,
|
||||
STA.is_close AS ticket_closed,
|
||||
ST.sla_id,
|
||||
SLA.stage_id AS sla_stage_id,
|
||||
ST.deadline AS sla_deadline,
|
||||
ST.reached_datetime AS sla_reached_datetime,
|
||||
ST.exceeded_days AS sla_exceeded_days,
|
||||
CASE
|
||||
WHEN ST.reached_datetime IS NOT NULL AND ST.reached_datetime < ST.deadline THEN 'reached'
|
||||
WHEN ST.reached_datetime IS NOT NULL AND ST.reached_datetime >= ST.deadline THEN 'failed'
|
||||
WHEN ST.reached_datetime IS NULL AND ST.deadline > NOW() THEN 'ongoing'
|
||||
ELSE 'failed'
|
||||
END AS sla_status,
|
||||
ST.reached_datetime >= ST.deadline OR (ST.reached_datetime IS NULL AND ST.deadline < NOW() AT TIME ZONE 'UTC') AS sla_status_failed
|
||||
FROM odex25_helpdesk_ticket T
|
||||
LEFT JOIN odex25_helpdesk_stage STA ON (T.stage_id = STA.id)
|
||||
LEFT JOIN odex25_helpdesk_sla_status ST ON (T.id = ST.ticket_id)
|
||||
LEFT JOIN odex25_helpdesk_sla SLA ON (ST.sla_id = SLA.id)
|
||||
)
|
||||
""")
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="odex25_helpdesk_sla_report_analysis_view_pivot" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.sla.report.analysis.pivot</field>
|
||||
<field name="model">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="SLA Status Analysis" disable_linking="True" sample="1">
|
||||
<field name="team_id" type="row"/>
|
||||
<field name="create_date" interval="month" type="col"/>
|
||||
<field name="ticket_failed" type="measure"/>
|
||||
<field name="sla_status_failed" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_report_analysis_view_graph" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.sla.report.analysis.graph</field>
|
||||
<field name="model">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="SLA Status Analysis" sample="1" disable_linking="1">
|
||||
<field name="team_id" type="row"/>
|
||||
<field name="create_date" interval="month" type="col"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_report_analysis_view_search" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.sla.report.analysis.search</field>
|
||||
<field name="model">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="SLA Status Analysis">
|
||||
<field name="create_date"/>
|
||||
<field name="sla_status_failed"/>
|
||||
<field name="ticket_failed"/>
|
||||
<field name="ticket_closed"/>
|
||||
<field name="user_id"/>
|
||||
<filter string="My Ticket" name="my_ticket" domain="[('user_id', '=',uid)]"/>
|
||||
<filter string="Failed Ticket" name="ticket_failed" domain="[('ticket_failed', '=', True)]"/>
|
||||
<filter string="Closed Ticket" name="ticket_closed" domain="[('ticket_closed', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="SLA in Progress" name="sla_inprogress" domain="[('sla_status', '=', 'ongoing')]"/>
|
||||
<filter string="SLA Success" name="sla_success" domain="[('sla_status', '=', 'reached')]"/>
|
||||
<separator/>
|
||||
<filter string="Last 7 days" name="last_7days" domain="[('create_date','>', (context_today() - datetime.timedelta(days=7)).strftime('%%Y-%%m-%%d'))]"/>
|
||||
<filter string="Last 30 days" name="last_month" domain="[('create_date','>', (context_today() - datetime.timedelta(days=30)).strftime('%%Y-%%m-%%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="SLA Status Deadline" name="filter_sla_status_failed" date="sla_status_failed"/>
|
||||
<filter string="Ticket Deadline" name="filter_ticket_deadline" date="ticket_deadline"/>
|
||||
<filter string="Ticket Creation Date" name="filter_create_date" date="create_date"/>
|
||||
<group expand="1" string="Group By">
|
||||
<filter string="Customer" name="partner_id" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="SLA Status Failed" name="sla_status_failed" context="{'group_by':'sla_status_failed'}"/>
|
||||
<filter string="SLA Status Deadline" name="sla_status_deadline" context="{'group_by':'sla_deadline'}"/>
|
||||
<filter string="Ticket Deadline" name="ticket_deadline" context="{'group_by':'ticket_deadline'}"/>
|
||||
<separator/>
|
||||
<filter string="Ticket Creation Date" name="month" context="{'group_by':'create_date:month'}" help="Creation Date"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="odex25_helpdesk_sla_report_analysis_action" model="ir.actions.act_window">
|
||||
<field name="name">SLA Status Analysis</field>
|
||||
<field name="res_model">odex25_helpdesk.sla.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk_sla_report_analysis_view_search"/>
|
||||
<field name="context">{'search_default_last_7days': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet !
|
||||
</p><p>
|
||||
Create tickets to get statistics.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_appraisal_view_report_pivot" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="odex25_helpdesk_sla_report_analysis_view_pivot"/>
|
||||
<field name="act_window_id" ref="odex25_helpdesk_sla_report_analysis_action"/>
|
||||
</record>
|
||||
|
||||
<record id="action_appraisal_view_report_graph" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="odex25_helpdesk_sla_report_analysis_view_graph"/>
|
||||
<field name="act_window_id" ref="odex25_helpdesk_sla_report_analysis_action"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="odex25_helpdesk_ticket_report_menu_sla_analysis"
|
||||
name="SLA Status Analysis"
|
||||
action="odex25_helpdesk_sla_report_analysis_action"
|
||||
sequence="10"
|
||||
parent="odex25_helpdesk_ticket_report_menu_main"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_odex25_helpdesk_tag,odex25_helpdesk.tag,model_odex25_helpdesk_tag,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
|
||||
access_odex25_helpdesk_sla,odex25_helpdesk.sla,model_odex25_helpdesk_sla,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
|
||||
access_odex25_helpdesk_sla_status,odex25_helpdesk.sla.status,model_odex25_helpdesk_sla_status,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
|
||||
access_odex25_helpdesk_sla_manager,odex25_helpdesk.sla.manager,model_odex25_helpdesk_sla,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
access_odex25_helpdesk_stage,odex25_helpdesk.stage,model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
|
||||
access_odex25_helpdesk_stage_manager,odex25_helpdesk.stage.manager,model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
access_odex25_helpdesk_stage_portal,odex25_helpdesk.stage.portal,odex25_helpdesk.model_odex25_helpdesk_stage,base.group_portal,1,0,0,0
|
||||
access_odex25_helpdesk_ticket_portal,odex25_helpdesk.ticket.portal,odex25_helpdesk.model_odex25_helpdesk_ticket,base.group_portal,1,0,0,0
|
||||
access_odex25_helpdesk_ticket,odex25_helpdesk.ticket,model_odex25_helpdesk_ticket,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
|
||||
access_odex25_helpdesk_team_public,odex25_helpdesk.team,model_odex25_helpdesk_team,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
|
||||
access_odex25_helpdesk_team_no_group,odex25_helpdesk.team,model_odex25_helpdesk_team,,1,0,0,0
|
||||
access_odex25_helpdesk_team_portal,odex25_helpdesk.team.portal,odex25_helpdesk.model_odex25_helpdesk_team,base.group_portal,1,0,0,0
|
||||
access_odex25_helpdesk_team_manager,odex25_helpdesk.team.manager,model_odex25_helpdesk_team,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
access_odex25_helpdesk_ticket_type_user,odex25_helpdesk.ticket.type.user,model_odex25_helpdesk_ticket_type,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0
|
||||
access_odex25_helpdesk_ticket_type_manager,odex25_helpdesk.ticket.type.manager,model_odex25_helpdesk_ticket_type,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
access_odex25_helpdesk_sla_report_analysis,access_odex25_helpdesk_sla_report_analysis,model_odex25_helpdesk_sla_report_analysis,odex25_helpdesk.group_odex25_helpdesk_manager,1,0,0,0
|
||||
access_mail_activity_type_odex25_helpdesk_manager,mail.activity.type.odex25_helpdesk.manager,mail.model_mail_activity_type,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
|
||||
access_zfp_helpdesk_service_category,zfp_helpdesk.zfp_helpdesk,odex25_helpdesk.model_service_category,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
|
||||
access_zfp_helpdesk_service,zfp_helpdesk.zfp_helpdesk,odex25_helpdesk.model_helpdesk_service,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_odex25_helpdesk_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_odex25_helpdesk_manager" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_odex25_helpdesk_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="group_odex25_helpdesk_assignment" model="res.groups">
|
||||
<field name="name">Assignment User</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_odex25_helpdesk_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_odex25_helpdesk_on_behalf" model="res.groups">
|
||||
<field name="name">On Behalf User</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_odex25_helpdesk_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_use_sla" model="res.groups">
|
||||
<field name="name">Show SLA Policies</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Manager gets all team access rights -->
|
||||
<record id="odex25_helpdesk_manager_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Administrator</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_team"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_odex25_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- odex25_helpdesk.ticket -->
|
||||
<!-- user only gets to read his own teams (or open teams) -->
|
||||
<record id="odex25_helpdesk_user_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk User</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_team"/>
|
||||
<field name="domain_force">['|', ('visibility_member_ids','in', user.id), ('visibility_member_ids','=',
|
||||
False)]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('group_odex25_helpdesk_user'))]"/>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_ticket_user_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket User</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_ticket"/>
|
||||
<field name="domain_force">['|', ('team_id.visibility_member_ids','in', user.id),
|
||||
('team_id.visibility_member_ids','=', False)]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('group_odex25_helpdesk_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_hide_tickets_managers_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket Managers</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_ticket"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_odex25_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_ticket_project_rule" model="ir.rule">
|
||||
<field name="name">Project: tickets multi-company</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_ticket"/>
|
||||
<field name="domain_force">['|',
|
||||
('company_id', '=', False),
|
||||
('company_id', 'in', company_ids)
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_team_project_rule" model="ir.rule">
|
||||
<field name="name">Project: teams multi-company</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_team"/>
|
||||
<field name="domain_force">['|',
|
||||
('company_id', '=', False),
|
||||
('company_id', 'in', company_ids),
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_sla_company_rule" model="ir.rule">
|
||||
<field name="name">Project: multi-company</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_sla"/>
|
||||
<field name="domain_force">['|',
|
||||
('company_id', '=', False),
|
||||
('company_id', 'in', company_ids),
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk_portal_ticket_rule" model="ir.rule">
|
||||
<field name="name">Tickets: portal users: portal or following</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
('message_partner_ids', 'in', [user.partner_id.id])
|
||||
]
|
||||
</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_report_analysis_rule_manager" model="ir.rule">
|
||||
<field name="name">Helpdesk SLA Report: multi-company</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_sla_report_analysis"/>
|
||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of',
|
||||
[user.company_id.id])]
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<path id="icon-a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/>
|
||||
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="#CD7690"/>
|
||||
<stop offset="100%" stop-color="#CA5377"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<mask id="icon-b" fill="#fff">
|
||||
<use xlink:href="#icon-a"/>
|
||||
</mask>
|
||||
<g mask="url(#icon-b)">
|
||||
<path fill="url(#icon-c)" d="M0 0H70V70H0z"/>
|
||||
<path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/>
|
||||
<path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.13l15.82-16.026C29.154 11.438 42.333 9 51 17c8.667 8 9.643 19.544 2.976 33.544L36.248 69H4z" opacity=".324"/>
|
||||
<path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/>
|
||||
<path fill="#000" d="M35.5 12c3.318 0 6.49.647 9.516 1.941 3.026 1.295 5.632 3.036 7.82 5.223 2.187 2.188 3.928 4.794 5.223 7.82A23.95 23.95 0 0 1 60 36.5c0 3.318-.647 6.49-1.941 9.516-1.295 3.026-3.036 5.632-5.223 7.82-2.188 2.187-4.794 3.928-7.82 5.223A23.95 23.95 0 0 1 35.5 61a23.95 23.95 0 0 1-9.516-1.941c-3.026-1.295-5.632-3.036-7.82-5.223-2.187-2.188-3.928-4.794-5.223-7.82A23.95 23.95 0 0 1 11 36.5c0-3.318.647-6.49 1.941-9.516 1.295-3.026 3.036-5.632 5.223-7.82 2.188-2.187 4.794-3.928 7.82-5.223A23.95 23.95 0 0 1 35.5 12zm.5 3c-3.86 0-7.526.95-11 2.852L30.911 24A15.103 15.103 0 0 1 36 23.113c1.727 0 3.423.295 5.089.887L47 17.852C43.526 15.951 39.86 15 36 15zM16.852 47L23 41.357a13.83 13.83 0 0 1-.887-4.857c0-1.648.295-3.267.887-4.857L16.852 26C14.951 29.316 14 32.816 14 36.5s.95 7.184 2.852 10.5zM36 58c3.86 0 7.526-.95 11-2.852L41.089 49a15.103 15.103 0 0 1-5.089.887c-1.727 0-3.423-.295-5.089-.887L25 55.148C28.474 57.049 32.14 58 36 58zm0-11c3.036 0 5.629-1.074 7.777-3.223C45.926 41.63 47 39.037 47 36c0-3.036-1.074-5.629-3.223-7.777C41.63 26.074 39.037 25 36 25c-3.036 0-5.629 1.074-7.777 3.223C26.074 30.37 25 32.963 25 36c0 3.036 1.074 5.629 3.223 7.777C30.37 45.926 32.963 47 36 47zm13-5.643L55.148 47C57.049 43.684 58 40.184 58 36.5s-.95-7.184-2.852-10.5L49 31.643c.592 1.59.887 3.209.887 4.857A13.83 13.83 0 0 1 49 41.357z" opacity=".3"/>
|
||||
<path fill="#FFF" d="M35.5 10c3.318 0 6.49.647 9.516 1.941 3.026 1.295 5.632 3.036 7.82 5.223 2.187 2.188 3.928 4.794 5.223 7.82A23.95 23.95 0 0 1 60 34.5c0 3.318-.647 6.49-1.941 9.516-1.295 3.026-3.036 5.632-5.223 7.82-2.188 2.187-4.794 3.928-7.82 5.223A23.95 23.95 0 0 1 35.5 59a23.95 23.95 0 0 1-9.516-1.941c-3.026-1.295-5.632-3.036-7.82-5.223-2.187-2.188-3.928-4.794-5.223-7.82A23.95 23.95 0 0 1 11 34.5c0-3.318.647-6.49 1.941-9.516 1.295-3.026 3.036-5.632 5.223-7.82 2.188-2.187 4.794-3.928 7.82-5.223A23.95 23.95 0 0 1 35.5 10zm.5 3c-3.86 0-7.526.95-11 2.852L30.911 22A15.103 15.103 0 0 1 36 21.113c1.727 0 3.423.295 5.089.887L47 15.852C43.526 13.951 39.86 13 36 13zM16.852 45L23 39.357a13.83 13.83 0 0 1-.887-4.857c0-1.648.295-3.267.887-4.857L16.852 24C14.951 27.316 14 30.816 14 34.5s.95 7.184 2.852 10.5zM36 56c3.86 0 7.526-.95 11-2.852L41.089 47a15.103 15.103 0 0 1-5.089.887c-1.727 0-3.423-.295-5.089-.887L25 53.148C28.474 55.049 32.14 56 36 56zm0-10c3.036 0 5.629-1.074 7.777-3.223C45.926 40.63 47 38.037 47 35c0-3.036-1.074-5.629-3.223-7.777C41.63 25.074 39.037 24 36 24c-3.036 0-5.629 1.074-7.777 3.223C26.074 29.37 25 31.963 25 35c0 3.036 1.074 5.629 3.223 7.777C30.37 44.926 32.963 46 36 46zm13-6.643L55.148 45C57.049 41.684 58 38.184 58 34.5s-.95-7.184-2.852-10.5L49 29.643c.592 1.59.887 3.209.887 4.857A13.83 13.83 0 0 1 49 39.357z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
.oe_website_rating_team .oe_rate{
|
||||
clear: both;
|
||||
}
|
||||
.oe_website_rating_team .oe_rate img {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.oe_website_rating_team .thumbnail{
|
||||
height: 355px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,264 @@
|
|||
odoo.define('odex25_helpdesk.dashboard', function (require) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This file defines the Helpdesk Dashboard view (alongside its renderer, model
|
||||
* and controller), extending the Kanban view.
|
||||
* The Helpdesk Dashboard view is registered to the view registry.
|
||||
* A large part of this code should be extracted in an AbstractDashboard
|
||||
* widget in web, to avoid code duplication (see SalesTeamDashboard).
|
||||
*/
|
||||
|
||||
var core = require('web.core');
|
||||
var KanbanController = require('web.KanbanController');
|
||||
var KanbanModel = require('web.KanbanModel');
|
||||
var KanbanRenderer = require('web.KanbanRenderer');
|
||||
var KanbanView = require('web.KanbanView');
|
||||
var session = require('web.session');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
var odex25_helpdeskDashboardRenderer = KanbanRenderer.extend({
|
||||
events: _.extend({}, KanbanRenderer.prototype.events, {
|
||||
'click .o_dashboard_action': '_onDashboardActionClicked',
|
||||
'click .o_target_to_set': '_onDashboardTargetClicked',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Notifies the controller that the target has changed.
|
||||
*
|
||||
* @private
|
||||
* @param {string} target_name the name of the changed target
|
||||
* @param {string} value the new value
|
||||
*/
|
||||
_notifyTargetChange: function (target_name, value) {
|
||||
this.trigger_up('dashboard_edit_target', {
|
||||
target_name: target_name,
|
||||
target_value: value,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_render: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
var values = self.state.dashboardValues;
|
||||
var odex25_helpdesk_dashboard = QWeb.render('odex25_helpdesk.odex25_helpdeskDashboard', {
|
||||
widget: self,
|
||||
show_demo: values.show_demo,
|
||||
rating_enable: values.rating_enable,
|
||||
success_rate_enable: values.success_rate_enable,
|
||||
values: values,
|
||||
});
|
||||
self.$el.prepend(odex25_helpdesk_dashboard);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent}
|
||||
*/
|
||||
_onDashboardActionClicked: function (e) {
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var $action = $(e.currentTarget);
|
||||
var action_ref = $action.attr('name');
|
||||
var title = $action.attr('title');
|
||||
var search_view_ref = $action.attr('search_view_ref');
|
||||
if ($action.attr('show_demo') != 'true'){
|
||||
if ($action.attr('name').includes("odex25_helpdesk.")) {
|
||||
this._rpc({
|
||||
model: 'odex25_helpdesk.ticket',
|
||||
method: 'create_action',
|
||||
args: [action_ref, title, search_view_ref],
|
||||
}).then(function (result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action, {
|
||||
additional_context: $action.attr('context')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.trigger_up('dashboard_open_action', {
|
||||
action_name: $action.attr('name'),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent}
|
||||
*/
|
||||
_onDashboardTargetClicked: function (e) {
|
||||
var self = this;
|
||||
var $target = $(e.currentTarget);
|
||||
var target_name = $target.attr('name');
|
||||
var target_value = $target.attr('value');
|
||||
|
||||
var $input = $('<input/>', {type: "text", name: target_name});
|
||||
if (target_value) {
|
||||
$input.attr('value', target_value);
|
||||
}
|
||||
$input.on('keyup input', function (e) {
|
||||
if (e.which === $.ui.keyCode.ENTER) {
|
||||
self._notifyTargetChange(target_name, $input.val());
|
||||
}
|
||||
});
|
||||
$input.on('blur', function () {
|
||||
self._notifyTargetChange(target_name, $input.val());
|
||||
});
|
||||
$input.replaceAll($target)
|
||||
.focus()
|
||||
.select();
|
||||
},
|
||||
});
|
||||
|
||||
var odex25_helpdeskDashboardModel = KanbanModel.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this.dashboardValues = {};
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
__get: function (localID) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (_.isObject(result)) {
|
||||
result.dashboardValues = this.dashboardValues[localID];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* @œverride
|
||||
* @returns {Promise}
|
||||
*/
|
||||
__load: function () {
|
||||
return this._loadDashboard(this._super.apply(this, arguments));
|
||||
},
|
||||
/**
|
||||
* @œverride
|
||||
* @returns {Promise}
|
||||
*/
|
||||
__reload: function () {
|
||||
return this._loadDashboard(this._super.apply(this, arguments));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Promise} super_def a promise that resolves with a dataPoint id
|
||||
* @returns {Promise -> string} resolves to the dataPoint id
|
||||
*/
|
||||
_loadDashboard: function (super_def) {
|
||||
var self = this;
|
||||
var dashboard_def = this._rpc({
|
||||
model: 'odex25_helpdesk.team',
|
||||
method: 'retrieve_dashboard',
|
||||
});
|
||||
return Promise.all([super_def, dashboard_def]).then(function(results) {
|
||||
var id = results[0];
|
||||
var dashboardValues = results[1];
|
||||
self.dashboardValues[id] = dashboardValues;
|
||||
return id;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var odex25_helpdeskDashboardController = KanbanController.extend({
|
||||
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
|
||||
dashboard_open_action: '_onDashboardOpenAction',
|
||||
dashboard_edit_target: '_onDashboardEditTarget',
|
||||
}),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} e
|
||||
*/
|
||||
_onDashboardEditTarget: function (e) {
|
||||
var target_name = e.data.target_name;
|
||||
var target_value = e.data.target_value;
|
||||
if (isNaN(target_value)) {
|
||||
this.do_warn(false, _t("Please enter an integer value"));
|
||||
} else {
|
||||
var values = {};
|
||||
values[target_name] = parseInt(target_value);
|
||||
this._rpc({
|
||||
model: 'res.users',
|
||||
method: 'write',
|
||||
args: [[session.uid], values],
|
||||
})
|
||||
.then(this.reload.bind(this));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {OdooEvent} e
|
||||
*/
|
||||
_onDashboardOpenAction: function (e) {
|
||||
var self = this;
|
||||
var action_name = e.data.action_name;
|
||||
if (_.contains(['action_view_rating_today', 'action_view_rating_7days'], action_name)) {
|
||||
return this._rpc({model: this.modelName, method: action_name})
|
||||
.then(function (data) {
|
||||
if (data) {
|
||||
return self.do_action(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.do_action(action_name);
|
||||
},
|
||||
});
|
||||
|
||||
var odex25_helpdeskDashboardView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Model: odex25_helpdeskDashboardModel,
|
||||
Renderer: odex25_helpdeskDashboardRenderer,
|
||||
Controller: odex25_helpdeskDashboardController,
|
||||
}),
|
||||
display_name: _lt('Dashboard'),
|
||||
icon: 'fa-dashboard',
|
||||
searchview_hidden: true,
|
||||
});
|
||||
|
||||
view_registry.add('odex25_helpdesk_dashboard', odex25_helpdeskDashboardView);
|
||||
|
||||
return {
|
||||
Model: odex25_helpdeskDashboardModel,
|
||||
Renderer: odex25_helpdeskDashboardRenderer,
|
||||
Controller: odex25_helpdeskDashboardController,
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
odoo.define('odex25_helpdesk.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('odex25_helpdesk_tour', {
|
||||
url: "/web",
|
||||
}, [{
|
||||
trigger: '.o_app[data-menu-xmlid="odex25_helpdesk.menu_odex25_helpdesk_root"]',
|
||||
content: _t('Want to <b>boost your customer satisfaction</b>?<br/><i>Click Helpdesk to start.</i>'),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.oe_kanban_action_button',
|
||||
extra_trigger: '.o_kanban_primary_left',
|
||||
content: _t('Click here to view this team\'s tickets.'),
|
||||
position: 'bottom',
|
||||
width: 200,
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_kanban_odex25_helpdesk_ticket',
|
||||
content: _t('Let\'s create your first ticket.'),
|
||||
position: 'bottom',
|
||||
width: 200,
|
||||
}, {
|
||||
trigger: 'input.field_name',
|
||||
extra_trigger: '.o_form_editable',
|
||||
content: _t('Enter a subject or title for this ticket.<br/><i>(e.g. Problem with installation, Wrong order, Can\'t understand bill, etc.)</i>'),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_field_widget.field_partner_id',
|
||||
extra_trigger: '.o_form_editable',
|
||||
content: _t('Enter the customer. Feel free to create it on the fly.'),
|
||||
position: 'top',
|
||||
}, {
|
||||
trigger: '.o_field_widget.field_user_id',
|
||||
extra_trigger: '.o_form_editable',
|
||||
content: _t('Assign the ticket to someone.'),
|
||||
position: 'right',
|
||||
}, {
|
||||
trigger: '.o_form_button_save',
|
||||
content: _t('Save this ticket and the modifications you\'ve made to it.'),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_back_button',
|
||||
extra_trigger: '.o_form_view.o_form_readonly',
|
||||
content: _t('Use the breadcrumbs to go back to the Kanban view.'),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_kanban_record',
|
||||
content: _t('Click these cards to open their form view, or <b>drag & drop</b> them through the different stages of this team.'),
|
||||
position: 'right',
|
||||
run: "drag_and_drop .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
trigger: '.o_priority',
|
||||
extra_trigger: '.o_kanban_record',
|
||||
content: _t('<b>Stars</b> mark the <b>ticket priority</b>. You can change it directly from here!'),
|
||||
position: 'bottom',
|
||||
run: "drag_and_drop .o_kanban_group:eq(2) ",
|
||||
}, {
|
||||
trigger: ".o_column_quick_create .o_quick_create_folded",
|
||||
content: _t('Add columns to configure <b>stages for your tickets</b>.<br/><i>e.g. Awaiting Customer Feedback, Customer Followup, ...</i>'),
|
||||
position: 'right',
|
||||
}
|
||||
// TODO: Restore this step
|
||||
// , {
|
||||
// trigger: '.dropdown-toggle[data-menu-xmlid="odex25_helpdesk.odex25_helpdesk_menu_config"]',
|
||||
// content: _t('Click here and select "Helpdesk Teams" for further configuration.'),
|
||||
// position: 'bottom',
|
||||
// }
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
.o_kanban_view.o_kanban_dashboard.o_odex25_helpdesk_kanban {
|
||||
$sale-table-spacing: 10px;
|
||||
$odex25_helpdesk-record-width: 420px;
|
||||
|
||||
.o_kanban_group {
|
||||
width: $odex25_helpdesk-record-width + 2*$o-kanban-group-padding;
|
||||
}
|
||||
.o_kanban_record {
|
||||
width: $odex25_helpdesk-record-width;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.o_odex25_helpdesk_dashboard {
|
||||
@include o-position-sticky($top: 0px)
|
||||
border-bottom: 1px solid #CED4DA;
|
||||
background-color: $o-view-background-color;
|
||||
z-index: 10;
|
||||
|
||||
.o_welcome_message {
|
||||
width: 100%;
|
||||
@include o-position-absolute($left: 0, $top: 0);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.o_welcome_image {
|
||||
padding: 20px;
|
||||
}
|
||||
.o_welcome_content > a {
|
||||
color: white;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.o_dashboard_action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: $o-brand-odoo;
|
||||
padding: 5px;
|
||||
font-size: medium;
|
||||
z-index: unset;
|
||||
height:auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon-top-right {
|
||||
margin-top: -$o-kanban-dashboard-vpadding;
|
||||
|
||||
span {
|
||||
left: 0px;
|
||||
right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
> .o_demo {
|
||||
opacity: 0.07;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
> table {
|
||||
margin-bottom: 0;
|
||||
table-layout: fixed;
|
||||
border-spacing: $sale-table-spacing 0px;
|
||||
border-collapse: separate;
|
||||
> tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
border-top: 1px solid $o-view-background-color;
|
||||
width: 25%;
|
||||
|
||||
height: 33px;
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.o_demo{
|
||||
cursor: default;
|
||||
a {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_main {
|
||||
background-color: $o-brand-primary;
|
||||
&:hover {
|
||||
background-color: darken($o-brand-primary, 10%);
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
&.o_demo{
|
||||
&:hover {
|
||||
background-color: $o-brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_warning {
|
||||
background-color: orange;
|
||||
&:hover {
|
||||
background-color: darken(orange, 10%);
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
&.o_demo{
|
||||
&:hover {
|
||||
background-color: orange;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_secondary {
|
||||
background-color: $o-brand-lightsecondary;
|
||||
&:hover {
|
||||
background-color: darken($o-brand-lightsecondary, 10%);
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
&.o_demo{
|
||||
&:hover {
|
||||
background-color: $o-brand-lightsecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_highlight, .o_highlight {
|
||||
font-size: 20px;
|
||||
}
|
||||
&.o_text {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<!--
|
||||
This template is the ribbon at the top of the Helpdesk dashboard that adds
|
||||
some figures to it. We call this rubbon the "odex25_helpdeskDashboard".
|
||||
-->
|
||||
<t t-name="odex25_helpdesk.odex25_helpdeskDashboard">
|
||||
<div t-if="show_demo or values" class="o_odex25_helpdesk_dashboard py-4 mb-3">
|
||||
<div>
|
||||
<!-- Fake Data for Demo-->
|
||||
<t t-set="demo_class" t-value="' '"/>
|
||||
<t t-if="show_demo" >
|
||||
<div class="ribbon ribbon-top-right">
|
||||
<span class="o_recruitment_purple">Sample</span>
|
||||
</div>
|
||||
<t t-set="demo_class" t-value="' o_demo '"/>
|
||||
|
||||
<t t-set="values['my_all']['count']" t-value="10"/>
|
||||
<t t-set="values['my_high']['count']" t-value="3"/>
|
||||
<t t-set="values['my_urgent']['count']" t-value="2"/>
|
||||
|
||||
<t t-set="values['my_all']['hours']" t-value="30"/>
|
||||
<t t-set="values['my_high']['hours']" t-value="10"/>
|
||||
<t t-set="values['my_urgent']['hours']" t-value="15"/>
|
||||
|
||||
<t t-set="values['my_all']['failed']" t-value="4"/>
|
||||
<t t-set="values['my_high']['failed']" t-value="2"/>
|
||||
<t t-set="values['my_urgent']['failed']" t-value="1"/>
|
||||
|
||||
<t t-set="values['today']['count']" t-value="1"/>
|
||||
<t t-set="values['today']['rating']" t-value="50"/>
|
||||
<t t-set="values['today']['success']" t-value="50"/>
|
||||
|
||||
<t t-set="values['7days']['count']" t-value="15"/>
|
||||
<t t-set="values['7days']['rating']" t-value="70"/>
|
||||
<t t-set="values['7days']['success']" t-value="80"/>
|
||||
|
||||
<t t-set="values['odex25_helpdesk_target_rating']" t-value="80"/>
|
||||
<t t-set="values['odex25_helpdesk_target_success']" t-value="85"/>
|
||||
<t t-set="values['odex25_helpdesk_target_closed']" t-value="85"/>
|
||||
</t>
|
||||
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td class="o_text">
|
||||
<div class="o_highlight">My Tickets</div>
|
||||
</td>
|
||||
<td t-att-class="demo_class + ' o_main o_dashboard_action'" title="My Open Tickets" name="odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<span class="o_highlight"><t t-esc="values['my_all']['count']"/></span><br/>
|
||||
Tickets
|
||||
</a>
|
||||
</td>
|
||||
<td t-att-class="demo_class + ' o_main o_dashboard_action'" title="My High Priority Tickets" name="odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create" context="{'search_default_priority': '2'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<span class="o_highlight"><t t-esc="values['my_high']['count']"/></span><br/>
|
||||
High Priority (<span title="Two stars, with a maximum of three" role="img" aria-label="Two stars, with a maximum of three"><span class="fa fa-star" style="color: gold"/><span class="fa fa-star" style="color: gold"/></span>)
|
||||
</a>
|
||||
</td>
|
||||
<td t-att-class="demo_class + ' o_main o_dashboard_action'" title="My Urgent Tickets" name="odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create" context="{'search_default_priority': '3'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<span class="o_highlight"><t t-esc="values['my_urgent']['count']"/></span><br/>
|
||||
Urgent (<span title="Three stars, maximum score" role="img" aria-label="Three stars, maximum score"><span class="fa fa-star" style="color: gold"/><span class="fa fa-star" style="color: gold"/><span class="fa fa-star" style="color: gold"/></span>)
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_text">Avg Open Hours</td>
|
||||
<td title="My Open Tickets Analysis" t-att-class="demo_class + ' o_main o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#"><t t-esc="values['my_all']['hours']"/></a>
|
||||
</td>
|
||||
<td title="My High Priority Tickets Analysis" t-att-class="demo_class + ' o_main o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard" context="{'search_default_priority': '2'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<t t-esc="values['my_high']['hours']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td title="My Urgent Tickets Analysis" t-att-class="demo_class + ' o_main o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard" context="{'search_default_priority': '3'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<t t-esc="values['my_urgent']['hours']"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_text" t-if="success_rate_enable">SLA Failed</td>
|
||||
<td t-if="success_rate_enable" t-att-class="demo_class + (values['my_all']['failed'] ? 'o_warning' : 'o_main') + ' o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_sla" title="My Failed Tickets" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#" class="o_dashboard_action" name="odex25_helpdesk.odex25_helpdesk_ticket_action_sla" data-extra="overdue" t-attr-show_demo="{{show_demo}}">
|
||||
<t t-esc="values['my_all']['failed']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="success_rate_enable" t-att-class="demo_class + (values['my_all']['failed'] ? 'o_warning' : 'o_main') + ' o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_sla" title="My High Priority Failed Tickets" context="{'search_default_priority': '2'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#" data-extra="overdue">
|
||||
<t t-esc="values['my_high']['failed']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="success_rate_enable" t-att-class="demo_class + (values['my_all']['failed'] ? 'o_warning' : 'o_main') + ' o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_sla" title="My Urgent Failed Tickets" context="{'search_default_priority': '3'}" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#" data-extra="overdue">
|
||||
<t t-esc="values['my_urgent']['failed']"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div><div>
|
||||
<table class="d-none d-md-table table table-sm">
|
||||
<tr>
|
||||
<td class="o_text">
|
||||
<div class="o_highlight">My Performance</div>
|
||||
Today
|
||||
</td>
|
||||
<td title="Tickets Closed Today"
|
||||
t-att-class=" 'o_dashboard_action ' + demo_class + ((values and values['odex25_helpdesk_target_closed'] and values['today']['count'] >=values['odex25_helpdesk_target_closed'])?'o_main':'o_secondary') "
|
||||
name="odex25_helpdesk.odex25_helpdesk_ticket_action_close_analysis"
|
||||
t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<span class="o_highlight">
|
||||
<t t-esc="values['today']['count']"/>
|
||||
</span><br/>
|
||||
Closed Tickets
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="rating_enable"
|
||||
title="Today Happy Rating"
|
||||
t-att-class="'o_dashboard_action ' + demo_class + ((values and values['odex25_helpdesk_target_rating'] and values['odex25_helpdesk_target_rating'] <= values['today']['rating'])?'o_main':'o_secondary')"
|
||||
name="action_view_rating_today"
|
||||
type="object"
|
||||
t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<span class="o_highlight">
|
||||
<t t-esc="values['today']['rating']"/> %
|
||||
</span><br/>
|
||||
Happy Rating
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="success_rate_enable"
|
||||
title="Today's Success Rate"
|
||||
t-att-class="'o_dashboard_action ' + demo_class + ((values and values['odex25_helpdesk_target_success'] and values['odex25_helpdesk_target_success'] <= values['today']['success'])?'o_main':'o_secondary')"
|
||||
name="odex25_helpdesk.odex25_helpdesk_ticket_action_success"
|
||||
t-attr-show_demo="{{show_demo}}">
|
||||
|
||||
<a href="#">
|
||||
<span class="o_highlight">
|
||||
<t t-esc="values['today']['success']"/>%
|
||||
</span><br/>
|
||||
Success Rate
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_text">Avg 7 days</td>
|
||||
<td title="Tickets Closed Avg 7 Days" t-att-class="demo_class + 'o_secondary o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_7days_analysis" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<t t-esc="values['7days']['count']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="rating_enable" title="Avg 7 Days Happy Rating" t-att-class="demo_class + 'o_secondary o_dashboard_action'" name="action_view_rating_7days" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<t t-esc="values['7days']['rating']"/> %
|
||||
</a>
|
||||
</td>
|
||||
<td t-if="success_rate_enable" title="Avg 7 Success Rate" t-att-class="demo_class + 'o_secondary o_dashboard_action'" name="odex25_helpdesk.odex25_helpdesk_ticket_action_7dayssuccess" t-attr-show_demo="{{show_demo}}">
|
||||
<a href="#">
|
||||
<t t-esc="values['7days']['success']"/>%
|
||||
</a>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td class="o_text">Daily Target</td>
|
||||
<td t-att-class="demo_class + 'o_secondary'">
|
||||
<span t-att-class="(show_demo ? '' : 'o_target_to_set')" name='odex25_helpdesk_target_closed' t-att-value="values['odex25_helpdesk_target_closed'] ? values['odex25_helpdesk_target_closed'] : undefined" title="Click to set">
|
||||
<t t-if="values['odex25_helpdesk_target_closed']">
|
||||
<t t-esc="values['odex25_helpdesk_target_closed']"/>
|
||||
</t>
|
||||
<t t-if="!values['odex25_helpdesk_target_closed']">
|
||||
Click to set
|
||||
</t>
|
||||
</span>
|
||||
</td>
|
||||
<td t-att-class="demo_class + 'o_secondary'" t-if="rating_enable">
|
||||
<span t-att-class="(show_demo ? '' : 'o_target_to_set')" name='odex25_helpdesk_target_rating' t-att-value="values['odex25_helpdesk_target_rating'] ? values['odex25_helpdesk_target_rating'] : undefined" title="Click to set">
|
||||
<t t-if="values['odex25_helpdesk_target_rating']">
|
||||
<t t-esc="values['odex25_helpdesk_target_rating']"/>%
|
||||
</t>
|
||||
<t t-if="!values['odex25_helpdesk_target_rating']">
|
||||
Click to set
|
||||
</t>
|
||||
</span>
|
||||
</td>
|
||||
<td t-att-class="demo_class + 'o_secondary'" t-if="success_rate_enable">
|
||||
<span t-att-class="(show_demo ? '' : 'o_target_to_set')" name='odex25_helpdesk_target_success' t-att-value="values['odex25_helpdesk_target_success'] ? values['odex25_helpdesk_target_success'] : undefined" title="Click to set">
|
||||
<t t-if="values['odex25_helpdesk_target_success']">
|
||||
<t t-esc="values['odex25_helpdesk_target_success']"/>%
|
||||
</t>
|
||||
<t t-if="!values['odex25_helpdesk_target_success']">
|
||||
Click to set
|
||||
</t>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
odoo.define('odex25_helpdesk.dashboard_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('Views', {}, function () {
|
||||
|
||||
QUnit.module('Helpdesk Dashboard', {
|
||||
beforeEach: function() {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
foo: {string: "Foo", type: "char"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, foo: "yop"},
|
||||
{id: 2, foo: "blip"},
|
||||
{id: 3, foo: "gnap"},
|
||||
{id: 4, foo: "blip"},
|
||||
]
|
||||
},
|
||||
};
|
||||
this.dashboard_data = {
|
||||
'7days': {count: 0, rating: 0, success: 0},
|
||||
odex25_helpdesk_target_closed: 12,
|
||||
odex25_helpdesk_target_rating: 0,
|
||||
odex25_helpdesk_target_success: 0,
|
||||
my_all: {count: 0, hours: 0, failed: 0},
|
||||
my_high: {count: 0, hours: 0, failed: 0},
|
||||
my_urgent: {count: 0, hours: 0, failed: 0},
|
||||
rating_enable: false,
|
||||
show_demo: false,
|
||||
success_rate_enable: false,
|
||||
today: {count: 0, rating: 0, success: 0},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('dashboard basic rendering', async function(assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var dashboard_data = this.dashboard_data;
|
||||
var kanban = await createView({
|
||||
View: view_registry.get('odex25_helpdesk_dashboard'),
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'retrieve_dashboard') {
|
||||
assert.ok(true, "should call /retrieve_dashboard");
|
||||
return Promise.resolve(dashboard_data);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(kanban, 'div.o_odex25_helpdesk_dashboard',
|
||||
"should render the dashboard");
|
||||
assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), '12',
|
||||
"should have written correct target");
|
||||
assert.hasAttrValue(kanban.$('.o_target_to_set'), 'value', '12',
|
||||
"target's value is 12");
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('edit the target', async function(assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var dashboard_data = this.dashboard_data;
|
||||
dashboard_data.odex25_helpdesk_target_closed = 0;
|
||||
var kanban = await createView({
|
||||
View: view_registry.get('odex25_helpdesk_dashboard'),
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'retrieve_dashboard') {
|
||||
// should be called twice: for the first rendering, and after the target update
|
||||
assert.ok(true, "should call /retrieve_dashboard");
|
||||
return Promise.resolve(dashboard_data);
|
||||
}
|
||||
if (args.model === 'res.users' && args.method === 'write') {
|
||||
assert.ok(true, "should modify odex25_helpdesk_target_closed");
|
||||
dashboard_data.odex25_helpdesk_target_closed = args.args[1]['odex25_helpdesk_target_closed'];
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), "Click to set",
|
||||
"should have correct target");
|
||||
assert.ok(!kanban.$('.o_target_to_set').attr('value'), "should have no target");
|
||||
|
||||
// edit the target
|
||||
await testUtils.dom.click(kanban.$('.o_target_to_set'));
|
||||
await testUtils.fields.editAndTrigger(kanban.$('.o_odex25_helpdesk_dashboard input'),
|
||||
1200, [$.Event('keyup', {which: $.ui.keyCode.ENTER})]); // set the target
|
||||
|
||||
assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), "1200",
|
||||
"should have correct target");
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('dashboard rendering with empty many2one', async function(assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// add an empty many2one
|
||||
this.data.partner.fields.partner_id = {string: "Partner", type: 'many2one', relation: 'partner'};
|
||||
this.data.partner.records[0].partner_id = false;
|
||||
|
||||
var dashboard_data = this.dashboard_data;
|
||||
var kanban = await createView({
|
||||
View: view_registry.get('odex25_helpdesk_dashboard'),
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<field name="partner_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'retrieve_dashboard') {
|
||||
assert.ok(true, "should call /retrieve_dashboard");
|
||||
return Promise.resolve(dashboard_data);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(kanban, 'div.o_odex25_helpdesk_dashboard',
|
||||
"should render the dashboard");
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_odex25_helpdesk_flow
|
||||
from . import test_odex25_helpdesk_sla
|
||||
from . import test_ui
|
||||
from . import test_doc_links
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from contextlib import contextmanager
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo import fields
|
||||
|
||||
|
||||
class odex25_helpdeskCommon(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(odex25_helpdeskCommon, cls).setUpClass()
|
||||
cls.env.user.tz = 'Europe/Brussels'
|
||||
cls.env['resource.calendar'].search([]).write({'tz': 'Europe/Brussels'})
|
||||
|
||||
# we create a Helpdesk user and a manager
|
||||
Users = cls.env['res.users'].with_context(tracking_disable=True)
|
||||
cls.main_company_id = cls.env.ref('base.main_company').id
|
||||
cls.odex25_helpdesk_manager = Users.create({
|
||||
'company_id': cls.main_company_id,
|
||||
'name': 'Helpdesk Manager',
|
||||
'login': 'hm',
|
||||
'email': 'hm@example.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id])],
|
||||
'tz': 'Europe/Brussels',
|
||||
})
|
||||
cls.odex25_helpdesk_user = Users.create({
|
||||
'company_id': cls.main_company_id,
|
||||
'name': 'Helpdesk User',
|
||||
'login': 'hu',
|
||||
'email': 'hu@example.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id])],
|
||||
'tz': 'Europe/Brussels',
|
||||
})
|
||||
# the manager defines a team for our tests (the .sudo() at the end is to avoid potential uid problems)
|
||||
cls.test_team = cls.env['odex25_helpdesk.team'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Test Team'}).sudo()
|
||||
# He then defines its stages
|
||||
stage_as_manager = cls.env['odex25_helpdesk.stage'].with_user(cls.odex25_helpdesk_manager)
|
||||
cls.stage_new = stage_as_manager.create({
|
||||
'name': 'New',
|
||||
'sequence': 10,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.stage_progress = stage_as_manager.create({
|
||||
'name': 'In Progress',
|
||||
'sequence': 20,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.stage_done = stage_as_manager.create({
|
||||
'name': 'Done',
|
||||
'sequence': 30,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
cls.stage_cancel = stage_as_manager.create({
|
||||
'name': 'Cancelled',
|
||||
'sequence': 40,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
|
||||
# He also creates a ticket types for Question and Issue
|
||||
cls.type_question = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({
|
||||
'name': 'Question_test',
|
||||
}).sudo()
|
||||
cls.type_issue = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({
|
||||
'name': 'Issue_test',
|
||||
}).sudo()
|
||||
|
||||
@classmethod
|
||||
def setUpSLATeam(cls):
|
||||
""" Generate Team, some stage and SLAs for the team """
|
||||
# create team and stages
|
||||
cls.team_with_sla = cls.env['odex25_helpdesk.team'].create({
|
||||
'name': 'Team with SLAs',
|
||||
'use_sla': True
|
||||
})
|
||||
|
||||
Stage = cls.env['odex25_helpdesk.stage']
|
||||
cls.team_sla_stage_new = Stage.create({
|
||||
'name': 'New',
|
||||
'sequence': 10,
|
||||
'team_ids': [(4, cls.team_with_sla.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.team_sla_stage_progress = Stage.create({
|
||||
'name': 'In Progress',
|
||||
'sequence': 20,
|
||||
'team_ids': [(4, cls.team_with_sla.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.team_sla_stage_done = Stage.create({
|
||||
'name': 'Done',
|
||||
'sequence': 30,
|
||||
'team_ids': [(4, cls.team_with_sla.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
cls.team_sla_stage_cancel = Stage.create({
|
||||
'name': 'Cancelled',
|
||||
'sequence': 40,
|
||||
'team_ids': [(4, cls.team_with_sla.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
|
||||
# create SLAs
|
||||
SLA = cls.env['odex25_helpdesk.sla']
|
||||
cls.sla_1_progress = SLA.create({
|
||||
'name': "2 days to be in progress",
|
||||
'stage_id': cls.team_sla_stage_progress.id,
|
||||
'time_days': 2,
|
||||
'team_id': cls.team_with_sla.id,
|
||||
})
|
||||
cls.sla_2_done = SLA.create({
|
||||
'name': "7 days to be in progress",
|
||||
'stage_id': cls.team_sla_stage_done.id,
|
||||
'time_days': 7,
|
||||
'team_id': cls.team_with_sla.id,
|
||||
'priority': '0',
|
||||
})
|
||||
cls.sla_3_done_prior = SLA.create({
|
||||
'name': "5 days to be in done for 3 stars ticket",
|
||||
'stage_id': cls.team_sla_stage_done.id,
|
||||
'time_days': 5,
|
||||
'team_id': cls.team_with_sla.id,
|
||||
'priority': '3',
|
||||
})
|
||||
|
||||
def _utils_set_create_date(self, records, date_str):
|
||||
""" This method is a hack in order to be able to define/redefine the create_date
|
||||
of the any recordset. This is done in SQL because ORM does not allow to write
|
||||
onto the create_date field.
|
||||
:param records: recordset of any odoo models
|
||||
"""
|
||||
query = """
|
||||
UPDATE %s
|
||||
SET create_date = %%s
|
||||
WHERE id IN %%s
|
||||
""" % (records._table,)
|
||||
self.env.cr.execute(query, (date_str, tuple(records.ids)))
|
||||
|
||||
records.invalidate_cache()
|
||||
|
||||
if records._name == 'odex25_helpdesk.ticket':
|
||||
field = self.env['odex25_helpdesk.sla.status']._fields['deadline']
|
||||
self.env.add_to_compute(field, records.sla_status_ids)
|
||||
records.recompute()
|
||||
|
||||
@contextmanager
|
||||
def _ticket_patch_now(self, datetime_str):
|
||||
datetime_now_old = getattr(fields.Datetime, 'now')
|
||||
datetime_today_old = getattr(fields.Datetime, 'today')
|
||||
|
||||
def new_now():
|
||||
return fields.Datetime.from_string(datetime_str)
|
||||
|
||||
def new_today():
|
||||
return fields.Datetime.from_string(datetime_str).replace(hour=0, minute=0, second=0)
|
||||
|
||||
try:
|
||||
setattr(fields.Datetime, 'now', new_now)
|
||||
setattr(fields.Datetime, 'today', new_today)
|
||||
|
||||
yield
|
||||
finally:
|
||||
# back
|
||||
setattr(fields.Datetime, 'now', datetime_now_old)
|
||||
setattr(fields.Datetime, 'today', datetime_today_old)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests.common import HttpCase, tagged
|
||||
import re
|
||||
|
||||
|
||||
@tagged('-standard', 'external', 'post_install', '-at_install') # nightly is not a real tag
|
||||
class TestDocLinks(HttpCase):
|
||||
"""
|
||||
Parse the 'odex25_helpdesk.team' view to extract all documentation links and
|
||||
check that every links are still valid.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set-up the test environment
|
||||
"""
|
||||
super(TestDocLinks, self).setUp()
|
||||
self.re = re.compile("<a href=\"(\S+/documentation/\S+)\"")
|
||||
self.links = set()
|
||||
|
||||
def test_01_links(self):
|
||||
"""
|
||||
Firs test: check that all documentation links in 'odex25_helpdesk.team'
|
||||
views are not broken.
|
||||
"""
|
||||
self._parse_view(self.env.ref('odex25_helpdesk.odex25_helpdesk_team_view_form'))
|
||||
|
||||
for link in self.links:
|
||||
self._check_link(link)
|
||||
|
||||
def _check_link(self, link):
|
||||
"""
|
||||
Try to open the link and check the response status code
|
||||
"""
|
||||
res = self.url_open(url=link)
|
||||
|
||||
self.assertEqual(
|
||||
res.status_code, 200,
|
||||
"The following link is broken: '%s'" % (link)
|
||||
)
|
||||
|
||||
def _parse_view(self, view):
|
||||
"""
|
||||
Analyse the view to extract documentation links and store them
|
||||
in a set.
|
||||
Then, parse its children if any.
|
||||
"""
|
||||
|
||||
# search the documentation links in the current view
|
||||
for match in re.finditer(self.re, view.arch):
|
||||
self.links.add(match.group(1))
|
||||
|
||||
# and then, search inside children
|
||||
for child in view.inherit_children_ids:
|
||||
self._parse_view(child)
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from .common import odex25_helpdeskCommon
|
||||
from odoo import fields
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Testodex25_helpdeskFlow(odex25_helpdeskCommon):
|
||||
""" Test used to check that the base functionalities of Helpdesk function as expected.
|
||||
- test_access_rights: tests a few access rights constraints
|
||||
- test_assign_close_dates: tests the assignation and closing time get computed correctly
|
||||
- test_ticket_partners: tests the number of tickets of a partner is computed correctly
|
||||
- test_team_assignation_[method]: tests the team assignation method work as expected
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(Testodex25_helpdeskFlow, self).setUp()
|
||||
|
||||
def test_access_rights(self):
|
||||
# Helpdesk user should only be able to:
|
||||
# read: teams, stages, SLAs, ticket types
|
||||
# read, create, write, unlink: tickets, tags
|
||||
# Helpdesk manager:
|
||||
# read, create, write, unlink: everything (from odex25_helpdesk)
|
||||
# we consider in these tests that if the user can do it, the manager can do it as well (as the group is implied)
|
||||
def test_write_and_unlink(record):
|
||||
record.write({'name': 'test_write'})
|
||||
record.unlink()
|
||||
|
||||
def test_not_write_and_unlink(self, record):
|
||||
with self.assertRaises(AccessError):
|
||||
record.write({'name': 'test_write'})
|
||||
with self.assertRaises(AccessError):
|
||||
record.unlink()
|
||||
# self.assertRaises(AccessError, record.write({'name': 'test_write'})) # , "Helpdesk user should not be able to write on %s" % record._name)
|
||||
# self.assertRaises(AccessError, record.unlink(), "Helpdesk user could unlink %s" % record._name)
|
||||
|
||||
# odex25_helpdesk.team access rights
|
||||
team = self.env['odex25_helpdesk.team'].with_user(self.odex25_helpdesk_manager).create({'name': 'test'})
|
||||
team.with_user(self.odex25_helpdesk_user).read()
|
||||
test_not_write_and_unlink(self, team.with_user(self.odex25_helpdesk_user))
|
||||
with self.assertRaises(AccessError):
|
||||
team.with_user(self.odex25_helpdesk_user).create({'name': 'test create'})
|
||||
test_write_and_unlink(team)
|
||||
|
||||
# odex25_helpdesk.ticket access rights
|
||||
ticket = self.env['odex25_helpdesk.ticket'].with_user(self.odex25_helpdesk_user).create({'name': 'test'})
|
||||
ticket.read()
|
||||
test_write_and_unlink(ticket)
|
||||
|
||||
# odex25_helpdesk.stage access rights
|
||||
stage = self.env['odex25_helpdesk.stage'].with_user(self.odex25_helpdesk_manager).create({
|
||||
'name': 'test',
|
||||
'team_ids': [(6, 0, [self.test_team.id])],
|
||||
})
|
||||
stage.with_user(self.odex25_helpdesk_user).read()
|
||||
test_not_write_and_unlink(self, stage.with_user(self.odex25_helpdesk_user))
|
||||
with self.assertRaises(AccessError):
|
||||
stage.with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'test create',
|
||||
'team_ids': [(6, 0, [self.test_team.id])],
|
||||
})
|
||||
test_write_and_unlink(stage)
|
||||
|
||||
# odex25_helpdesk.sla access rights
|
||||
sla = self.env['odex25_helpdesk.sla'].with_user(self.odex25_helpdesk_manager).create({
|
||||
'name': 'test',
|
||||
'team_id': self.test_team.id,
|
||||
'stage_id': self.stage_done.id,
|
||||
})
|
||||
sla.with_user(self.odex25_helpdesk_user).read()
|
||||
test_not_write_and_unlink(self, sla.with_user(self.odex25_helpdesk_user))
|
||||
with self.assertRaises(AccessError):
|
||||
sla.with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'test create',
|
||||
'team_id': self.test_team.id,
|
||||
'stage_id': self.stage_done.id,
|
||||
})
|
||||
test_write_and_unlink(sla)
|
||||
|
||||
# odex25_helpdesk.ticket.type access rights
|
||||
ticket_type = self.env['odex25_helpdesk.ticket.type'].with_user(self.odex25_helpdesk_manager).create({
|
||||
'name': 'test with unique name please',
|
||||
})
|
||||
ticket_type.with_user(self.odex25_helpdesk_user).read()
|
||||
test_not_write_and_unlink(self, ticket_type.with_user(self.odex25_helpdesk_user))
|
||||
with self.assertRaises(AccessError):
|
||||
ticket_type.with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'test create with unique name please',
|
||||
})
|
||||
test_write_and_unlink(ticket_type)
|
||||
|
||||
# odex25_helpdesk.tag access rights
|
||||
tag = self.env['odex25_helpdesk.tag'].with_user(self.odex25_helpdesk_user).create({'name': 'test with unique name please'})
|
||||
tag.read()
|
||||
test_write_and_unlink(tag)
|
||||
|
||||
def test_assign_close_dates(self):
|
||||
# Helpdesk user create a ticket
|
||||
ticket1 = self.env['odex25_helpdesk.ticket'].with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'test ticket 1',
|
||||
'team_id': self.test_team.id,
|
||||
})
|
||||
self._utils_set_create_date(ticket1, '2019-01-08 12:00:00')
|
||||
|
||||
with self._ticket_patch_now('2019-01-10 13:00:00'):
|
||||
# the Helpdesk user takes the ticket
|
||||
ticket1.assign_ticket_to_self()
|
||||
# we verify the ticket is correctly assigned
|
||||
self.assertEqual(ticket1.user_id.id, ticket1._uid, "Assignation for ticket not correct")
|
||||
self.assertEqual(ticket1.assign_hours, 17, "Assignation time for ticket not correct")
|
||||
with self._ticket_patch_now('2019-01-10 15:00:00'):
|
||||
# we close the ticket and verify its closing time
|
||||
ticket1.write({'stage_id': self.stage_done.id})
|
||||
self.assertEqual(ticket1.close_hours, 19, "Close time for ticket not correct")
|
||||
|
||||
def test_ticket_partners(self):
|
||||
# we create a partner
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Freddy Krueger'
|
||||
})
|
||||
# Helpdesk user creates 2 tickets for the partner
|
||||
ticket1 = self.env['odex25_helpdesk.ticket'].with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'partner ticket 1',
|
||||
'team_id': self.test_team.id,
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
self.env['odex25_helpdesk.ticket'].with_user(self.odex25_helpdesk_user).create({
|
||||
'name': 'partner ticket 2',
|
||||
'team_id': self.test_team.id,
|
||||
'partner_id': partner.id,
|
||||
})
|
||||
self.assertTrue(ticket1.partner_ticket_count == 2, "Incorrect number of tickets from the same partner.")
|
||||
|
||||
def test_team_assignation_randomly(self):
|
||||
# we put the Helpdesk user and manager in the test_team's members
|
||||
self.test_team.member_ids = [(6, 0, [self.odex25_helpdesk_user.id, self.odex25_helpdesk_manager.id])]
|
||||
# we set the assignation method to randomly (=uniformly distributed)
|
||||
self.test_team.assign_method = 'randomly'
|
||||
# we create a bunch of tickets
|
||||
for i in range(10):
|
||||
self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test ticket ' + str(i),
|
||||
'team_id': self.test_team.id,
|
||||
})
|
||||
# ensure both members have the same amount of tickets assigned
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_user.id)]), 5)
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_manager.id)]), 5)
|
||||
|
||||
def test_team_assignation_balanced(self):
|
||||
# we put the Helpdesk user and manager in the test_team's members
|
||||
self.test_team.member_ids = [(6, 0, [self.odex25_helpdesk_user.id, self.odex25_helpdesk_manager.id])]
|
||||
# we set the assignation method to randomly (=uniformly distributed)
|
||||
self.test_team.assign_method = 'balanced'
|
||||
# we create a bunch of tickets
|
||||
for i in range(4):
|
||||
self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test ticket ' + str(i),
|
||||
'team_id': self.test_team.id,
|
||||
})
|
||||
# ensure both members have the same amount of tickets assigned
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_user.id)]), 2)
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_manager.id)]), 2)
|
||||
|
||||
# Helpdesk user finishes his 2 tickets
|
||||
self.env['odex25_helpdesk.ticket'].search([('user_id', '=', self.odex25_helpdesk_user.id)]).write({'stage_id': self.stage_done.id})
|
||||
|
||||
# we create 4 new tickets
|
||||
for i in range(4):
|
||||
self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test ticket ' + str(i),
|
||||
'team_id': self.test_team.id,
|
||||
})
|
||||
|
||||
# ensure both members have the same amount of tickets assigned
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_user.id), ('close_date', '=', False)]), 3)
|
||||
self.assertEqual(self.env['odex25_helpdesk.ticket'].search_count([('user_id', '=', self.odex25_helpdesk_manager.id), ('close_date', '=', False)]), 3)
|
||||
|
||||
def test_create_from_email_multicompany(self):
|
||||
company0 = self.env.company
|
||||
company1 = self.env['res.company'].create({'name': 'new_company0'})
|
||||
Partner = self.env['res.partner']
|
||||
|
||||
self.env.user.write({
|
||||
'company_ids': [(4, company0.id, False), (4, company1.id, False)],
|
||||
})
|
||||
|
||||
odex25_helpdesk_team_model = self.env['ir.model'].search([('model', '=', 'odex25_helpdesk_team')])
|
||||
ticket_model = self.env['ir.model'].search([('model', '=', 'odex25_helpdesk.ticket')])
|
||||
self.env["ir.config_parameter"].sudo().set_param("mail.catchall.domain", 'aqualung.com')
|
||||
|
||||
odex25_helpdesk_team0 = self.env['odex25_helpdesk.team'].create({
|
||||
'name': 'Helpdesk team 0',
|
||||
'company_id': company0.id,
|
||||
})
|
||||
odex25_helpdesk_team1 = self.env['odex25_helpdesk.team'].create({
|
||||
'name': 'Helpdesk team 1',
|
||||
'company_id': company1.id,
|
||||
})
|
||||
|
||||
mail_alias0 = self.env['mail.alias'].create({
|
||||
'alias_name': 'odex25_helpdesk_team_0',
|
||||
'alias_model_id': ticket_model.id,
|
||||
'alias_parent_model_id': odex25_helpdesk_team_model.id,
|
||||
'alias_parent_thread_id': odex25_helpdesk_team0.id,
|
||||
'alias_defaults': "{'team_id': %s}" % odex25_helpdesk_team0.id,
|
||||
})
|
||||
mail_alias1 = self.env['mail.alias'].create({
|
||||
'alias_name': 'odex25_helpdesk_team_1',
|
||||
'alias_model_id': ticket_model.id,
|
||||
'alias_parent_model_id': odex25_helpdesk_team_model.id,
|
||||
'alias_parent_thread_id': odex25_helpdesk_team1.id,
|
||||
'alias_defaults': "{'team_id': %s}" % odex25_helpdesk_team1.id,
|
||||
})
|
||||
|
||||
new_message0 = """MIME-Version: 1.0
|
||||
Date: Thu, 27 Dec 2018 16:27:45 +0100
|
||||
Message-ID: blablabla0
|
||||
Subject: Helpdesk team 0 in company 0
|
||||
From: A client <client_a@someprovider.com>
|
||||
To: odex25_helpdesk_team_0@aqualung.com
|
||||
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
|
||||
|
||||
--000000000000a47519057e029630
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
|
||||
--000000000000a47519057e029630
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<div>A good message</div>
|
||||
|
||||
--000000000000a47519057e029630--
|
||||
"""
|
||||
|
||||
new_message1 = """MIME-Version: 1.0
|
||||
Date: Thu, 27 Dec 2018 16:27:45 +0100
|
||||
Message-ID: blablabla1
|
||||
Subject: Helpdesk team 1 in company 1
|
||||
From: B client <client_b@someprovider.com>
|
||||
To: odex25_helpdesk_team_1@aqualung.com
|
||||
Content-Type: multipart/alternative; boundary="000000000000a47519057e029630"
|
||||
|
||||
--000000000000a47519057e029630
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
|
||||
--000000000000a47519057e029630
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<div>A good message bis</div>
|
||||
|
||||
--000000000000a47519057e029630--
|
||||
"""
|
||||
partners_exist = Partner.search([('email', 'in', ['client_a@someprovider.com', 'client_b@someprovider.com'])])
|
||||
self.assertFalse(partners_exist)
|
||||
|
||||
odex25_helpdesk_ticket0_id = self.env['mail.thread'].message_process('odex25_helpdesk.ticket', new_message0)
|
||||
odex25_helpdesk_ticket1_id = self.env['mail.thread'].message_process('odex25_helpdesk.ticket', new_message1)
|
||||
|
||||
odex25_helpdesk_ticket0 = self.env['odex25_helpdesk.ticket'].browse(odex25_helpdesk_ticket0_id)
|
||||
odex25_helpdesk_ticket1 = self.env['odex25_helpdesk.ticket'].browse(odex25_helpdesk_ticket1_id)
|
||||
|
||||
self.assertEqual(odex25_helpdesk_ticket0.team_id, odex25_helpdesk_team0)
|
||||
self.assertEqual(odex25_helpdesk_ticket1.team_id, odex25_helpdesk_team1)
|
||||
|
||||
self.assertEqual(odex25_helpdesk_ticket0.company_id, company0)
|
||||
self.assertEqual(odex25_helpdesk_ticket1.company_id, company1)
|
||||
|
||||
partner0 = Partner.search([('email', '=', 'client_a@someprovider.com')])
|
||||
partner1 = Partner.search([('email', '=', 'client_b@someprovider.com')])
|
||||
self.assertTrue(partner0)
|
||||
self.assertTrue(partner1)
|
||||
|
||||
self.assertEqual(partner0.company_id, company0)
|
||||
self.assertEqual(partner1.company_id, company1)
|
||||
|
||||
self.assertEqual(odex25_helpdesk_ticket0.partner_id, partner0)
|
||||
self.assertEqual(odex25_helpdesk_ticket1.partner_id, partner1)
|
||||
|
||||
self.assertTrue(partner0 in odex25_helpdesk_ticket0.message_follower_ids.mapped('partner_id'))
|
||||
self.assertTrue(partner1 in odex25_helpdesk_ticket1.message_follower_ids.mapped('partner_id'))
|
||||
|
||||
def test_team_assignation_balanced(self):
|
||||
#We create an sla policy with minimum priority set as '2'
|
||||
self.test_team.use_sla = True
|
||||
sla = self.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'test sla policy',
|
||||
'team_id': self.test_team.id,
|
||||
'stage_id': self.stage_progress.id,
|
||||
'priority': '2',
|
||||
'time_days': 0,
|
||||
'time_hours': 1
|
||||
})
|
||||
|
||||
#We create a ticket with priority less than what's on the sla policy
|
||||
ticket_1 = self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test ',
|
||||
'team_id': self.test_team.id,
|
||||
'priority': '1'
|
||||
})
|
||||
|
||||
#We create a ticket with priority equal to what's on the sla policy
|
||||
ticket_2 = self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test sla ticket',
|
||||
'team_id': self.test_team.id,
|
||||
'priority': '2'
|
||||
})
|
||||
|
||||
#We create a ticket with priority greater than what's on the sla policy
|
||||
ticket_3 = self.env['odex25_helpdesk.ticket'].create({
|
||||
'name': 'test sla ticket',
|
||||
'team_id': self.test_team.id,
|
||||
'priority': '3'
|
||||
})
|
||||
#We confirm that the sla policy has been applied successfully on the ticket.
|
||||
#sla policy must not be applied
|
||||
self.assertTrue(sla not in ticket_1.sla_status_ids.mapped('sla_id'))
|
||||
#sla policy must be applied
|
||||
self.assertTrue(sla in ticket_2.sla_status_ids.mapped('sla_id'))
|
||||
self.assertTrue(sla in ticket_3.sla_status_ids.mapped('sla_id'))
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import SavepointCase
|
||||
|
||||
NOW = datetime(2018, 10, 10, 9, 18)
|
||||
NOW2 = datetime(2019, 1, 8, 9, 0)
|
||||
|
||||
|
||||
class odex25_helpdeskSLA(SavepointCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(odex25_helpdeskSLA, cls).setUpClass()
|
||||
|
||||
# we create a Helpdesk user and a manager
|
||||
Users = cls.env['res.users'].with_context(tracking_disable=True)
|
||||
cls.main_company_id = cls.env.ref('base.main_company').id
|
||||
cls.odex25_helpdesk_manager = Users.create({
|
||||
'company_id': cls.main_company_id,
|
||||
'name': 'Helpdesk Manager',
|
||||
'login': 'hm',
|
||||
'email': 'hm@example.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id])]
|
||||
})
|
||||
cls.odex25_helpdesk_user = Users.create({
|
||||
'company_id': cls.main_company_id,
|
||||
'name': 'Helpdesk User',
|
||||
'login': 'hu',
|
||||
'email': 'hu@example.com',
|
||||
'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id])]
|
||||
})
|
||||
# the manager defines a team for our tests (the .sudo() at the end is to avoid potential uid problems)
|
||||
cls.test_team = cls.env['odex25_helpdesk.team'].with_user(cls.odex25_helpdesk_manager).create({
|
||||
'name': 'Test Team',
|
||||
'use_sla': True
|
||||
}).sudo()
|
||||
# He then defines its stages
|
||||
stage_as_manager = cls.env['odex25_helpdesk.stage'].with_user(cls.odex25_helpdesk_manager)
|
||||
cls.stage_new = stage_as_manager.create({
|
||||
'name': 'New',
|
||||
'sequence': 10,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.stage_progress = stage_as_manager.create({
|
||||
'name': 'In Progress',
|
||||
'sequence': 20,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.stage_wait = stage_as_manager.create({
|
||||
'name': 'Waiting',
|
||||
'sequence': 25,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': False,
|
||||
})
|
||||
cls.stage_done = stage_as_manager.create({
|
||||
'name': 'Done',
|
||||
'sequence': 30,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
cls.stage_cancel = stage_as_manager.create({
|
||||
'name': 'Cancelled',
|
||||
'sequence': 40,
|
||||
'team_ids': [(4, cls.test_team.id, 0)],
|
||||
'is_close': True,
|
||||
})
|
||||
|
||||
cls.tag_vip = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'VIP'})
|
||||
cls.tag_urgent = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Urgent'})
|
||||
cls.tag_freeze = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Freeze'})
|
||||
|
||||
cls.sla = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_days': 1,
|
||||
'time_hours': 24,
|
||||
'stage_id': cls.stage_progress.id,
|
||||
})
|
||||
cls.sla_2 = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA done stage with freeze time',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_days': 1,
|
||||
'time_hours': 2,
|
||||
'time_minutes': 2,
|
||||
'tag_ids': [(4, cls.tag_freeze.id)],
|
||||
'exclude_stage_ids': cls.stage_wait.ids,
|
||||
'stage_id': cls.stage_done.id,
|
||||
})
|
||||
cls.sla_assigning_1 = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA assigning no stage',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_hours': 1,
|
||||
'target_type': 'assigning'
|
||||
})
|
||||
|
||||
cls.sla_assigning_2 = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA assigning new stage',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_hours': 1,
|
||||
'stage_id': cls.stage_new.id,
|
||||
'target_type': 'assigning'
|
||||
})
|
||||
|
||||
cls.sla_assigning_3 = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA assigning progress stage',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_hours': 1,
|
||||
'stage_id': cls.stage_progress.id,
|
||||
'target_type': 'assigning'
|
||||
})
|
||||
|
||||
cls.sla_assigning_4 = cls.env['odex25_helpdesk.sla'].create({
|
||||
'name': 'SLA assigning done stage',
|
||||
'team_id': cls.test_team.id,
|
||||
'time_hours': 1,
|
||||
'stage_id': cls.stage_done.id,
|
||||
'target_type': 'assigning'
|
||||
})
|
||||
|
||||
# He also creates a ticket types for Question and Issue
|
||||
cls.type_question = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({
|
||||
'name': 'Question_test',
|
||||
}).sudo()
|
||||
cls.type_issue = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({
|
||||
'name': 'Issue_test',
|
||||
}).sudo()
|
||||
|
||||
def _utils_set_create_date(self, records, date_str, ticket_to_update=False):
|
||||
""" This method is a hack in order to be able to define/redefine the create_date
|
||||
of the any recordset. This is done in SQL because ORM does not allow to write
|
||||
onto the create_date field.
|
||||
:param records: recordset of any odoo models
|
||||
"""
|
||||
query = """
|
||||
UPDATE %s
|
||||
SET create_date = %%s
|
||||
WHERE id IN %%s
|
||||
""" % (records._table,)
|
||||
self.env.cr.execute(query, (date_str, tuple(records.ids)))
|
||||
|
||||
records.invalidate_cache()
|
||||
|
||||
if ticket_to_update:
|
||||
ticket_to_update.sla_status_ids._compute_deadline()
|
||||
|
||||
@contextmanager
|
||||
def _ticket_patch_now(self, datetime_str):
|
||||
datetime_now_old = getattr(fields.Datetime, 'now')
|
||||
datetime_today_old = getattr(fields.Datetime, 'today')
|
||||
|
||||
def new_now():
|
||||
return fields.Datetime.from_string(datetime_str)
|
||||
|
||||
def new_today():
|
||||
return fields.Datetime.from_string(datetime_str).replace(hour=0, minute=0, second=0)
|
||||
|
||||
try:
|
||||
setattr(fields.Datetime, 'now', new_now)
|
||||
setattr(fields.Datetime, 'today', new_today)
|
||||
|
||||
yield
|
||||
finally:
|
||||
# back
|
||||
setattr(fields.Datetime, 'now', datetime_now_old)
|
||||
setattr(fields.Datetime, 'today', datetime_today_old)
|
||||
|
||||
def create_ticket(self, *arg, **kwargs):
|
||||
default_values = {
|
||||
'name': "Help me",
|
||||
'team_id': self.test_team.id,
|
||||
'tag_ids': [(4, self.tag_urgent.id)],
|
||||
'stage_id': self.stage_new.id,
|
||||
}
|
||||
if 'tag_ids' in kwargs:
|
||||
# from recordset to ORM command
|
||||
kwargs['tag_ids'] = [(6, False, [tag.id for tag in kwargs['tag_ids']])]
|
||||
values = dict(default_values, **kwargs)
|
||||
return self.env['odex25_helpdesk.ticket'].create(values)
|
||||
|
||||
def test_sla_no_tag(self):
|
||||
""" SLA without tag should apply to all tickets """
|
||||
self.sla.tag_ids = [(5,)]
|
||||
ticket = self.create_ticket(tag_ids=self.tag_urgent)
|
||||
self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied")
|
||||
|
||||
def test_sla_single_tag(self):
|
||||
self.sla.tag_ids = [(4, self.tag_urgent.id)]
|
||||
ticket = self.create_ticket(tag_ids=self.tag_urgent)
|
||||
self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied")
|
||||
|
||||
def test_sla_multiple_tags(self):
|
||||
self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)]
|
||||
ticket = self.create_ticket(tag_ids=self.tag_urgent)
|
||||
self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should not have been applied yet")
|
||||
ticket.tag_ids = [(4, self.tag_vip.id)]
|
||||
self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied")
|
||||
|
||||
def test_sla_tag_and_ticket_type(self):
|
||||
self.sla.tag_ids = [(6, False, self.tag_urgent.ids)]
|
||||
self.sla.ticket_type_id = self.type_question
|
||||
ticket = self.create_ticket(tag_ids=self.tag_urgent)
|
||||
self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should not have been applied yet")
|
||||
ticket.ticket_type_id = self.type_question
|
||||
self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied")
|
||||
|
||||
def test_sla_remove_tag(self):
|
||||
self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)]
|
||||
ticket = self.create_ticket(tag_ids=self.tag_urgent | self.tag_vip)
|
||||
self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied")
|
||||
ticket.tag_ids = [(5,)] # Remove all tags
|
||||
self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should no longer apply")
|
||||
|
||||
@patch.object(fields.Datetime, 'now', lambda: NOW2)
|
||||
def test_sla_waiting(self):
|
||||
ticket = self.create_ticket(tag_ids=self.tag_freeze)
|
||||
self._utils_set_create_date(ticket, '2019-01-08 9:00:00', ticket)
|
||||
status = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_2.id)
|
||||
self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes')
|
||||
|
||||
ticket.write({'stage_id': self.stage_progress.id})
|
||||
initial_values = {ticket.id: {'stage_id': self.stage_new}}
|
||||
ticket.message_track(['stage_id'], initial_values)
|
||||
self._utils_set_create_date(ticket.message_ids.tracking_value_ids, '2019-01-08 11:09:50', ticket)
|
||||
self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes')
|
||||
|
||||
# We are in waiting stage, they are no more deadline.
|
||||
ticket.write({'stage_id': self.stage_wait.id})
|
||||
initial_values = {ticket.id: {'stage_id': self.stage_progress}}
|
||||
ticket.message_track(['stage_id'], initial_values)
|
||||
self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-08 12:15:00', ticket)
|
||||
self.assertFalse(status.deadline, 'In waiting stage: no more deadline')
|
||||
|
||||
# We have a response of our customer, the ticket switch to in progress stage (outside working hours)
|
||||
ticket.write({'stage_id': self.stage_progress.id})
|
||||
initial_values = {ticket.id: {'stage_id': self.stage_wait}}
|
||||
ticket.message_track(['stage_id'], initial_values)
|
||||
self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-12 10:35:58', ticket)
|
||||
# waiting time = 3 full working days 9 - 10 - 11 January (12 doesn't count as it's Saturday)
|
||||
# + (8 January) 12:15:00 -> 16:00:00 (end of working day) 3,75 hours
|
||||
# Old deadline = '2019-01-09 12:02:00'
|
||||
# New: '2019-01-09 12:02:00' + 3 days (waiting) + 2 days (weekend) + 3.75 hours (waiting) = '2019-01-14 15:47:00'
|
||||
self.assertEqual(status.deadline, datetime(2019, 1, 14, 15, 47), 'We have waiting time: deadline = old_deadline + 3 full working days (waiting) + 3.75 hours (waiting) + 2 days (weekend)')
|
||||
|
||||
ticket.write({'stage_id': self.stage_wait.id})
|
||||
initial_values = {ticket.id: {'stage_id': self.stage_progress}}
|
||||
ticket.message_track(['stage_id'], initial_values)
|
||||
self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-14 15:30:00', ticket)
|
||||
self.assertFalse(status.deadline, 'In waiting stage: no more deadline')
|
||||
|
||||
# We need to patch now with a new value as it will be used to compute freezed time.
|
||||
with patch.object(fields.Datetime, 'now', lambda: datetime(2019, 1, 16, 15, 0)):
|
||||
ticket.write({'stage_id': self.stage_done.id})
|
||||
initial_values = {ticket.id: {'stage_id': self.stage_wait}}
|
||||
ticket.message_track(['stage_id'], initial_values)
|
||||
self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-16 15:00:00', ticket)
|
||||
self.assertEqual(status.deadline, datetime(2019, 1, 16, 15, 17), 'We have waiting time: deadline = old_deadline + 7.5 hours (waiting)')
|
||||
|
||||
def test_sla_assigning(self):
|
||||
ticket = self.create_ticket()
|
||||
|
||||
status_1 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_1.id)
|
||||
status_2 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_2.id)
|
||||
status_3 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_3.id)
|
||||
status_4 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_4.id)
|
||||
|
||||
self.assertFalse(status_1.reached_datetime, "SLA status 1: reached not his target")
|
||||
self.assertFalse(status_2.reached_datetime, "SLA status 2: reached not his target")
|
||||
self.assertFalse(status_3.reached_datetime, "SLA status 3: reached not his target")
|
||||
self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target")
|
||||
self.assertTrue(status_1.deadline, "SLA status 1: has deadline")
|
||||
self.assertTrue(status_2.deadline, "SLA status 2: has deadline")
|
||||
self.assertFalse(status_3.deadline, "SLA status 3: hasn't deadline")
|
||||
self.assertFalse(status_4.deadline, "SLA status 4: hasn't deadline")
|
||||
|
||||
ticket.write({'user_id': self.odex25_helpdesk_user.id})
|
||||
|
||||
self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target")
|
||||
self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target")
|
||||
self.assertFalse(status_3.reached_datetime, "SLA status 3: reached not his target")
|
||||
self.assertTrue(status_1.deadline, "SLA status 1: has deadline")
|
||||
self.assertTrue(status_2.deadline, "SLA status 2: has deadline")
|
||||
self.assertFalse(status_3.deadline, "SLA status 3: hasn't deadline")
|
||||
|
||||
ticket.write({'stage_id': self.stage_progress.id})
|
||||
|
||||
self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target")
|
||||
self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target")
|
||||
self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target")
|
||||
self.assertTrue(status_1.deadline, "SLA status 1: has deadline")
|
||||
self.assertTrue(status_2.deadline, "SLA status 2: has deadline")
|
||||
self.assertTrue(status_3.deadline, "SLA status 3: has deadline")
|
||||
|
||||
ticket.write({'user_id': False})
|
||||
|
||||
self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target")
|
||||
self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target")
|
||||
self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target")
|
||||
self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target")
|
||||
self.assertTrue(status_1.deadline, "SLA status 1: has deadline")
|
||||
self.assertTrue(status_2.deadline, "SLA status 2: has deadline")
|
||||
self.assertTrue(status_3.deadline, "SLA status 3: has deadline")
|
||||
self.assertFalse(status_4.deadline, "SLA status 4: hasn't deadline")
|
||||
|
||||
ticket.write({'stage_id': self.stage_done.id})
|
||||
|
||||
self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target")
|
||||
self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target")
|
||||
self.assertTrue(status_3.deadline, "SLA status 3: has deadline")
|
||||
self.assertTrue(status_4.deadline, "SLA status 4: has deadline")
|
||||
|
||||
ticket.write({'user_id': self.odex25_helpdesk_user.id})
|
||||
|
||||
self.assertTrue(status_4.reached_datetime, "SLA status 4: reached his target")
|
||||
|
||||
def test_sla_assigning_skip_step(self):
|
||||
ticket = self.create_ticket()
|
||||
|
||||
status_1 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_1.id)
|
||||
status_2 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_2.id)
|
||||
|
||||
self.assertTrue(status_2.deadline, "SLA status 2: has deadline")
|
||||
|
||||
ticket.write({'stage_id': self.stage_progress.id})
|
||||
|
||||
self.assertFalse(status_2.deadline, "SLA status 2: has no more deadline")
|
||||
self.assertFalse(status_2.reached_datetime, "SLA status 2: reached not his target")
|
||||
|
||||
ticket.write({'user_id': self.odex25_helpdesk_user.id})
|
||||
|
||||
self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target")
|
||||
|
||||
@patch.object(fields.Date, 'today', lambda: NOW.date())
|
||||
@patch.object(fields.Datetime, 'today', lambda: NOW.replace(hour=0, minute=0, second=0))
|
||||
@patch.object(fields.Datetime, 'now', lambda: NOW)
|
||||
def test_failed_tickets(self):
|
||||
self.sla.time_days = 0
|
||||
self.sla.time_hours = 3
|
||||
# Failed ticket
|
||||
failed_ticket = self.create_ticket(user_id=self.env.user.id, create_date=NOW - relativedelta(hours=3, minutes=2))
|
||||
|
||||
# Not failed ticket
|
||||
ticket = self.create_ticket(user_id=self.env.user.id, create_date=NOW - relativedelta(hours=2, minutes=2))
|
||||
|
||||
data = self.env['odex25_helpdesk.team'].retrieve_dashboard()
|
||||
self.assertEqual(data['my_all']['count'], 2, "There should be 2 tickets")
|
||||
self.assertEqual(data['my_all']['failed'], 1, "There should be 1 failed ticket")
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.tagged('post_install', '-at_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
def test_ui(self):
|
||||
self.start_tour("/web", 'odex25_helpdesk_tour', login="admin")
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="Helpdesk assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_helpdesk/static/src/scss/odex25_helpdesk.scss"/>
|
||||
<link rel="stylesheet" href="/odex25_helpdesk/static/src/css/portal_odex25_helpdesk.css"/>
|
||||
<script type="text/javascript" src="/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js"></script>
|
||||
<script type="text/javascript" src="/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="odex25_helpdesk_tests" inherit_id="web.qunit_suite_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_helpdesk/static/tests/odex25_helpdesk_dashboard_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="digest_digest_view_form" model="ir.ui.view">
|
||||
<field name="name">digest.digest.view.form.inherit.sale.order</field>
|
||||
<field name="model">digest.digest</field>
|
||||
<field name="inherit_id" ref="digest.digest_digest_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='kpi_general']" position="after">
|
||||
<group name="kpi_odex25_helpdesk" string="Helpdesk" groups='odex25_helpdesk.group_odex25_helpdesk_user'>
|
||||
<field name="kpi_odex25_helpdesk_tickets_closed"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<!-- Activity types config -->
|
||||
<!-- Activity types config -->
|
||||
<record id="mail_act_odex25_helpdesk_assistance" model="mail.activity.type">
|
||||
<field name="name">Ticket Assistance</field>
|
||||
<field name="summary">Your Assistance is Required</field>
|
||||
<field name="icon">fa-ticket</field>
|
||||
<field name="res_model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_activity_type_action_config_odex25_helpdesk" model="ir.actions.act_window">
|
||||
<field name="name">Activity Types</field>
|
||||
<field name="res_model">mail.activity.type</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">['|', ('res_model_id', '=', False), ('res_model_id.model', '=', 'odex25_helpdesk.ticket')]</field>
|
||||
<field name="context">{'default_res_model': 'odex25_helpdesk.ticket'}</field>
|
||||
</record>
|
||||
<menuitem id="odex25_helpdesk_menu_config_activity_type"
|
||||
action="mail_activity_type_action_config_odex25_helpdesk"
|
||||
parent="odex25_helpdesk_menu_config"
|
||||
groups="base.group_no_one"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<template id="portal_my_home_menu_odex25_helpdesk" name="Portal layout : Helpdesk tickets menu entries" inherit_id="portal.portal_breadcrumbs" priority="50">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<t t-set="rating_page" t-value="page_name == 'rating'"/>
|
||||
<li t-if="page_name == 'ticket' or ticket or rating_page" t-attf-class="breadcrumb-item #{'active ' if not ticket else ''}">
|
||||
<a t-if="ticket or rating_page" t-attf-href="/my/tickets?{{ keep_query() }}">Tickets</a>
|
||||
<t t-else="">Tickets</t>
|
||||
</li>
|
||||
<li t-if="ticket" class="breadcrumb-item active">
|
||||
#<span t-field="ticket.id"/>
|
||||
</li>
|
||||
<li t-if="rating_page" t-attf-class="breadcrumb-item active">
|
||||
Our Ratings
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_home_odex25_helpdesk_ticket" name="Show Tickets" customize_show="True" inherit_id="portal.portal_my_home" priority="50">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Tickets</t>
|
||||
<t t-set="url" t-value="'/my/tickets'"/>
|
||||
<t t-set="placeholder_count" t-value="'ticket_count'"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_odex25_helpdesk_ticket" name="Helpdesk Ticket">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Tickets</t>
|
||||
</t>
|
||||
<div t-if="not grouped_tickets" class="alert alert-info">
|
||||
There are currently no Ticket for your account.
|
||||
</div>
|
||||
<t t-else="">
|
||||
<t t-call="portal.portal_table">
|
||||
<t t-foreach="grouped_tickets" t-as="tickets">
|
||||
<thead>
|
||||
<tr t-attf-class="{{'thead-light' if not groupby == 'none' else ''}}">
|
||||
<th class="text-left">Ref</th>
|
||||
<th t-if="groupby == 'none'" class="w-100">Description</th>
|
||||
<th t-else="">
|
||||
<em class="font-weight-normal text-muted">Tickets in stage:</em>
|
||||
<span t-field="tickets[0].stage_id.name"/></th>
|
||||
<th/>
|
||||
<th t-if="groupby != 'stage'" class="text-center">Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="tickets" t-as="ticket">
|
||||
<tr>
|
||||
<td class="text-left"><a t-attf-href="/odex25_helpdesk/ticket/#{ticket.id}"><small>#</small><t t-esc="ticket.id"/></a></td>
|
||||
<td><a t-attf-href="/odex25_helpdesk/ticket/#{ticket.id}"><span t-field="ticket.name"/></a></td><td/>
|
||||
<td t-if="groupby != 'stage'" class="text-center"><span class="badge badge-pill badge-info" t-field="ticket.stage_id.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="tickets_followup" name="Helpdesk Tickets">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="wrapwrap_classes" t-value="'o_portal_bg_dark'"/>
|
||||
|
||||
<t t-set="o_portal_fullwidth_alert" groups="odex25_helpdesk.group_odex25_helpdesk_user">
|
||||
<t t-call="portal.portal_back_in_edit_mode">
|
||||
<t t-set="backend_url" t-value="'/web#model=odex25_helpdesk.ticket&id=%s&view_type=form' % (ticket.id)"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md">
|
||||
<h5 class="d-flex mb-1 mb-md-0">
|
||||
<div class="col-9 text-truncate">
|
||||
<span t-field="ticket.name"/>
|
||||
<small class="text-muted "> (#<span t-field="ticket.id"/>)</small>
|
||||
</div>
|
||||
<div class="col-3 text-right">
|
||||
<small class="text-right">Status:</small>
|
||||
<span t-field="ticket.stage_id.name" class=" badge badge-pill badge-info" title="Current stage of this ticket"/>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<div class="row mb-4">
|
||||
<strong class="col-lg-2">Reported on</strong>
|
||||
<span class="col-lg-10" t-field="ticket.create_date" t-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div class="row mb-4" t-if="ticket.team_id.portal_show_rating">
|
||||
<strong class="col-lg-2">Managed by</strong>
|
||||
<span class="col-lg-10">
|
||||
<a t-attf-href="/odex25_helpdesk/rating/#{ticket.team_id.id}">
|
||||
<span t-field="ticket.team_id.name"/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row mb-4" t-if="ticket.partner_id">
|
||||
<strong class="col-lg-2">Reported by</strong>
|
||||
<div class="col-lg-10">
|
||||
<div class="row">
|
||||
<div class="col flex-grow-0 pr-3">
|
||||
<img t-if="ticket.partner_id.image_1024" class="rounded-circle o_portal_contact_img" t-attf-src="data:image/png;base64,#{ticket.partner_id.image_1024}" alt="Contact"/>
|
||||
<img t-else="" class="rounded-circle o_portal_contact_img" src="/web/static/src/img/user_menu_avatar.png" alt="Contact"/>
|
||||
</div>
|
||||
<div class="col pl-sm-0">
|
||||
<div t-field="ticket.partner_id" t-options='{"widget": "contact", "fields": ["name", "email"], "no_marker": true}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4" t-if="ticket.user_id">
|
||||
<strong class="col-lg-2">Assigned to</strong>
|
||||
<div class="col-lg-10">
|
||||
<div class="row">
|
||||
<div class="col flex-grow-0 pr-3">
|
||||
<img t-if="ticket.user_id.image_1024" class="rounded-circle o_portal_contact_img" t-attf-src="data:image/png;base64,#{ticket.user_id.image_1024}" alt="Contact"/>
|
||||
<img t-else="" class="rounded-circle o_portal_contact_img" src="/web/static/src/img/user_menu_avatar.png" alt="Contact"/>
|
||||
</div>
|
||||
<div class="col pl-sm-0">
|
||||
<div t-field="ticket.user_id" t-options='{"widget": "contact", "fields": ["name", "email"], "no_marker": true}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4" name="description">
|
||||
<strong class="col-lg-2">Description</strong>
|
||||
<div t-if="ticket.description" class="col-lg-10" t-field="ticket.description"/>
|
||||
<div t-else="" class="col-lg-10">
|
||||
<em class="text-muted"><small>No description</small></em>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
|
||||
<div t-if="ticket.team_id.allow_portal_ticket_closing and not ticket.stage_id.is_close and not ticket.closed_by_partner" class="modal" tabindex="-1" role="dialog" id="odex25_helpdesk_ticket_close_modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Close ticket</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you wish to proceed?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a role="button" class="btn btn-primary" t-att-href="'/my/ticket/close/%s/%s' % (ticket.id, ticket.access_token)">Close the ticket</a>
|
||||
<button type="button" class="btn btn-light" data-dismiss="modal">Discard</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="ticket.team_id.allow_portal_ticket_closing and not ticket.stage_id.is_close and not ticket.closed_by_partner" class="text-center mt-5">
|
||||
<button class="btn btn-primary mb-1 pt-1" data-target="#odex25_helpdesk_ticket_close_modal" data-toggle="modal"><small><b>Close this ticket</b></small></button>
|
||||
<p><small>
|
||||
If the issue has been solved, you can close the request.
|
||||
</small></p>
|
||||
</div>
|
||||
|
||||
<div class="o_portal_messages_container mt32">
|
||||
<h4>Message and communication history</h4>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="token" t-value="ticket.access_token"/>
|
||||
<t t-set="object" t-value="ticket"/>
|
||||
<t t-set="pid" t-value="pid"/>
|
||||
<t t-set="hash" t-value="hash"/>
|
||||
<t t-set="disable_composer" t-value="ticket.stage_id.is_close"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Page : Rating of a particular team -->
|
||||
<template id="team_rating_progress_data" name="Ticket Rating Page">
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" t-attf-style="width: #{stats[duration][5]}%;" title="Happy" role="img" aria-label="Happy">
|
||||
<t t-esc="int(stats[duration][5])"/>%
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" t-attf-style="width: #{stats[duration][3]}%;" title="Average" role="img" aria-label="Average">
|
||||
<t t-esc="int(stats[duration][3])"/>%
|
||||
</div>
|
||||
<div class="progress-bar bg-danger" t-attf-style="width: #{stats[duration][1]}%;" title="Bad" role="img" aria-label="Bad">
|
||||
<t t-esc="int(stats[duration][1])"/>%
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="team_rating_data" name="Helpdesk Ticket Rating Page">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h5>Last 7 days</h5>
|
||||
<t t-set="duration" t-value="7"/>
|
||||
<t t-set="stats" t-value="stats"/>
|
||||
<t t-call="odex25_helpdesk.team_rating_progress_data"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5>Last 30 days</h5>
|
||||
<t t-set="duration" t-value="30"/>
|
||||
<t t-set="stats" t-value="stats"/>
|
||||
<t t-call="odex25_helpdesk.team_rating_progress_data"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h5>Last 3 months</h5>
|
||||
<t t-set="duration" t-value="90"/>
|
||||
<t t-set="stats" t-value="stats"/>
|
||||
<t t-call="odex25_helpdesk.team_rating_progress_data"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h5 class="o_page_header">Latest Feedbacks</h5>
|
||||
<t t-foreach="ratings" t-as="rating">
|
||||
<img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" t-att-title="rating.res_name"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="team_rating_page" name="Helpdesk Ticket Rating Page">
|
||||
<t t-set="no_breadcrumbs" t-value="True"/>
|
||||
<t t-call="portal.portal_layout">
|
||||
<h1 class="o_page_header p-4">Our Customer Satisfaction</h1>
|
||||
<t t-if="teams">
|
||||
<div id="wrap" class="pl-4">
|
||||
<t t-foreach="teams" t-as="t">
|
||||
<t t-set="team" t-value="t['team']"/>
|
||||
<t t-set="ratings" t-value="t['ratings']"/>
|
||||
<t t-set="stats" t-value="t['stats']"/>
|
||||
<div class="oe_structure" id="oe_structure_odex25_helpdesk_team_rating_1"/>
|
||||
<div class="container oe_website_rating_team">
|
||||
<h2 t-esc="team.name" class="text-muted" />
|
||||
<div class="row mb32">
|
||||
<div class="col-lg-8">
|
||||
<t t-if="ratings" t-call="odex25_helpdesk.team_rating_data"/>
|
||||
<t t-else="">
|
||||
There are no ratings yet.
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_odex25_helpdesk_team_rating_2"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,635 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<!-- odex25_helpdesk.TEAM -->
|
||||
<record id="odex25_helpdesk_team_view_tree" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.team.tree</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Helpdesk Team" multi_edit="1" sample="1">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name" class="field_name"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_sla_action" model="ir.actions.act_window">
|
||||
<field name="name">SLA Policies</field>
|
||||
<field name="res_model">odex25_helpdesk.sla</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'default_team_id': active_id, 'search_default_team_id': active_id}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No SLA policies found. Let's create one!
|
||||
</p>
|
||||
<p>
|
||||
Make sure tickets are handled in a timely manner by using SLA Policies.
|
||||
<br/>
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="email_template_action_odex25_helpdesk" model="ir.actions.act_window">
|
||||
<field name="name">Templates</field>
|
||||
<field name="res_model">mail.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('team_id', '=', active_id)]</field>
|
||||
<field name="context">{'default_team_id': active_id}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new template
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_team_action" model="ir.actions.act_window">
|
||||
<field name="name">Helpdesk Teams</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No teams found
|
||||
</p>
|
||||
<p>
|
||||
Teams regroup tickets for people sharing the same expertise or from the same area.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_team_view_form" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.team.form</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="team search" class="oe_form_configuration">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box"/>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger"
|
||||
attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title" id='title'>
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1 id="name">
|
||||
<field name="name" placeholder="Helpdesk Team..."/>
|
||||
</h1>
|
||||
</div>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="description" placeholder="Description for customer portal"/>
|
||||
<field name="company_id" groups="base.group_multi_company" required="1"/>
|
||||
<h2>Productivity & Visibility</h2>
|
||||
<div class="row mt16 o_settings_container" id="productivity">
|
||||
<div class="col-lg-6 o_setting_box"
|
||||
title="With random assignation, every user gets the same number of tickets. With balanced assignation, tickets are assigned to the user with the least amount of open tickets.">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="assign_method"/>
|
||||
<div class="text-muted">
|
||||
How to assign newly created tickets to the right person
|
||||
</div>
|
||||
<div>
|
||||
<field name="assign_method" class="mt16 o_light_label" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box" attrs="{'invisible': [('assign_method', '=', 'manual')]}">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="member_ids"/>
|
||||
<div class="text-muted">
|
||||
Individuals to whom the tickets will be automatically assigned. Keep empty for
|
||||
everyone to be part of the team.
|
||||
</div>
|
||||
<div>
|
||||
<field name="member_ids" widget="many2many_tags" options="{'color_field': 'color'}"
|
||||
class="mt16"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="visibility_member_ids"/>
|
||||
<div class="text-muted">
|
||||
Team Members to whom this team will be visible. Keep empty for everyone to see this
|
||||
team.
|
||||
</div>
|
||||
<div>
|
||||
<field name="visibility_member_ids" widget="many2many_tags"
|
||||
options="{'color_field': 'color'}" class="mt16"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Channels</h2>
|
||||
<div class="row mt16 o_settings_container" id="channels">
|
||||
<div class="col-lg-6 o_setting_box" id="alias_channels">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_alias"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_alias"/>
|
||||
<div class="text-muted">
|
||||
Incoming emails create tickets
|
||||
</div>
|
||||
<div attrs="{'invisible': [('use_alias','=',False)]}" class="mt16">
|
||||
<div class="oe_edit_only" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<label for="alias_id" string="Send emails to:"/>
|
||||
<field name="alias_name"/>@
|
||||
<field name="alias_domain" class="oe_inline" readonly="1"/>
|
||||
to create tickets
|
||||
</div>
|
||||
<p class="oe_read_only" attrs="{'invisible': [('alias_domain', '=', False)]}">Send
|
||||
emails to
|
||||
<strong>
|
||||
<field name="alias_id" class="oe_read_only oe_inline" required="False"/>
|
||||
</strong>
|
||||
to create tickets
|
||||
</p>
|
||||
<field name="has_external_mail_server" invisible="1"/>
|
||||
<p class="text-muted"
|
||||
attrs="{'invisible': ['|', ('alias_domain', '!=', False), ('has_external_mail_server', '!=', False)]}">
|
||||
<i class="fa fa-lightbulb-o" role='img'/>
|
||||
Enable the External Email Servers feature in the General Settings and indicate
|
||||
an alias domain
|
||||
</p>
|
||||
<p attrs="{'invisible': [('alias_domain', '!=', False)]}">
|
||||
<button name="%(base_setup.action_general_configuration)d" type="action"
|
||||
string="Configure a custom domain" icon="fa-arrow-right"
|
||||
class="btn-link"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_website_helpdesk_livechat"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_website_helpdesk_livechat"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/livechat/livechat.html"
|
||||
title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Get in touch with your website visitors
|
||||
</div>
|
||||
<div id="im_livechat"
|
||||
attrs="{'invisible': [('use_website_helpdesk_livechat','=',False)]}">
|
||||
<div class="text-warning mb4 mt16">
|
||||
Save this page and refresh to activate the feature.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32 o_settings_container" id="website_form_channel">
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_website_helpdesk_form"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_website_helpdesk_form"/>
|
||||
<div class="text-muted">
|
||||
Submit tickets with an online form
|
||||
</div>
|
||||
<div id='website_form' attrs="{'invisible': [('use_website_helpdesk_form','=',False)]}">
|
||||
<div class="text-warning mb4 mt16">
|
||||
Save this page and refresh to activate the feature.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 o_setting_box" id="api_doc">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_api"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_api"/>
|
||||
<div class="text-muted">
|
||||
Connect third party application and create tickets using web services
|
||||
</div>
|
||||
<div attrs="{'invisible': [('use_api','=',False)]}" class="mt16">
|
||||
<!-- TODO: write a dedicated doc on how to create helpdesk tickets -->
|
||||
<a href="https://www.odoo.com/documentation/14.0/developer/api/external_api.html"
|
||||
target="_blank">
|
||||
<i class="fa fa-arrow-right"></i>
|
||||
View documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mt32">Sell & Track Hours</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-lg-6 o_setting_box" id="timesheet">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_odex25_helpdesk_timesheet"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_odex25_helpdesk_timesheet"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/helpdesk/invoice_time.html"
|
||||
title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Record timesheets on your tickets
|
||||
</div>
|
||||
<div id='odex25_helpdesk_timesheet'
|
||||
attrs="{'invisible': [('use_odex25_helpdesk_timesheet', '=', False)]}">
|
||||
<div class="text-warning mb4 mt16">
|
||||
Save this page and refresh to activate the feature
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box" id="sale_timesheet"
|
||||
attrs="{'invisible': [('use_odex25_helpdesk_timesheet', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_odex25_helpdesk_sale_timesheet"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_odex25_helpdesk_sale_timesheet"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/helpdesk/reinvoice_from_project.html"
|
||||
title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Reinvoice time to your customer through tasks
|
||||
</div>
|
||||
<div id='odex25_helpdesk_sale_timesheet'
|
||||
attrs="{'invisible': [('use_odex25_helpdesk_sale_timesheet', '=', False)]}">
|
||||
<div class="text-warning mb4 mt16">
|
||||
Save this page and refresh to activate the feature
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Performance</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-lg-6 o_setting_box" id="sla">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_sla"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_sla"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/helpdesk/getting_started.html"
|
||||
title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Set up your Service Level Agreements to track performance
|
||||
</div>
|
||||
<div attrs="{'invisible': [('use_sla','=',False)]}" class="mt16">
|
||||
<button name="%(odex25_helpdesk_sla_action)d" type="action"
|
||||
string="Configure SLA Policies" icon="fa-arrow-right" class="btn-link"/>
|
||||
</div>
|
||||
<div attrs="{'invisible': [('use_sla', '=', False)]}" class="mt16">
|
||||
<label for="resource_calendar_id"/>
|
||||
<div class="text-muted">
|
||||
Set the calendar used to compute SLA target
|
||||
</div>
|
||||
<div class="mt16">
|
||||
<field name="resource_calendar_id"
|
||||
attrs="{'required': [('use_sla', '=', True)]}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box" id="rating">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_rating"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_rating"/>
|
||||
<div class="text-muted">
|
||||
Allow your customers to easily rate your services. Activate this option will add a
|
||||
default email template on non folded closing stages
|
||||
</div>
|
||||
<div id="use_rating" attrs="{'invisible': [('use_rating', '=', False)]}">
|
||||
<field name="rating_percentage_satisfaction" invisible="1"/>
|
||||
<div attrs="{'invisible': [('use_rating', '=', True), ('rating_percentage_satisfaction', '!=', -1)]}"
|
||||
class="mt16">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="website_rating" class="col-lg-6 o_setting_box"
|
||||
attrs="{'invisible': [('use_rating', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="portal_show_rating"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="portal_show_rating"/>
|
||||
<div class="text-muted">
|
||||
Publish this team's ratings on your website
|
||||
</div>
|
||||
<div attrs="{'invisible': ['|', ('portal_show_rating', '=', False), ('portal_rating_url','=',False)]}"
|
||||
class="mt16">
|
||||
<button class="btn-link" role="button" icon="fa-arrow-right">
|
||||
<field name="portal_rating_url" nolabel="1" readonly="1" class="oe_inline"
|
||||
widget="url" text="View this team's ratings"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Self-Service</h2>
|
||||
<div class="row mt16 o_settings_container" id="self-Service">
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_website_helpdesk_forum"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_website_helpdesk_forum"/>
|
||||
<div class="text-muted">
|
||||
Question and answer section on your website
|
||||
</div>
|
||||
<div id="use_website_helpdesk_forum"
|
||||
attrs="{'invisible': [('use_website_helpdesk_forum', '=', False)]}">
|
||||
<div class="text-warning mb4 mt16">
|
||||
Save this page and refresh to activate the feature.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="use_website_helpdesk_slides"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="use_website_helpdesk_slides" string="eLearning"/>
|
||||
<div class="text-muted">
|
||||
Share presentation and videos, and organize into courses
|
||||
</div>
|
||||
<div id="use_website_helpdesk_slides"
|
||||
attrs="{'invisible': [('use_website_helpdesk_slides', '=', False)]}">
|
||||
<div class="text-warning mb4 mt16" id="o_slide_option">
|
||||
Save this page and refresh to activate the feature.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="allow_portal_ticket_closing"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="allow_portal_ticket_closing"/>
|
||||
<a href="https://www.odoo.com/documentation/user/13.0/helpdesk/close_tickets.html"
|
||||
title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Allow customers to close their tickets
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h2>After Sales-->
|
||||
<!-- <a href="https://www.odoo.com/documentation/user/13.0/helpdesk/after_sales.html"-->
|
||||
<!-- title="Documentation" class="o_doc_link" target="_blank"></a>-->
|
||||
<!-- </h2>-->
|
||||
<!-- <div class="row mt32 o_settings_container" id="after-sales">-->
|
||||
<!-- <div class="col-lg-6 o_setting_box">-->
|
||||
<!-- <div class="o_setting_left_pane">-->
|
||||
<!-- <field name="use_credit_notes"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="o_setting_right_pane">-->
|
||||
<!-- <label for="use_credit_notes"/>-->
|
||||
<!-- <div class="text-muted">-->
|
||||
<!-- Generate credit notes from tickets-->
|
||||
<!-- </div>-->
|
||||
<!-- <div id="use_credit_notes" attrs="{'invisible': [('use_credit_notes', '=', False)]}">-->
|
||||
<!-- <div class="text-warning mb4 mt16">-->
|
||||
<!-- Save this page and refresh to activate the feature.-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-lg-6 o_setting_box">-->
|
||||
<!-- <div class="o_setting_left_pane">-->
|
||||
<!-- <field name="use_coupons"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="o_setting_right_pane">-->
|
||||
<!-- <label for="use_coupons"/>-->
|
||||
<!-- <div class="text-muted">-->
|
||||
<!-- Grant coupons from tickets-->
|
||||
<!-- </div>-->
|
||||
<!-- <div id="use_coupons" attrs="{'invisible': [('use_coupons', '=', False)]}">-->
|
||||
<!-- <div class="text-warning mb4 mt16">-->
|
||||
<!-- Save this page and refresh to activate the feature.-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-lg-6 o_setting_box">-->
|
||||
<!-- <div class="o_setting_left_pane">-->
|
||||
<!-- <field name="use_product_returns"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="o_setting_right_pane">-->
|
||||
<!-- <label for="use_product_returns"/>-->
|
||||
<!-- <div class="text-muted">-->
|
||||
<!-- Allow product returns from tickets-->
|
||||
<!-- </div>-->
|
||||
<!-- <div id="use_product_returns"-->
|
||||
<!-- attrs="{'invisible': [('use_product_returns', '=', False)]}">-->
|
||||
<!-- <div class="text-warning mb4 mt16">-->
|
||||
<!-- Save this page and refresh to activate the feature.-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="col-lg-6 o_setting_box">-->
|
||||
<!-- <div class="o_setting_left_pane">-->
|
||||
<!-- <field name="use_product_repairs"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="o_setting_right_pane">-->
|
||||
<!-- <label for="use_product_repairs"/>-->
|
||||
<!-- <div class="text-muted">-->
|
||||
<!-- Repair broken products-->
|
||||
<!-- </div>-->
|
||||
<!-- <div id="use_product_repairs"-->
|
||||
<!-- attrs="{'invisible': [('use_product_repairs', '=', False)]}">-->
|
||||
<!-- <div class="text-warning mb4 mt16">-->
|
||||
<!-- Save this page and refresh to activate the feature.-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="oe_chatter" groups="base.group_user">
|
||||
<field name="message_follower_ids"
|
||||
help="Follow this team to automatically track the events associated to tickets of this team."/>
|
||||
</div>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="odex25_helpdesk_team_menu" name="Helpdesk Teams" action="odex25_helpdesk_team_action"
|
||||
sequence="0" parent="odex25_helpdesk.odex25_helpdesk_menu_config"
|
||||
groups="odex25_helpdesk.group_odex25_helpdesk_manager"/>
|
||||
|
||||
<record id="odex25_helpdesk_team_view_search" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.team.search</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Team Search">
|
||||
<field name="name"/>
|
||||
<filter string="Archived" domain="[('active', '=', False)]" name="archived"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_team_view_kanban" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.team.dashboard</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="priority">200</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="oe_background_grey o_kanban_dashboard o_odex25_helpdesk_kanban p-0" create="0"
|
||||
js_class="odex25_helpdesk_dashboard">
|
||||
<field name="name"/>
|
||||
<field name="color"/>
|
||||
<field name="use_alias"/>
|
||||
<field name="alias_name"/>
|
||||
<field name="alias_domain"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="use_rating"/>
|
||||
<field name="rating_percentage_satisfaction" invisible="1"/>
|
||||
<field name="use_sla"/>
|
||||
<field name="upcoming_sla_fail_tickets"/>
|
||||
<field name="unassigned_tickets"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="#{!selection_mode ? kanban_color(record.color.raw_value) : ''}">
|
||||
<span class="oe_kanban_color_help"
|
||||
t-attf-title="In #{kanban_getcolorname(record.color.raw_value)}" role="img"
|
||||
t-attf-aria-label="In #{kanban_getcolorname(record.color.raw_value)}"/>
|
||||
<div t-attf-class="o_kanban_card_header">
|
||||
<div class="o_kanban_card_header_title">
|
||||
<div class="o_primary">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<small t-if="record.use_alias.value and record.alias_name.value and record.alias_domain.value">
|
||||
<i class="fa fa-envelope-o" title="Domain alias" role="img"
|
||||
aria-label="Domain alias"></i>&nbsp;
|
||||
<t t-esc="record.alias_id.value"/>
|
||||
</small>
|
||||
</div>
|
||||
<div class="o_kanban_manage_button_section" t-if="!selection_mode"
|
||||
groups="odex25_helpdesk.group_odex25_helpdesk_manager">
|
||||
<a class="o_kanban_manage_toggle_button" href="#">
|
||||
<i class="fa fa-ellipsis-v" role="img" aria-label="Manage" title="Manage"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container o_kanban_card_content" t-if="!selection_mode">
|
||||
<div class="row">
|
||||
<div class="col-6 o_kanban_primary_left">
|
||||
<button class="btn btn-primary" name="action_view_ticket" type="object">
|
||||
Tickets
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 o_kanban_primary_right">
|
||||
<div class="mb4" groups="odex25_helpdesk.group_use_sla"
|
||||
attrs="{'invisible': [('use_sla', '=', False)]}">
|
||||
<a name="%(odex25_helpdesk.action_upcoming_sla_fail_all_tickets)d"
|
||||
type="action"
|
||||
context="{'search_default_team_id': active_id, 'default_team_id': active_id}">
|
||||
<t t-esc="record.upcoming_sla_fail_tickets.raw_value"/>
|
||||
SLA Issues
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb4">
|
||||
<a name="%(odex25_helpdesk.odex25_helpdesk_ticket_action_unassigned)d"
|
||||
type="action"
|
||||
context="{'search_default_team_id': active_id, 'default_team_id': active_id}">
|
||||
<t t-esc="record.unassigned_tickets.raw_value"/>
|
||||
Unassigned Tickets
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb4" t-if="record.use_rating.raw_value">
|
||||
<a name="action_view_all_rating" type="object">See Customer Satisfaction</a>
|
||||
</div>
|
||||
<div class="mb4" t-if="record.use_sla.raw_value">
|
||||
<a name="%(odex25_helpdesk_sla_action_main)d" type="action"
|
||||
context="{'search_default_team_id': active_id, 'default_team_id': active_id}">
|
||||
See SLAs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- Smiley indicator of rating:
|
||||
<div t-if="record.use_rating.raw_value and record.rating_percentage_satisfaction.raw_value >= 0" class="row text-center">
|
||||
<a name="action_view_all_rating" type="object" title="Percentage of happy people about this team" class="float-right">
|
||||
<h5 t-attf-class="badge #{record.rating_percentage_satisfaction.raw_value >= 50 ? 'badge-success' : 'badge-warning'}">
|
||||
<i t-attf-class="fa #{record.rating_percentage_satisfaction.raw_value >= 50 ? 'fa-smile-o' : 'fa-frown-o'}" role="img" aria-label="Satisfaction rate" title="Satisfaction rate"/> <t t-raw="record.rating_percentage_satisfaction.raw_value"/> %
|
||||
</h5>
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="container o_kanban_card_manage_pane dropdown-menu" role="menu">
|
||||
<div class="row" >
|
||||
<div class="col-xs-6 o_kanban_card_manage_section o_kanban_manage_view" invisible="1">
|
||||
<div class="o_kanban_card_manage_title">
|
||||
<span>View Tickets</span>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(odex25_helpdesk.helpdesk_ticket_action_Archived)d" type="action">
|
||||
Archived
|
||||
</a>
|
||||
</div>
|
||||
<div name="sla_failed" attrs="{'invisible': [('use_sla', '=', False)]}"
|
||||
groups="odex25_helpdesk.group_use_sla">
|
||||
<a name="%(odex25_helpdesk.helpdesk_ticket_action_slafailed)d"
|
||||
type="action">SLA
|
||||
Failed
|
||||
</a>
|
||||
</div>
|
||||
<div t-if="record.use_rating.raw_value">
|
||||
<a name="action_unhappy_rating_ticket" type="object">Not Satisfied</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 o_kanban_card_manage_section o_kanban_manage_reports" invisible="1">
|
||||
<div class="o_kanban_card_manage_title">
|
||||
<span>Reporting</span>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(odex25_helpdesk.odex25_helpdesk_ticket_team_analysis_action)d"
|
||||
type="action">
|
||||
Opened Tickets Analysis
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a name="%(odex25_helpdesk.odex25_helpdesk_ticket_action_team_performance)d"
|
||||
type="action">Performance Analysis
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="widget.editable" class="o_kanban_card_manage_settings row">
|
||||
<div role="menuitem" aria-haspopup="true" class="col-8">
|
||||
<ul class="oe_kanban_colorpicker" data-field="color" role="menu"/>
|
||||
</div>
|
||||
<div role="menuitem" class="col-4 text-right">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_team_dashboard_action_main" model="ir.actions.act_window">
|
||||
<field name="name">Helpdesk Overview</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="view_id" ref="odex25_helpdesk.odex25_helpdesk_team_view_kanban"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No teams found
|
||||
</p>
|
||||
<p>
|
||||
Teams regroup tickets for people sharing the same expertise or from the same area.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="odex25_helpdesk_menu_team_dashboard"
|
||||
action="odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main"
|
||||
sequence="5" parent="odex25_helpdesk.menu_odex25_helpdesk_root" name="Overview"
|
||||
groups="odex25_helpdesk.group_odex25_helpdesk_user"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_form_inherit_odex25_helpdesk" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.odex25_helpdesk</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_user'))]"/>
|
||||
<field name="priority" eval="8"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="object"
|
||||
name="action_open_odex25_helpdesk_ticket" context="{'default_partner_id': active_id}" icon="fa-life-ring" attrs="{'invisible': [('ticket_count', '=', 0)]}">
|
||||
<div class="o_stat_info">
|
||||
<field name="ticket_count" class="o_stat_value"/>
|
||||
<span class="o_stat_text"> Tickets</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="service_category_form" model="ir.ui.view">
|
||||
<field name="name">service.category.form</field>
|
||||
<field name="model">service.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name" class="oe_inline"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="service_category_tree" model="ir.ui.view">
|
||||
<field name="name">service.category.tree</field>
|
||||
<field name="model">service.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="odex25_helpdesk_category_action" model="ir.actions.act_window">
|
||||
<field name="name">Service Category</field>
|
||||
<field name="res_model">service.category</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Service Category found. Let's create one!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="service_category_action" model="ir.actions.act_window">-->
|
||||
<!-- <field name="name">Service Category</field>-->
|
||||
<!-- <field name="res_model">service.category</field>-->
|
||||
<!-- <field name="view_type">form</field>-->
|
||||
<!-- <field name="view_id" ref="service_category_tree"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<menuitem id="odex25_helpdesk_menu_service_category"
|
||||
action="odex25_helpdesk_category_action"
|
||||
sequence="100" parent="odex25_helpdesk.odex25_helpdesk_menu_config"
|
||||
groups="odex25_helpdesk.group_odex25_helpdesk_manager"/>
|
||||
|
||||
|
||||
<!-- <menuitem-->
|
||||
<!-- id="menu_service_category"-->
|
||||
<!-- action="service_category_action"-->
|
||||
<!-- parent="odex25_helpdesk.odex25_helpdesk_menu_config"-->
|
||||
<!-- sequence="10"/>-->
|
||||
|
||||
<!--
|
||||
HelpdeskService views & action & menu
|
||||
-->
|
||||
<record id="helpdesk_service_form" model="ir.ui.view">
|
||||
<field name="name">helpdesk.service.form</field>
|
||||
<field name="model">helpdesk.service</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name" class="oe_inline"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="category_id"/>
|
||||
<field name="priority" widget="priority"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_service_tree" model="ir.ui.view">
|
||||
<field name="name">helpdesk.service.tree</field>
|
||||
<field name="model">helpdesk.service</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_service_action" model="ir.actions.act_window">
|
||||
<field name="name">Helpdesk Service</field>
|
||||
<field name="res_model">helpdesk.service</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_service_action" model="ir.actions.act_window">
|
||||
<field name="name">Helpdesk Service</field>
|
||||
<field name="res_model">helpdesk.service</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No Service found. Let's create one!
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="odex25_helpdesk_menu_service"
|
||||
action="odex25_helpdesk_service_action"
|
||||
sequence="100" parent="odex25_helpdesk.odex25_helpdesk_menu_config"
|
||||
groups="odex25_helpdesk.group_odex25_helpdesk_manager"/>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'EXP Helpdesk Assignation methods',
|
||||
'summary': 'Ticket Assignation methods for team members considering ticket types',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': ['odex25_helpdesk_security'],
|
||||
'description': """
|
||||
Ticket Assignation methods for team members considering ticket types
|
||||
""",
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/helpdesk_views.xml',
|
||||
],
|
||||
'license': '',
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_helpdesk_assignation_method
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-21 11:37+0000\n"
|
||||
"PO-Revision-Date: 2022-09-21 11:37+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model,name:odex25_helpdesk_assignation_method.model_odex25_helpdesk_team
|
||||
msgid "Helpdesk Team"
|
||||
msgstr "فريق مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model,name:odex25_helpdesk_assignation_method.model_odex25_helpdesk_ticket
|
||||
msgid "Helpdesk Ticket"
|
||||
msgstr "تذكرة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method
|
||||
msgid "How to assign newly created tickets to the right person"
|
||||
msgstr "كيفية تعيين التذاكر التي تم إنشاؤها حديثًا إلى الشخص المناسب"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method
|
||||
msgid "Keep empty for everyone to see this team"
|
||||
msgstr "ابق فارغًا حتى يرى الجميع هذا الفريق"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__member_id
|
||||
msgid "Member"
|
||||
msgstr "العضو"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__members_ids
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method
|
||||
msgid "Members"
|
||||
msgstr "الأعضاء"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__team_id
|
||||
msgid "Team"
|
||||
msgstr "فريق"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__team_leader_id
|
||||
msgid "Team Leader"
|
||||
msgstr "قائد الفريق"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__member_ids
|
||||
msgid "Team Members"
|
||||
msgstr "أعضاء الفريق"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__ticket_type_ids
|
||||
msgid "Ticket Type"
|
||||
msgstr "نوع التذكرة"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method
|
||||
msgid ""
|
||||
"With random assignation, every user gets the same number of tickets. With "
|
||||
"balanced assignation, tickets are assigned to the user with the least amount"
|
||||
" of open tickets."
|
||||
msgstr "مع التخصيص العشوائي ، يحصل كل مستخدم على نفس عدد التذاكر. مع التوزيع المتوازن ، يتم تخصيص التذاكر للمستخدم بأقل عدد من التذاكر المفتوحة"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: code:addons/odex25_helpdesk_assignation_method/models/helpdesk_team.py:0
|
||||
#, python-format
|
||||
msgid "You must have team members assigned to change the assignation method."
|
||||
msgstr "يجب أن يكون لديك أعضاء فريق معينين لتغيير طريقة التعيين"
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model,name:odex25_helpdesk_assignation_method.model_helpdesk_team_member
|
||||
msgid "helpdesk.team.member"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_assignation_method
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__service_id
|
||||
msgid "Service"
|
||||
msgstr "الخدمة"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import helpdesk_team
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = "odex25_helpdesk.ticket"
|
||||
|
||||
team_leader_id = fields.Many2one(related='team_id.team_leader_id')
|
||||
|
||||
@api.onchange('service_id')
|
||||
def get_user_and_assign_it(self):
|
||||
for rec in self:
|
||||
result = rec._onchange_ticket_type_values(rec.team_id,rec.service_id)
|
||||
rec.user_id = result['user_id']
|
||||
|
||||
def _onchange_ticket_type_values(self, team, ticket_type=None):
|
||||
return {
|
||||
'user_id': team.get_new_user(ticket_type),
|
||||
}
|
||||
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super(HelpdeskTicket, self).create(vals)
|
||||
result = self._onchange_ticket_type_values(res.team_id,res.service_id)
|
||||
res.user_id = result['user_id']
|
||||
return res
|
||||
|
||||
class HelpdeskTeam(models.Model):
|
||||
_inherit = "odex25_helpdesk.team"
|
||||
|
||||
team_leader_id = fields.Many2one('res.users',domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id)])
|
||||
members_ids = fields.One2many('helpdesk.team.member','team_id')
|
||||
member_ids = fields.Many2many('res.users', string='Team Members', domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)])
|
||||
|
||||
@api.onchange('members_ids')
|
||||
def _onchange_members_ids(self):
|
||||
"""
|
||||
update the member_ids based on members_ids
|
||||
"""
|
||||
for team in self:
|
||||
for rec in team.member_ids:
|
||||
rec = False
|
||||
members = [member.member_id.id for member in self.members_ids]
|
||||
addmember = [(6,0,members)]
|
||||
team.update({
|
||||
'member_ids': addmember
|
||||
})
|
||||
|
||||
@api.constrains('assign_method', 'members_ids')
|
||||
def _check_members_assignation(self):
|
||||
if not self.members_ids and self.assign_method != 'manual':
|
||||
raise ValidationError(_("You must have team members assigned to change the assignation method."))
|
||||
|
||||
def get_new_user(self, ticket_type=None):
|
||||
for rec in self:
|
||||
rec.ensure_one()
|
||||
new_user = self.env['res.users']
|
||||
members_ids = []
|
||||
if ticket_type:
|
||||
for member in rec.members_ids:
|
||||
if ticket_type in member.service_id:
|
||||
members_ids.append(member.member_id.id)
|
||||
members_ids = sorted(members_ids)
|
||||
elif len(members_ids) == 0:
|
||||
members_ids = sorted([member.member_id.id for member in rec.members_ids])
|
||||
if members_ids:
|
||||
if rec.assign_method == 'randomly':
|
||||
# randomly means new ticketss get uniformly distributed
|
||||
previous_assigned_user = self.env['odex25_helpdesk.ticket'].search([('team_id', '=', rec.id)], order='create_date desc', limit=1).user_id
|
||||
# handle the case where the previous_assigned_user has left the team (or there is none).
|
||||
if previous_assigned_user and previous_assigned_user.id in members_ids:
|
||||
previous_index = members_ids.index(previous_assigned_user.id)
|
||||
new_user = new_user.browse(members_ids[(previous_index + 1) % len(members_ids)])
|
||||
else:
|
||||
new_user = new_user.browse(members_ids[0])
|
||||
elif rec.assign_method == 'balanced':
|
||||
read_group_res = self.env['odex25_helpdesk.ticket'].read_group([('stage_id.is_close', '=', False), ('user_id', 'in', members_ids)], ['user_id'], ['user_id'])
|
||||
# add all the members in case a member has no more open tickets (and thus doesn't appear in the previous read_group)
|
||||
count_dict = dict((m_id, 0) for m_id in members_ids)
|
||||
count_dict.update((data['user_id'][0], data['user_id_count']) for data in read_group_res)
|
||||
new_user = new_user.browse(min(count_dict, key=count_dict.get))
|
||||
return new_user
|
||||
|
||||
|
||||
class HelpdeskTeamMemebers(models.Model):
|
||||
_name = "helpdesk.team.member"
|
||||
|
||||
team_id = fields.Many2one('odex25_helpdesk.team')
|
||||
member_id = fields.Many2one('res.users',domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)])
|
||||
# ticket_type_ids = fields.Many2many('odex25_helpdesk.ticket.type')
|
||||
service_id = fields.Many2many('helpdesk.service')
|
||||
|
||||
#
|
||||
#
|
||||
# class InheritUser(models.Model):
|
||||
# _inherit = 'res.users'
|
||||
#
|
||||
# @api.model
|
||||
# def name_search(self, name='', args=None, operator='ilike', limit=100):
|
||||
# if self._context.get('members', []):
|
||||
# member_id = self.env['odex25_helpdesk.team'].new('members_ids',self._context.get('members', []))
|
||||
# args.append(('id', 'not in',
|
||||
# [isinstance(d['member_id'], tuple) and d['member_id'][0] or d['member_id']
|
||||
# for d in member_id]))
|
||||
# return super(InheritUser, self).name_search(name, args=args, operator=operator, limit=limit)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_helpdesk_team_member,helpdesk.team.member,odex25_helpdesk_assignation_method.model_helpdesk_team_member,,1,1,1,1
|
||||
|
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1,83 @@
|
|||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan"
|
||||
style="text-align: center;font-size: 25px;font-weight: 600;margin: 0px !important;color:#145374;">
|
||||
ONE OF ODEX MODULES</h2>
|
||||
<h6 class="oe_slogan" style="text-align: center;font-size: 18px;">
|
||||
ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system
|
||||
<br/>
|
||||
.to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one
|
||||
solution
|
||||
</h6>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container" style="padding: 1% 0% 0% 3%;">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan"
|
||||
style="text-align: center;font-size: 25px;font-weight: 600;margin: 0px !important;color:#145374;">
|
||||
Contact Us
|
||||
</h2>
|
||||
<br/>
|
||||
<br/>
|
||||
<div style="display:flex;padding-top: 20px;justify-content: space-between;">
|
||||
<div style="flex-basis: 18%;"></div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://exp-sa.com" target="_blank">
|
||||
<img src="internet.png" style="width: 100%;border-radius: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://exp-sa.com/" target="_blank">
|
||||
www.exp-sa.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://twitter.com/expcosa/" target="_blank">
|
||||
<img src="twitter.png" style="width: 100%;border-radius: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://twitter.com/expcosa/" target="_blank">
|
||||
exposa
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://www.linkedin.com/in/expert-company-52b5b812b/" target="_blank">
|
||||
<img src="linkedin.png" style="width: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://www.linkedin.com/in/expert-company-52b5b812b/" target="_blank">
|
||||
exposa
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="http://info@exp-sa.com/" target="_blank">
|
||||
<img src="mail.png" style="width: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="http://info@exp-sa.com/" target="_blank">
|
||||
Info@exp-sa.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container oe_separator">
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_team_view_form_assignation_method" model="ir.ui.view">
|
||||
<field name="name">helpdesk.team.form.inherit.assignation.method</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_team_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='productivity']" position="replace">
|
||||
<div class="row mt16 o_settings_container" id="productivity">
|
||||
<div class="row mt16 o_settings_container" id="productivity">
|
||||
<div class="col-md-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="team_leader_id"/>
|
||||
<div class="text-muted">
|
||||
Keep empty for everyone to see this team
|
||||
</div>
|
||||
<div>
|
||||
<field name="team_leader_id" class="mt16"/>
|
||||
<field name="member_ids" invisible="1" class="mt16"/>
|
||||
<group>
|
||||
<field name="is_internal_team"/>
|
||||
<field name="is_vip_team"/>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 o_setting_box" title="With random assignation, every user gets the same number of tickets. With balanced assignation, tickets are assigned to the user with the least amount of open tickets.">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="assign_method"/>
|
||||
<div class="text-muted">
|
||||
How to assign newly created tickets to the right person
|
||||
</div>
|
||||
<div>
|
||||
<field name="assign_method" class="mt16"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<field name="members_ids">
|
||||
<tree string="Members" editable="bottom">
|
||||
<field name="member_id" required="1" context="{'members':parent.members_ids}"/>
|
||||
<!-- <field name="ticket_type_ids" required="1" widget="many2many_tags"/>-->
|
||||
<field name="service_id" required="1" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'EXP Helpdesk Reopen',
|
||||
'summary': 'adding reopen feature to helpdesk',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': ['odex25_helpdesk'],
|
||||
'description': """
|
||||
ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system
|
||||
.to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one solution
|
||||
""",
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'views/helpdesk_views.xml',
|
||||
'views/cron_repair.xml',
|
||||
],
|
||||
'license': '',
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_helpdesk_reopen
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-20 13:31+0000\n"
|
||||
"PO-Revision-Date: 2023-08-20 13:31+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_auto_close
|
||||
msgid "Auto Close Kanban Stage"
|
||||
msgstr "مرحلة الإغلاق التلقائي"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__close_time
|
||||
msgid "Close Time"
|
||||
msgstr "زمن الإقفال"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.actions.server,name:odex25_helpdesk_reopen.ir_cron_automatic_done_state_ir_actions_server
|
||||
#: model:ir.cron,cron_name:odex25_helpdesk_reopen.ir_cron_automatic_done_state
|
||||
#: model:ir.cron,name:odex25_helpdesk_reopen.ir_cron_automatic_done_state
|
||||
msgid "Help Desk Cancel ;Automatic Done State"
|
||||
msgstr "إلغاء مكتب المساعدة ؛ حالة تم تلقائيًا"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model,name:odex25_helpdesk_reopen.model_odex25_helpdesk_stage
|
||||
msgid "Helpdesk Stage"
|
||||
msgstr "مرحلة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model,name:odex25_helpdesk_reopen.model_odex25_helpdesk_ticket
|
||||
msgid "Helpdesk Ticket"
|
||||
msgstr "تذكرة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_reopen
|
||||
msgid "Re-open Kanban Stage"
|
||||
msgstr "أعد فتح مرحلة كانبان"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__reopen_time
|
||||
msgid "Reopen Time"
|
||||
msgstr "وقت إعادة الفتح"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_auto_close
|
||||
msgid ""
|
||||
"in this stage the ticket will be closed again if the customer not send email"
|
||||
msgstr "في هذه المرحلة ، سيتم إغلاق التذكرة مرة أخرى إذا لم يرسل العميل بريدًا إلكترونيًا"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_reopen
|
||||
msgid ""
|
||||
"in this stage the ticket will be opened again if the customerreplied to your"
|
||||
" close stage email"
|
||||
msgstr ""
|
||||
"في هذه المرحلة ، سيتم فتح التذكرة مرة أخرى إذا قام العميل بالرد على البريد "
|
||||
"الإلكتروني الخاص بك"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__close_time
|
||||
msgid "the time that make a ticket close task"
|
||||
msgstr "الوقت الذي يجعل مهمة مغلقة"
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__reopen_time
|
||||
msgid "the time that make a ticket reopen task"
|
||||
msgstr "الوقت الذي يجعل مهمة إعادة فتح تذكرة"
|
||||
|
||||
|
||||
#. module: odex25_helpdesk_reopen
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_done
|
||||
msgid "Close Ticket"
|
||||
msgstr "إغلاق التذكرة"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import helpdesk
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
from odoo import api, fields, models, tools, _
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HelpdeskStage(models.Model):
|
||||
_inherit = 'odex25_helpdesk.stage'
|
||||
|
||||
is_reopen = fields.Boolean('Re-open Kanban Stage', help="in this stage the ticket will be opened again if the "
|
||||
"customer""replied to your close stage email")
|
||||
reopen_time = fields.Float(string="Reopen Time", help="the time that make a ticket reopen task")
|
||||
is_auto_close = fields.Boolean('Auto Close Kanban Stage',help="in this stage the ticket will be closed again if "
|
||||
"the customer not send email")
|
||||
close_time = fields.Float(string="Close Time", help="the time that make a ticket close task")
|
||||
is_done = fields.Boolean(string="Close Ticket")
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = 'odex25_helpdesk.ticket'
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if self.partner_email and self.partner_id and not self.partner_id.email:
|
||||
self.partner_id.email = self.partner_email
|
||||
|
||||
if self.partner_email and not self.partner_id:
|
||||
# we consider that posting a message with a specified recipient (not a follower, a specific one)
|
||||
# on a document without customer means that it was created through the chatter using
|
||||
# suggested recipients. This heuristic allows to avoid ugly hacks in JS.
|
||||
new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email)
|
||||
if new_partner:
|
||||
self.search([
|
||||
('partner_id', '=', False),
|
||||
('partner_email', '=', new_partner.email),
|
||||
('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id})
|
||||
if message.author_id:
|
||||
if message.author_id.email == self.partner_email and self.stage_id.is_close:
|
||||
previous = self.env['mail.message'].search([
|
||||
('res_id', '=', self.id),
|
||||
('subject', '!=', ''),
|
||||
('model', '=', 'odex25_helpdesk.ticket'),
|
||||
('author_id', '!=', self.partner_id.id),
|
||||
])
|
||||
if len(previous) != 0 and previous[0]:
|
||||
previous = previous[0]
|
||||
for stage in self.team_id.stage_ids:
|
||||
if stage.is_reopen:
|
||||
difference = fields.Datetime.from_string(message.date) - fields.Datetime.from_string(
|
||||
previous.date)
|
||||
difference = str(difference).split(':')
|
||||
hour, minute = divmod(stage.reopen_time, 1)
|
||||
minute *= 60
|
||||
result = '{}:{}'.format(int(hour), int(minute))
|
||||
result = result.split(':')
|
||||
if int(difference[0]) < int(result[0]):
|
||||
self.stage_id = stage.id
|
||||
break
|
||||
elif int(difference[1]) <= int(result[1]):
|
||||
self.stage_id = stage.id
|
||||
|
||||
return super(HelpdeskTicket, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
@api.model
|
||||
def automatic_close_state(self):
|
||||
ticket = self.env['odex25_helpdesk.ticket'].search([])
|
||||
is_done = self.env['odex25_helpdesk.stage'].search([('is_done', '=', True)], limit=1)
|
||||
for rec in ticket:
|
||||
if rec.stage_id.is_auto_close:
|
||||
close_time = rec.stage_id.close_time
|
||||
hour, minute = divmod(close_time, 1)
|
||||
minute *= 60
|
||||
to_str = fields.Datetime.to_string(
|
||||
fields.Datetime.from_string(fields.Datetime.now()) - relativedelta(hours=hour, minutes=int(minute)))
|
||||
if not rec.stage_id.is_close:
|
||||
msg_res = rec.message_ids.filtered(lambda x: str(x.date) < to_str)
|
||||
if len(rec.message_ids) == len(msg_res):
|
||||
rec.stage_id = is_done.id
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding='UTF-8'?>
|
||||
<odoo>
|
||||
<record id="ir_cron_automatic_done_state" model="ir.cron">
|
||||
<field name="name">Help Desk Cancel ;Automatic Done State</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_ticket"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.automatic_close_state()</field>
|
||||
<field name="interval_number">3</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_stage_reopen_view_form" model="ir.ui.view">
|
||||
<field name="name">helpdesk.stage.reopen.form</field>
|
||||
<field name="model">odex25_helpdesk.stage</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_stage_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='fold']" position="after">
|
||||
<field name="is_reopen" />
|
||||
<field name="is_done" />
|
||||
<field name="reopen_time" widget="float_time"
|
||||
attrs="{'invisible':[('is_reopen','=',False)],'required':[('is_reopen','!=',False)]}" />
|
||||
<field
|
||||
name="is_auto_close" />
|
||||
<field name="close_time" widget="float_time"
|
||||
attrs="{'invisible':[('is_auto_close','=',False)],'required':[('is_auto_close','!=',False)]}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': 'Helpdesk After Sales',
|
||||
'summary': 'Project, Tasks, After Sales',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': ['odex25_helpdesk', 'sale_management'],
|
||||
'auto_install': True,
|
||||
'description': """
|
||||
Manage the after sale of the products from helpdesk tickets.
|
||||
""",
|
||||
'data': [
|
||||
'views/odex25_helpdesk_views.xml',
|
||||
],
|
||||
'demo': ['data/odex25_helpdesk_sale_demo.xml'],
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="sale_order_odex25_helpdesk_1" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_2"/>
|
||||
</record>
|
||||
<record id="sale_order_line_odex25_helpdesk_1" model="sale.order.line">
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_27').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_27"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="price_unit">3645.00</field>
|
||||
<field name="order_id" ref="sale_order_odex25_helpdesk_1"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_odex25_helpdesk_2" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_10"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_10"/>
|
||||
</record>
|
||||
<record id="sale_order_line_odex25_helpdesk_2" model="sale.order.line">
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_10').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_10"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="price_unit">14.00</field>
|
||||
<field name="order_id" ref="sale_order_odex25_helpdesk_2"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_odex25_helpdesk_3" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_2"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_2"/>
|
||||
</record>
|
||||
<record id="sale_order_line_odex25_helpdesk_3" model="sale.order.line">
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_12').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_12"/>
|
||||
<field name="product_uom_qty">1</field>
|
||||
<field name="price_unit">12.50</field>
|
||||
<field name="order_id" ref="sale_order_odex25_helpdesk_3"/>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_13" model="odex25_helpdesk.ticket">
|
||||
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_2"/>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_16" model="odex25_helpdesk.ticket">
|
||||
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_1"/>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_10" model="odex25_helpdesk.ticket">
|
||||
<field name="sale_order_id" ref="sale_order_odex25_helpdesk_3"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_helpdesk_sale
|
||||
#
|
||||
# Translators:
|
||||
# Mustafa Rawi <mustafa@cubexco.com>, 2020
|
||||
# Osama Ahmaro <osamaahmaro@gmail.com>, 2020
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~13.5+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-09-01 07:39+0000\n"
|
||||
"PO-Revision-Date: 2020-09-07 08:20+0000\n"
|
||||
"Last-Translator: Osama Ahmaro <osamaahmaro@gmail.com>, 2020\n"
|
||||
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: ar\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__commercial_partner_id
|
||||
msgid "Commercial Entity"
|
||||
msgstr "الكيان التجاري"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model,name:odex25_helpdesk_sale.model_odex25_helpdesk_ticket
|
||||
msgid "Helpdesk Ticket"
|
||||
msgstr "تذكرة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id
|
||||
msgid "Ref. Sales Order"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sale
|
||||
#: model:ir.model.fields,help:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id
|
||||
msgid ""
|
||||
"Reference of the Sales Order to which this ticket refers. Setting this "
|
||||
"information aims at easing your After Sales process and only serves "
|
||||
"indicative purposes."
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import odex25_helpdesk
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class odex25_helpdeskTicket(models.Model):
|
||||
_inherit = 'odex25_helpdesk.ticket'
|
||||
|
||||
commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id')
|
||||
sale_order_id = fields.Many2one('sale.order', string='Ref. Sales Order',
|
||||
domain="""[
|
||||
'|', (not commercial_partner_id, '=', 1), ('partner_id', 'child_of', commercial_partner_id or []),
|
||||
('company_id', '=', company_id)]""",
|
||||
groups="sales_team.group_sale_salesman,account.group_account_invoice",
|
||||
help="Reference of the Sales Order to which this ticket refers. Setting this information aims at easing your After Sales process and only serves indicative purposes.")
|
||||
|
||||
def copy(self, default=None):
|
||||
if not self.env.user.has_group('sales_team.group_sale_salesman') and not self.env.user.has_group('account.group_account_invoice'):
|
||||
if default is None:
|
||||
default = {'sale_order_id': False}
|
||||
else:
|
||||
default.update({'sale_order_id': False})
|
||||
return super(odex25_helpdeskTicket, self).copy(default=default)
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
|
@ -0,0 +1,44 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_invoicing" model="ir.ui.view">
|
||||
<field name='name'>odex25_helpdesk.ticket.form.inherit.invoicing</field>
|
||||
<field name="model">odex25_helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name='email_cc' position="after">
|
||||
<field name="commercial_partner_id" invisible="1"/>
|
||||
<field name="sale_order_id" options='{"no_open": True}' readonly="1" invisible="1"/>
|
||||
</field>
|
||||
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||
<attribute name="options">{'always_reload': True}</attribute>
|
||||
<attribute name="context">{'res_partner_search_mode': 'customer'}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="quick_create_ticket_form" model="ir.ui.view">
|
||||
<field name='name'>odex25_helpdesk.ticket.form.quick_create</field>
|
||||
<field name="model">odex25_helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.quick_create_ticket_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="attributes">
|
||||
<attribute name="options">{'always_reload': True}</attribute>
|
||||
<attribute name="context">{'res_partner_search_mode': 'customer'}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk_ticket_view_form_inherit_sale_user" model="ir.ui.view">
|
||||
<field name='name'>odex25_helpdesk.ticket.form.inherit.invoicing</field>
|
||||
<field name="model">odex25_helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_invoicing"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='sale_order_id']" position="attributes">
|
||||
<attribute name="options">{"no_create": True}</attribute>
|
||||
<attribute name="readonly">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'EXP Helpdesk Security',
|
||||
'summary': 'Ticket Security',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': ['odex25_helpdesk', 'odex25_helpdesk_reopen'],
|
||||
'description': """
|
||||
Ticket Security
|
||||
""",
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
# 'data/helpdesk_data.xml',
|
||||
'security/helpdesk_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/view.xml',
|
||||
],
|
||||
'qweb': [
|
||||
"static/src/xml/template.xml",
|
||||
],
|
||||
'license': '',
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import controller
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.tools.translate import _
|
||||
from odoo.addons.mail.controllers.main import MailController
|
||||
from odoo.osv.expression import OR
|
||||
from odoo.tools import consteq, pycompat
|
||||
|
||||
class MailController(MailController):
|
||||
|
||||
@http.route('/mail/view', type='http', auth='none')
|
||||
def mail_action_view(self, model=None, res_id=None, message_id=None, access_token=None, **kwargs):
|
||||
user = request.session.uid
|
||||
if message_id:
|
||||
try:
|
||||
message = request.env['mail.message'].sudo().browse(int(message_id)).exists()
|
||||
except:
|
||||
message = request.env['mail.message']
|
||||
if message:
|
||||
model, res_id = message.model, message.res_id
|
||||
else:
|
||||
# either a wrong message_id, either someone trying ids -> just go to messaging
|
||||
return self._redirect_to_messaging()
|
||||
elif res_id and isinstance(res_id, str):
|
||||
res_id = int(res_id)
|
||||
|
||||
if request.session.uid:
|
||||
user = request.env['res.users'].search([('id','=',request.session.uid)])
|
||||
if not user.has_group('odex25_helpdesk_security.group_helpdesk_normal_manager') and not user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'):
|
||||
return False
|
||||
else:
|
||||
return self._redirect_to_record(model, res_id, access_token)
|
||||
return self._redirect_to_record(model, res_id, access_token)
|
||||
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_helpdesk_security
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-29 07:21+0000\n"
|
||||
"PO-Revision-Date: 2023-08-29 07:21+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_manager_ticket_action_main_tree
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_normal_user_action_main_tree
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_ticket_normal_user_menu_main
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.odex25_helpdesk_ticket_action_main_my_tree
|
||||
msgid "All Tickets"
|
||||
msgstr "كل التذاكر"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Can't create ticket in team , only team leader can create ticket in this "
|
||||
"channel"
|
||||
msgstr ""
|
||||
"لا يمكن إنشاء تذكرة في الفريق ، يمكن لقائد الفريق فقط إنشاء تذكرة في هذه "
|
||||
"القناة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Can't create ticket in team without leader and members except for helpdesk "
|
||||
"manager"
|
||||
msgstr ""
|
||||
"لا يمكن إنشاء تذكرة في الفريق بدون قائد وأعضاء باستثناء مدير مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.action_upcoming_sla_fail_all_tickets_normal_user
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_action_team_user
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_action_unassigned_normal_user
|
||||
msgid "Click create a new ticket."
|
||||
msgstr "انقر فوق إنشاء تذكرة جديدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_team_dashboard_normal_user_action_main
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_menu_team_normal_user_dashboard
|
||||
msgid "Dashboard"
|
||||
msgstr "لوحة عرض"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.menu_helpdesk_normal_user_root
|
||||
msgid "Helpdesk"
|
||||
msgstr "مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_manager
|
||||
msgid "Helpdesk Manager"
|
||||
msgstr "مدير الدعم الفني"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.model,name:odex25_helpdesk_security.model_odex25_helpdesk_stage
|
||||
msgid "Helpdesk Stage"
|
||||
msgstr "مرحلة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.model,name:odex25_helpdesk_security.model_odex25_helpdesk_ticket
|
||||
msgid "Helpdesk Ticket"
|
||||
msgstr "تذكرة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_manager_ticket_action_main_tree
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_normal_user_action_main_tree
|
||||
msgid "No tickets to display."
|
||||
msgstr "لا تذاكر لعرضها."
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.ticket_report_menu_main
|
||||
msgid "Reporting"
|
||||
msgstr "التقارير"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.menu_sla_analysis
|
||||
msgid "SLA Status Analysis"
|
||||
msgstr "تحليل حالة اتفاقية مستوى الخدمة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_user_manager
|
||||
msgid "Team Manager"
|
||||
msgstr "قائد فريق"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_user
|
||||
msgid "Team Member"
|
||||
msgstr "عضو فريق"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.action_upcoming_sla_fail_all_tickets_normal_user
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_action_team_user
|
||||
#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_action_unassigned_normal_user
|
||||
#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_ticket_report_menu
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban
|
||||
msgid "Tickets"
|
||||
msgstr "تذاكر"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban
|
||||
msgid "Tickets to Review"
|
||||
msgstr "تذاكر للمراجعة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban
|
||||
msgid "Unassigned Tickets"
|
||||
msgstr "التذاكر غير المخصصة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0
|
||||
#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You don't have permission to delete this record, please contact Helpdesk "
|
||||
"Manager"
|
||||
msgstr "ليس لديك إذن بحذف هذا السجل ، يرجى الاتصال بمدير مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You're not allowed to create ticket in this team, please select a team that "
|
||||
"you are a member in."
|
||||
msgstr ""
|
||||
"غير مسموح لك بإنشاء تذكرة في هذا الفريق ، يرجى تحديد الفريق الذي أنت عضو "
|
||||
"فيه."
|
||||
|
||||
#. module: odex25_helpdesk_security
|
||||
#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_team_dashboard_normal_user_action_main
|
||||
msgid "Your teams will appear here."
|
||||
msgstr "ستظهر فرقك هنا."
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import odex25_helpdesk_ticket
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = 'odex25_helpdesk.ticket'
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
prevent creating ticket for other teams for normal users
|
||||
"""
|
||||
if 'team_id' in vals:
|
||||
team = self.env['odex25_helpdesk.team'].browse(vals['team_id'])
|
||||
if not team.team_leader_id and not team.member_ids and not self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_manager'):
|
||||
raise ValidationError(
|
||||
_("Can't create ticket in team without leader and members except for helpdesk manager"))
|
||||
|
||||
if team.team_leader_id and not team.member_ids and self.env.user.id != team.team_leader_id.id:
|
||||
raise ValidationError(
|
||||
_("Can't create ticket in team , only team leader can create ticket in this channel"))
|
||||
|
||||
if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'):
|
||||
if 'team_id' in vals and vals['team_id']:
|
||||
team = self.env['odex25_helpdesk.team'].search([('id','=',vals['team_id'])])
|
||||
members = [member.id for member in team.member_ids]
|
||||
members.append(team.team_leader_id.id)
|
||||
if self.env.user.id not in members:
|
||||
raise ValidationError(_("You're not allowed to create ticket in this team, please select a team that you are a member in."))
|
||||
res = super(HelpdeskTicket,self).create(vals)
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
prevent deleting for normal users
|
||||
"""
|
||||
if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'):
|
||||
raise ValidationError(_("You don't have permission to delete this record, please contact Helpdesk Manager"))
|
||||
return super(HelpdeskTicket,self).unlink()
|
||||
|
||||
|
||||
class HelpdeskStage(models.Model):
|
||||
_inherit = 'odex25_helpdesk.stage'
|
||||
|
||||
def unlink(self):
|
||||
"""
|
||||
prevent deleting for normal users
|
||||
"""
|
||||
if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'):
|
||||
raise ValidationError(_("You don't have permission to delete this record, please contact Helpdesk Manager"))
|
||||
return super(HelpdeskStage,self).unlink()
|
||||
|
||||
# @api.multi
|
||||
# def write(self,vals):
|
||||
# """
|
||||
# prevent writing for normal users
|
||||
# """
|
||||
# if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'):
|
||||
# raise ValidationError(_("You don't have permission to this record, please contact Helpdesk Manager"))
|
||||
# return super(HelpdeskStage,self).unlink()
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
|
||||
|
||||
<record id="group_helpdesk_normal_user" model="res.groups">
|
||||
<field name="name">Team Member</field>
|
||||
<field name="implied_ids" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_helpdesk_normal_user_manager" model="res.groups">
|
||||
<field name="name">Team Manager</field>
|
||||
<field name="implied_ids" eval="[(4, ref('group_helpdesk_normal_user')),(4, ref('odex25_helpdesk.group_odex25_helpdesk_manager')),(4, ref('odex25_helpdesk.group_odex25_helpdesk_assignment'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_helpdesk_normal_manager" model="res.groups">
|
||||
<field name="name">Helpdesk Manager</field>
|
||||
<field name="implied_ids" eval="[(4, ref('odex25_helpdesk.group_odex25_helpdesk_manager')),(4, ref('odex25_helpdesk.group_odex25_helpdesk_assignment'))
|
||||
,(4, ref('odex25_helpdesk.group_odex25_helpdesk_on_behalf'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
<!-- <record id="odex25_helpdesk_ticket_user_rule" model="ir.rule">-->
|
||||
<!-- <field name="name">Helpdesk Ticket User</field>-->
|
||||
<!-- <field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>-->
|
||||
<!-- <field name="domain_force">[('user_id','=',uid)]-->
|
||||
<!-- </field>-->
|
||||
<!-- <field name="groups" eval="[(4, ref('group_helpdesk_normal_user_manager'))]"/>-->
|
||||
<!-- </record>-->
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_helpdesk_user_ir_config_parameter,helpdesk.ir.config.parameter,base.model_ir_config_parameter,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,0
|
||||
access_helpdesk_stage,helpdesk.stage,odex25_helpdesk.model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1
|
||||
access_helpdesk_tags,helpdesk.tag,odex25_helpdesk.model_odex25_helpdesk_tag,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1
|
||||
access_helpdesk_sla_report_analysis,sla_report_analysis,odex25_helpdesk.model_odex25_helpdesk_sla_report_analysis,odex25_helpdesk_security.group_helpdesk_normal_user_manager,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<template id="template" xml:space="preserve">
|
||||
<t t-extend="KanbanView.Group">
|
||||
<t t-jquery="div[class='o_kanban_header']" t-operation="replace">
|
||||
<div class="o_kanban_header">
|
||||
<div class="o_kanban_header_title" t-att-title="widget.data_records.length + ' records'" data-delay="500">
|
||||
<span class="o_column_title"><t t-esc="widget.title"/></span>
|
||||
<span class="o_column_unfold"><i class="fa fa-arrows-h"/></span>
|
||||
<!-- <span class="o_kanban_config dropdown">-->
|
||||
<!-- <a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="fa fa-gear"/></a>-->
|
||||
<!-- <ul class="dropdown-menu" role="menu">-->
|
||||
<!-- <li><a class="o_kanban_toggle_fold" href="#">Fold</a></li>-->
|
||||
<!-- <t t-if="widget.grouped_by_m2o" >-->
|
||||
<!-- <li t-if="widget.editable and widget.id"><a class="o_column_edit" href="#">Edit Stage</a></li>-->
|
||||
<!-- <li t-if="widget.deletable and widget.id"><a class="o_column_delete" href="#">Delete</a></li>-->
|
||||
<!-- </t>-->
|
||||
<!-- <t t-if="widget.has_active_field">-->
|
||||
<!-- <t t-if="widget.data.model != 'helpdesk.ticket'" >-->
|
||||
<!-- <li groups="odex25_helpdesk_security.group_helpdesk_normal_manager"><a class="o_column_archive" href="#">Archive Records</a></li>-->
|
||||
<!-- <li groups="odex25_helpdesk_security.group_helpdesk_normal_manager"><a class="o_column_unarchive" href="#">Restore Records</a></li>-->
|
||||
<!-- </t>-->
|
||||
<!-- </t>-->
|
||||
<!-- </ul>-->
|
||||
<!-- </span>-->
|
||||
<span t-if="widget.quick_create" class="o_kanban_quick_add"><i class="fa fa-plus"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
|
||||
<!-- main menu modification -->
|
||||
<record id="helpdesk_manager_ticket_action_main_tree" model="ir.actions.act_window">
|
||||
<field name="name">All Tickets</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
|
||||
<field name="context">{'search_default_my_ticket': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
No tickets to display.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem id="odex25_helpdesk_ticket_action_main_my_tree"
|
||||
action="helpdesk_manager_ticket_action_main_tree"
|
||||
sequence="10" parent="odex25_helpdesk.menu_odex25_helpdesk_root"/>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_main_my" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- my ticket average hours action -->
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('stage_id.is_close', '=', False)]</field>
|
||||
</record>
|
||||
<!-- my tickets average hours based on periority actions -->
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard" model="ir.actions.act_window">
|
||||
<field name="domain">[('stage_id.is_close', '=', False),('user_id','=',uid),('priority', '=', '2')]</field>
|
||||
</record>
|
||||
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_sla" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('priority', '!=', False)]</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!-- My performance actions -->
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_close_analysis" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() -
|
||||
datetime.timedelta(hours=15)).strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_success" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() -
|
||||
datetime.timedelta(hours=15)).strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
</field>
|
||||
</record>
|
||||
<!-- 7 days avarage -->
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_7days_analysis" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() -
|
||||
datetime.timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_7dayssuccess" model="ir.actions.act_window">
|
||||
<field name="domain">[('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() -
|
||||
datetime.timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))]
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- in kanban, the Archived and SLA faild actions domained -->
|
||||
<record id="odex25_helpdesk.helpdesk_ticket_action_Archived" model="ir.actions.act_window">
|
||||
<field name="domain">['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)]</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.helpdesk_ticket_action_slafailed" model="ir.actions.act_window">
|
||||
<field name="domain">['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)]</field>
|
||||
</record>
|
||||
<!-- reports in kanban domained -->
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_team_analysis_action" model="ir.actions.act_window">
|
||||
<field name="domain">['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)]</field>
|
||||
</record>
|
||||
<record id="odex25_helpdesk.odex25_helpdesk_ticket_action_team_performance" model="ir.actions.act_window">
|
||||
<field name="domain">['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)]</field>
|
||||
</record>
|
||||
|
||||
<!--
|
||||
here start separate normal user menues and actions and give the
|
||||
helpdesk manager the original access;
|
||||
-->
|
||||
<!-- normal user -->
|
||||
<record id="action_upcoming_sla_fail_all_tickets_normal_user" model="ir.actions.act_window">
|
||||
<field name="name">Tickets</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="view_mode">kanban,tree,form,pivot,graph</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
|
||||
<field name="context">{'search_default_upcoming_sla_fail': True}</field>
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click create a new ticket.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_action_unassigned_normal_user" model="ir.actions.act_window">
|
||||
<field name="name">Tickets</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{'search_default_team_id': active_id, 'search_default_unassigned': True}</field>
|
||||
<field name="domain">[('user_id','=',uid)]</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click create a new ticket.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_team_dashboard_normal_user_action_main" model="ir.actions.act_window">
|
||||
<field name="name">Dashboard</field>
|
||||
<field name="res_model">odex25_helpdesk.team</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="domain">['|',('member_ids','in',uid),'|',('team_leader_id','=',uid),'&',('team_leader_id','=',False),('member_ids','=',False)]</field>
|
||||
<field name="view_id" ref="odex25_helpdesk.odex25_helpdesk_team_view_kanban"/>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
Your teams will appear here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_helpdesk_normal_user_root" name="Helpdesk"
|
||||
sequence="26"
|
||||
action="helpdesk_team_dashboard_normal_user_action_main"
|
||||
web_icon="odex25_helpdesk,static/description/icon.png"
|
||||
groups="group_helpdesk_normal_user"/>
|
||||
|
||||
|
||||
<record id="helpdesk_ticket_normal_user_action_main_tree" model="ir.actions.act_window">
|
||||
<field name="name">All Tickets</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="view_mode">tree,kanban,form</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
|
||||
<field name="context">{'search_default_my_ticket': True}</field>
|
||||
<field name="domain">['|',('user_id','=',uid),('team_leader_id','=',uid)]</field>
|
||||
<field name="help" type="html">
|
||||
<p>
|
||||
No tickets to display.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="helpdesk_ticket_normal_user_menu_main" action="helpdesk_ticket_normal_user_action_main_tree"
|
||||
sequence="10" parent="menu_helpdesk_normal_user_root" groups="group_helpdesk_normal_user"/>
|
||||
|
||||
<menuitem id="helpdesk_menu_team_normal_user_dashboard" action="helpdesk_team_dashboard_normal_user_action_main"
|
||||
sequence="5" parent="menu_helpdesk_normal_user_root"
|
||||
groups="group_helpdesk_normal_user"/>
|
||||
|
||||
<menuitem id="ticket_report_menu_main" name="Reporting" sequence="15" parent="menu_helpdesk_normal_user_root"
|
||||
groups="group_helpdesk_normal_user_manager"/>
|
||||
|
||||
<menuitem id="helpdesk_ticket_report_menu" name="Tickets"
|
||||
action="odex25_helpdesk.odex25_helpdesk_ticket_analysis_action"
|
||||
sequence="10" parent="ticket_report_menu_main" groups="group_helpdesk_normal_user_manager"/>
|
||||
|
||||
<menuitem id="menu_sla_analysis" name="SLA Status Analysis"
|
||||
action="odex25_helpdesk.odex25_helpdesk_sla_report_analysis_action"
|
||||
sequence="12" parent="ticket_report_menu_main" groups="group_helpdesk_normal_user_manager"/>
|
||||
|
||||
|
||||
<!-- <record model='ir.ui.menu' id='odex25_helpdesk.odex25_helpdesk_ticket_report_menu_main'>-->
|
||||
<!-- <field name="groups_id" eval="[(4,ref('odex25_helpdesk_security.group_helpdesk_normal_user_manager'))]"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
|
||||
<!-- action for halpdask team ticket in kanban user -->
|
||||
<record id="helpdesk_ticket_action_team_user" model="ir.actions.act_window">
|
||||
<field name="name">Tickets</field>
|
||||
<field name="res_model">odex25_helpdesk.ticket</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{'search_default_my_ticket': True}</field>
|
||||
<field name="domain">['|',('user_id','=',uid),('team_leader_id','=',uid),('team_id', '=', active_id)]</field>
|
||||
<field name="search_view_id" ref="odex25_helpdesk.odex25_helpdesk_tickets_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click create a new ticket.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_team_normal_user_view_kanban" model="ir.ui.view">
|
||||
<field name="name">helpdesk.team.dashboard</field>
|
||||
<field name="model">odex25_helpdesk.team</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_team_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div/button[1]' position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[1]' position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[2]' position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[3]' position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[4]' position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[1]' position="after">
|
||||
<a name="%(odex25_helpdesk_security.action_upcoming_sla_fail_all_tickets_normal_user)d"
|
||||
groups="odex25_helpdesk_security.group_helpdesk_normal_user"
|
||||
type="action" context="{'search_default_my_ticket': True, 'default_team_id': active_id}">
|
||||
<t t-esc="record.upcoming_sla_fail_tickets.raw_value"/>
|
||||
Tickets to Review
|
||||
</a>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div[2]/div[2]' position="after">
|
||||
<a name="%(odex25_helpdesk_security.helpdesk_ticket_action_unassigned_normal_user)d" type="action"
|
||||
groups="odex25_helpdesk_security.group_helpdesk_normal_user"
|
||||
context="{'search_default_team_id': active_id, 'default_team_id': active_id}">
|
||||
<t t-esc="record.unassigned_tickets.raw_value"/>
|
||||
Unassigned Tickets
|
||||
</a>
|
||||
</xpath>
|
||||
<xpath expr='//kanban/templates/t/div/div[2]/div/div/button[1]' position="after">
|
||||
<button class="btn btn-primary" groups="odex25_helpdesk_security.group_helpdesk_normal_user"
|
||||
name="%(helpdesk_ticket_action_team_user)d" type="action">Tickets
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//kanban/templates/t/div/div[3]/div/div[2]" position="attributes">
|
||||
<attribute name="groups">
|
||||
odex25_helpdesk_security.group_helpdesk_normal_user_manager,odex25_helpdesk_security.group_helpdesk_normal_manager
|
||||
</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//kanban/templates/t/div/div[3]/div[2]" position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_normal_kanban" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.kanban</field>
|
||||
<field name="model">odex25_helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_ticket_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//kanban/templates/t/div/div[1]" position="attributes">
|
||||
<attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>
|
||||
</xpath>
|
||||
<!-- <xpath expr="//kanban/templates/t/div/div[1]/ul/li[2]" position="attributes">-->
|
||||
<!-- <attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- <xpath expr="//kanban/templates/t/div/div[1]/ul/t[1]" position="attributes">-->
|
||||
<!-- <attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- <xpath expr="//kanban/templates/t/div/div[1]/ul/t[2]" position="attributes">-->
|
||||
<!-- <attribute name="groups">odex25_helpdesk_security.group_helpdesk_normal_manager</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- original -->
|
||||
<record model="ir.ui.menu" id="odex25_helpdesk.menu_odex25_helpdesk_root">
|
||||
<field name="groups_id" eval="[(6,0, [ref('odex25_helpdesk_security.group_helpdesk_normal_manager')])]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.menu" id="odex25_helpdesk.odex25_helpdesk_ticket_menu_main">
|
||||
<!-- <field name="action">odex25_helpdesk_security.helpdesk_manager_ticket_action_main_tree</field> -->
|
||||
<field name="groups_id" eval="[(6,0, [ref('odex25_helpdesk_security.group_helpdesk_normal_manager')])]"/>
|
||||
</record>
|
||||
|
||||
<!-- prevent normal users from editing the stage -->
|
||||
<record id="helpdesk_stage_readonly_view_form" model="ir.ui.view">
|
||||
<field name="name">helpdesk.stage.reopen.form</field>
|
||||
<field name="model">odex25_helpdesk.stage</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_stage_view_form"/>
|
||||
<field name="groups_id" eval="[ (4, ref('odex25_helpdesk_security.group_helpdesk_normal_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='sequence']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='team_ids']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
<attribute name="options">{'no_open' : True}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='template_id']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
<attribute name="options">{'no_open' : True}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='is_close']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='fold']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- prevent normal users from editing the stage -->
|
||||
<record id="helpdesk_stage_readonly_reopen_view_form" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.stage.reopen.form</field>
|
||||
<field name="model">odex25_helpdesk.stage</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk_reopen.helpdesk_stage_reopen_view_form"/>
|
||||
<field name="groups_id" eval="[ (4, ref('odex25_helpdesk_security.group_helpdesk_normal_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='is_reopen']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='reopen_time']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<template id="mail.message_user_assigned">
|
||||
<p>Dear<t t-esc="object.user_id.sudo().name"/>,
|
||||
</p>
|
||||
|
||||
<p>You have been assigned to the
|
||||
<t t-esc="object.env['ir.model']._get(object._name).name.lower()"/>
|
||||
<t t-esc="object.name_get()[0][1]"/>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
<a groups="odex25_helpdesk_security.group_helpdesk_normal_manager,odex25_helpdesk_security.group_helpdesk_normal_user"
|
||||
t-att-href="'/mail/view?model=%s&res_id=%s' % (object._name, object.id)"
|
||||
style="background-color: #3E5D7F; margin-top: 10px; padding: 10px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;">
|
||||
View
|
||||
<t t-esc="object.env['ir.model']._get(object._name).name.lower()"/>
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'EXP Helpdesk SLA Escalation Reminder',
|
||||
'summary': 'Reminder in SLA Policy to reminde the team leader of the task depending on configuration',
|
||||
'author': "Expert Co Ltd",
|
||||
'website': "http://www.ex.com",
|
||||
'category': 'Odex25-Helpdesk/Odex25-Helpdesk',
|
||||
'depends': ['odex25_helpdesk'],
|
||||
'description': """
|
||||
Reminder in SLA Policy to reminde the team leader of the task depending on configuration
|
||||
""",
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/reminder_cron.xml',
|
||||
'data/reminder_templates.xml',
|
||||
'views/helpdesk_sla_views.xml',
|
||||
],
|
||||
'license': '',
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding='UTF-8'?>
|
||||
<odoo>
|
||||
<record id="helpdesk_sla_reminder" model="ir.cron">
|
||||
<field name="name">Helpdesk: reminder and escalation SLAs</field>
|
||||
<field name="model_id" ref="model_odex25_helpdesk_sla"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.reminder_and_escalation()</field>
|
||||
<field name="interval_number">30</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ticket_sla_reminder" model="mail.template">
|
||||
<field name="name">Ticket Reminder</field>
|
||||
<field name="email_from"></field>
|
||||
<field name="subject">Ticket Reminder</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="email_to"></field>
|
||||
<field name="partner_to"></field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
<field name="body_html" type="xml">
|
||||
<div style="max-width:600px; height:auto; margin-left:30px;">
|
||||
<div style="margin-left:30px;align=center;" width="60%">
|
||||
<p><strong style="color:#e47e01">Reminder!</strong></p>
|
||||
<p>Dear ${ctx['mail_to'|safe]},</p>
|
||||
<span>your ticket</span>
|
||||
<strong>
|
||||
% if object.access_token:
|
||||
<a href="#" data-oe-id='${object.id}' data-oe-model="helpdesk.ticket">${object.name|safe}#${object.id|safe}</a>
|
||||
<!-- <a href="/helpdesk/ticket/${object.id}/${object.access_token}">${object.name|safe}#${object.id|safe}</a> -->
|
||||
% endif
|
||||
</strong>
|
||||
<span>is about to seek the deadline</span>
|
||||
<span>in ${ctx['time_untill']|safe}.</span><span> Please follow up.</span>
|
||||
</div>
|
||||
<p>
|
||||
<strong><span style="margin-left:30px;">Kind regards, </span></strong><br/>
|
||||
<span style="margin-left:30px;font-weight:normal;">${object.team_id.name or 'Helpdesk'} Team.</span>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="ticket_sla_escalation" model="mail.template">
|
||||
<field name="name">Ticket Escalation</field>
|
||||
<field name="email_from"></field>
|
||||
<field name="subject">Ticket Escalation</field>
|
||||
<field name="model_id" ref="odex25_helpdesk.model_odex25_helpdesk_ticket"/>
|
||||
<field name="email_to"></field>
|
||||
<field name="partner_to"></field>
|
||||
<field name="auto_delete" eval="False"/>
|
||||
<field name="body_html" type="xml">
|
||||
<div style="max-width:600px; height:auto; margin-left:30px;">
|
||||
<div style="margin-left:30px;align=center;" width="60%">
|
||||
<p style="color:#ca0c05;"><strong>Escalation!</strong></p>
|
||||
<p>Dear ${ctx['mail_to'|safe]},</p>
|
||||
<span>The ticket </span>
|
||||
<strong>
|
||||
% if object.access_token:
|
||||
<a href="#" data-oe-id='${object.id}' data-oe-model="helpdesk.ticket">${object.name|safe}#${object.id|safe}</a>
|
||||
<!-- <a href="/helpdesk/ticket/${object.id}/${object.access_token}">${object.name|safe}#${object.id|safe}</a> -->
|
||||
% endif
|
||||
</strong>
|
||||
<span>assigned to ${object.user_id.name}</span>
|
||||
<span>exceeded the deadline by ${ctx['time_untill']|safe}.</span>
|
||||
</div>
|
||||
<p>
|
||||
<strong><span style="margin-left:30px;">Kind regards, </span></strong><br/>
|
||||
<span style="margin-left:30px;font-weight:normal;">${object.team_id.name or 'Helpdesk'} Team.</span>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * odex25_helpdesk_sla_escalation_reminder
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-09-21 11:48+0000\n"
|
||||
"PO-Revision-Date: 2022-09-21 11:48+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid " Hours and "
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid " Minutes"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:mail.template,body_html:odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation
|
||||
msgid ""
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<div style=\"max-width:600px; height:auto; margin-left:30px;\">\n"
|
||||
" <div style=\"margin-left:30px;align=center;\" width=\"60%\">\n"
|
||||
" <p style=\"color:#ca0c05;\"><strong>Escalation!</strong></p>\n"
|
||||
" <p>Dear ${ctx['mail_to'|safe]},</p>\n"
|
||||
" <span>The ticket </span> \n"
|
||||
" <strong>\n"
|
||||
" % if object.access_token:\n"
|
||||
" <a href=\"#\" data-oe-id=\"${object.id}\" data-oe-model=\"helpdesk.ticket\">${object.name|safe}#${object.id|safe}</a>\n"
|
||||
" <!-- <a href=\"/helpdesk/ticket/${object.id}/${object.access_token}\">${object.name|safe}#${object.id|safe}</a> -->\n"
|
||||
" % endif\n"
|
||||
" </strong>\n"
|
||||
" <span>assigned to ${object.user_id.name}</span> \n"
|
||||
" <span>exceeded the deadline by ${ctx['time_untill']|safe}.</span>\n"
|
||||
" </div>\n"
|
||||
" <p>\n"
|
||||
" <strong><span style=\"margin-left:30px;\">Kind regards, </span></strong><br/>\n"
|
||||
" <span style=\"margin-left:30px;font-weight:normal;\">${object.team_id.name or 'Helpdesk'} Team.</span>\n"
|
||||
" </p>\n"
|
||||
" </div>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:mail.template,body_html:odex25_helpdesk_sla_escalation_reminder.ticket_sla_reminder
|
||||
msgid ""
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<div style=\"max-width:600px; height:auto; margin-left:30px;\">\n"
|
||||
" <div style=\"margin-left:30px;align=center;\" width=\"60%\">\n"
|
||||
" <p><strong style=\"color:#e47e01\">Reminder!</strong></p>\n"
|
||||
" <p>عزيزي ${ctx['mail_to'|safe]},</p>\n"
|
||||
" <span>تذكرتك</span> \n"
|
||||
" <strong>\n"
|
||||
" % if object.access_token:\n"
|
||||
" <a href=\"#\" data-oe-id=\"${object.id}\" data-oe-model=\"helpdesk.ticket\">${object.name|safe}#${object.id|safe}</a>\n"
|
||||
" <!-- <a href=\"/helpdesk/ticket/${object.id}/${object.access_token}\">${object.name|safe}#${object.id|safe}</a> -->\n"
|
||||
" % endif\n"
|
||||
" </strong>\n"
|
||||
" <span>على وشك البحث عن الموعد النهائي</span>\n"
|
||||
" <span>in ${ctx['time_untill']|safe}.</span><span>أرجو المتابعة</span>\n"
|
||||
" </div>\n"
|
||||
" <p>\n"
|
||||
" <strong><span style=\"margin-left:30px;\">أطيب التحيات </span></strong><br/>\n"
|
||||
" <span style=\"margin-left:30px;font-weight:normal;\">${object.team_id.name or 'الدعم الفني'} فريق.</span>\n"
|
||||
" </p>\n"
|
||||
" </div>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields.selection,name:odex25_helpdesk_sla_escalation_reminder.selection__helpdesk_sla_policy__type__after
|
||||
msgid "After"
|
||||
msgstr "بعد"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields.selection,name:odex25_helpdesk_sla_escalation_reminder.selection__helpdesk_sla_policy__type__before
|
||||
msgid "Before"
|
||||
msgstr "قبل"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid "Days, "
|
||||
msgstr "الأيام"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__display_name
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "الاسم المعروض"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__escalation_ids
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Escalation"
|
||||
msgstr "التصعيد"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_odex25_helpdesk_sla
|
||||
msgid "Helpdesk SLA Policies"
|
||||
msgstr "سياسات اتفاق مستوى الخدمة لمكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_odex25_helpdesk_ticket
|
||||
msgid "Helpdesk Ticket"
|
||||
msgstr "تذكرة مكتب المساعدة"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.actions.server,name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder_ir_actions_server
|
||||
#: model:ir.cron,cron_name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder
|
||||
#: model:ir.cron,name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder
|
||||
msgid "Helpdesk: reminder and escalation SLAs"
|
||||
msgstr "مكتب المساعدة: تذكير وتصعيد اتفاقيات مستوى الخدمة"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid "Hours Can't be more than working hours, Kindly! try again"
|
||||
msgstr "لا يمكن أن تكون الساعات أكثر من ساعات العمل ، يرجى! حاول مرة أخرى"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__id
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid "Kindly, make sure that time is not less than or equal zero"
|
||||
msgstr "يرجى التأكد من أن الوقت ليس أقل من أو يساوي الصفر"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid "Kindly, make sure that time is not less than zero"
|
||||
msgstr "يرجى التأكد من أن الوقت لا يقل عن الصفر"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla____last_update
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "آخر تعديل في"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0
|
||||
#, python-format
|
||||
msgid "Madam, Sir"
|
||||
msgstr "سيدتي ، سيدي"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__reminder_ids
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Reminder"
|
||||
msgstr "تذكير"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Reminder After-Days"
|
||||
msgstr ""
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Reminder After-Hours"
|
||||
msgstr "تذكير بعد الأيام"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Reminder Before-Days"
|
||||
msgstr "تذكير قبل الأيام"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy
|
||||
msgid "Reminder Before-Hours"
|
||||
msgstr "تذكير قبل ساعات"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__reminder_days
|
||||
msgid "Reminder Days"
|
||||
msgstr "أيام التذكير"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__reminder_hours
|
||||
msgid "Reminder Hours"
|
||||
msgstr "ساعات التذكير"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__reminders_ids
|
||||
msgid "Reminders"
|
||||
msgstr "تذكير"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__sla_escalation_id
|
||||
msgid "Sla Escalation"
|
||||
msgstr "تصعيد اتفاقية مستوى الخدمة (SLA)"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__sla_reminder_id
|
||||
msgid "Sla Reminder"
|
||||
msgstr "تذكير اتفاقية مستوى الخدمة (SLA)"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__team_id
|
||||
msgid "Team"
|
||||
msgstr "الفريق"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:mail.template,subject:odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation
|
||||
msgid "Ticket Escalation"
|
||||
msgstr "تصعيد التذاكر"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:mail.template,subject:odex25_helpdesk_sla_escalation_reminder.ticket_sla_reminder
|
||||
msgid "Ticket Reminder"
|
||||
msgstr "تذكير التذكرة"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__type
|
||||
msgid "Type"
|
||||
msgstr "النوع"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__user_id
|
||||
msgid "User"
|
||||
msgstr "المستخدم"
|
||||
|
||||
#. module: odex25_helpdesk_sla_escalation_reminder
|
||||
#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_helpdesk_sla_policy
|
||||
msgid "helpdesk.sla.policy"
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import helpdesk_reminder_policy
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import datetime
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_inherit = "odex25_helpdesk.ticket"
|
||||
|
||||
reminders_ids = fields.Many2many('helpdesk.sla.policy')
|
||||
# escalations_ids = fields.Many2many('helpdesk.sla.policy')
|
||||
|
||||
|
||||
class HelpdeskSLA(models.Model):
|
||||
_inherit = "odex25_helpdesk.sla"
|
||||
|
||||
reminder_ids = fields.One2many('helpdesk.sla.policy','sla_reminder_id')
|
||||
escalation_ids = fields.One2many('helpdesk.sla.policy','sla_escalation_id')
|
||||
|
||||
@api.constrains('time_hours')
|
||||
def prevent_hours_morethan_workinghours(self):
|
||||
working_calendar = self.env.user.company_id.resource_calendar_id
|
||||
for rec in self:
|
||||
if working_calendar.is_full_day:
|
||||
if rec.time_hours > working_calendar.working_hours:
|
||||
raise ValidationError(_("Hours Can't be more than working hours, Kindly! try again"))
|
||||
else:
|
||||
if rec.time_hours > working_calendar.shift_one_working_hours or rec.time_hours > working_calendar.shift_two_working_hours:
|
||||
raise ValidationError(_("Hours Can't be more than working hours, Kindly! try again"))
|
||||
|
||||
@api.model
|
||||
def reminder_and_escalation(self):
|
||||
"""
|
||||
send reminder and escalation according to sla policy
|
||||
"""
|
||||
policies = self.env['odex25_helpdesk.sla'].search([])
|
||||
for sla in policies:
|
||||
if sla.reminder_ids:
|
||||
for reminder in sla.reminder_ids:
|
||||
tickets = self.env['odex25_helpdesk.ticket'].search([('sla_id','=',sla.id),('reminders_ids','not in',reminder.id),('deadline','!=',None)])
|
||||
_logger.error('length of tickets %s',len(tickets))
|
||||
for ticket in tickets:
|
||||
difference = fields.Datetime.from_string(ticket.deadline) - datetime.datetime.now()
|
||||
difference = str(difference).split(',')
|
||||
hour, minute = divmod(reminder.reminder_hours, 1)
|
||||
minute *= 60
|
||||
result = '{}:{}'.format(int(hour), int(minute))
|
||||
result = result.split(':')
|
||||
if len(difference) == 2:
|
||||
days = int(difference[0].split(' ')[0])
|
||||
hours = int(difference[1].split(':')[0])
|
||||
minutes = int(difference[1].split(':')[1])
|
||||
else:
|
||||
days = 0
|
||||
hours = int(difference[0].split(':')[0])
|
||||
minutes = int(difference[0].split(':')[1])
|
||||
if days <= reminder.reminder_days:
|
||||
if hours < int(result[0]):
|
||||
if ticket.sla_id.stage_id.id != ticket.stage_id.id:
|
||||
template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder')
|
||||
template.email_to = ticket.user_id.partner_id.email
|
||||
times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes')
|
||||
if ticket.user_id:
|
||||
to = ticket.user_id.name
|
||||
else:
|
||||
to = _("Madam, Sir")
|
||||
template.sudo().with_context({
|
||||
'time_untill':times,
|
||||
'mail_to': to,
|
||||
'lang':self.env.user.lang,
|
||||
}).send_mail(ticket.id, force_send=True, raise_exception=False)
|
||||
ticket.write({'reminders_ids': [(4, reminder.id, 0)]})
|
||||
elif hours == int(result[0]):
|
||||
if minutes <= int(result[1]):
|
||||
if ticket.sla_id.stage_id.id != ticket.stage_id.id:
|
||||
template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder')
|
||||
template.email_to = ticket.user_id.partner_id.email
|
||||
times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes')
|
||||
if ticket.user_id:
|
||||
to = ticket.user_id.name
|
||||
else:
|
||||
to = _("Madam, Sir")
|
||||
template.sudo().with_context({
|
||||
'time_untill':times,
|
||||
'mail_to': to,
|
||||
'lang':self.env.user.lang,
|
||||
}).send_mail(ticket.id, force_send=True, raise_exception=False)
|
||||
ticket.write({'reminders_ids': [(4, reminder.id, 0)]})
|
||||
elif days <= -1:
|
||||
if ticket.sla_id.stage_id.id != ticket.stage_id.id:
|
||||
template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder')
|
||||
template.email_to = ticket.user_id.partner_id.email
|
||||
times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes')
|
||||
if ticket.user_id:
|
||||
to = ticket.user_id.name
|
||||
else:
|
||||
to = _("Madam, Sir")
|
||||
template.sudo().with_context({
|
||||
'time_untill':times,
|
||||
'mail_to': to,
|
||||
'lang':self.env.user.lang,
|
||||
}).send_mail(ticket.id, force_send=True, raise_exception=False)
|
||||
ticket.write({'reminders_ids': [(4, reminder.id, 0)]})
|
||||
else:
|
||||
continue
|
||||
if sla.escalation_ids:
|
||||
for escalation in sla.escalation_ids:
|
||||
tickets = self.env['odex25_helpdesk.ticket'].search([('sla_id','=',sla.id),('reminders_ids','not in',escalation.id),('deadline','!=',None)])
|
||||
_logger.error('length of tickets %s',len(tickets))
|
||||
for ticket in tickets:
|
||||
difference = datetime.datetime.now() - fields.Datetime.from_string(ticket.deadline)
|
||||
difference = str(difference).split(',')
|
||||
hour, minute = divmod(escalation.reminder_hours, 1)
|
||||
minute *= 60
|
||||
result = '{}:{}'.format(int(hour), int(minute))
|
||||
result = result.split(':')
|
||||
if len(difference) == 2:
|
||||
days = int(difference[0].split(' ')[0])
|
||||
hours = int(difference[1].split(':')[0])
|
||||
minutes = int(difference[1].split(':')[1])
|
||||
else:
|
||||
days = 0
|
||||
hours = int(difference[0].split(':')[0])
|
||||
minutes = int(difference[0].split(':')[1])
|
||||
if days >= escalation.reminder_days:
|
||||
if hours > int(result[0]):
|
||||
if ticket.sla_id.stage_id.id != ticket.stage_id.id:
|
||||
template = self.env.ref('odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation')
|
||||
template.email_to = escalation.user_id.partner_id.email
|
||||
times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes')
|
||||
if escalation.user_id:
|
||||
to = escalation.user_id.name
|
||||
else:
|
||||
to = _("Madam, Sir")
|
||||
template.sudo().with_context({
|
||||
'time_untill':times,
|
||||
'mail_to': to,
|
||||
'lang':self.env.user.lang,
|
||||
}).send_mail(ticket.id, force_send=True, raise_exception=False)
|
||||
ticket.write({'reminders_ids': [(4, escalation.id, 0)]})
|
||||
elif hours == int(result[0]):
|
||||
if minutes >= int(result[1]):
|
||||
if ticket.sla_id.stage_id.id != ticket.stage_id.id:
|
||||
template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_escalation')
|
||||
template.email_to = escalation.user_id.partner_id.email
|
||||
times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes')
|
||||
if escalation.user_id:
|
||||
to = escalation.user_id.name
|
||||
else:
|
||||
to = _("Madam, Sir")
|
||||
template.sudo().with_context({
|
||||
'time_untill':times,
|
||||
'mail_to': to,
|
||||
'lang':self.env.user.lang,
|
||||
}).send_mail(ticket.id, force_send=True, raise_exception=False)
|
||||
ticket.write({'reminders_ids': [(4, escalation.id, 0)]})
|
||||
else:
|
||||
continue
|
||||
|
||||
|
||||
class HelpdeskSLAReminderPolicy(models.Model):
|
||||
_name = 'helpdesk.sla.policy'
|
||||
|
||||
sla_reminder_id = fields.Many2one('odex25_helpdesk.sla')
|
||||
sla_escalation_id = fields.Many2one('odex25_helpdesk.sla')
|
||||
team_id = fields.Many2one(related="sla_reminder_id.team_id",store=True)
|
||||
|
||||
type = fields.Selection([
|
||||
('after','After'),
|
||||
('before','Before'),
|
||||
])
|
||||
reminder_hours = fields.Float()
|
||||
reminder_days = fields.Integer()
|
||||
user_id = fields.Many2one('res.users')
|
||||
|
||||
@api.constrains('reminder_hours','reminder_days')
|
||||
def _prevent_zero(self):
|
||||
"""
|
||||
Prevent the time to be zero if the days are zero
|
||||
"""
|
||||
if self.reminder_days < 0.0 or self.reminder_hours < 0.0:
|
||||
raise ValidationError(_("Kindly, make sure that time is not less than zero"))
|
||||
if self.reminder_days == 0.0 and self.reminder_hours <= 0.0:
|
||||
raise ValidationError(_("Kindly, make sure that time is not less than or equal zero"))
|
||||
|
||||
@api.onchange('team_id')
|
||||
def _domain_user_to_sla(self):
|
||||
"""
|
||||
return domain in users
|
||||
"""
|
||||
users = []
|
||||
if self.sla_reminder_id:
|
||||
users = self.sla_reminder_id.team_id.members_ids.mapped('member_id').ids
|
||||
users.append(self.sla_reminder_id.team_id.team_leader_id.id)
|
||||
if self.sla_escalation_id:
|
||||
users = self.sla_escalation_id.team_id.members_ids.mapped('member_id').ids
|
||||
users.append(self.sla_escalation_id.team_id.team_leader_id.id)
|
||||
return{
|
||||
'domain':{'user_id':[('id','in',users)]}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_helpdesk_sla_policy,odex25_helpdesk_sla_escalation_reminder.sla.policy,odex25_helpdesk_sla_escalation_reminder.model_helpdesk_sla_policy,,1,1,1,1
|
||||
|
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1,83 @@
|
|||
<section class="oe_container">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan"
|
||||
style="text-align: center;font-size: 25px;font-weight: 600;margin: 0px !important;color:#145374;">
|
||||
ONE OF ODEX MODULES</h2>
|
||||
<h6 class="oe_slogan" style="text-align: center;font-size: 18px;">
|
||||
ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system
|
||||
<br/>
|
||||
.to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one
|
||||
solution
|
||||
</h6>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container" style="padding: 1% 0% 0% 3%;">
|
||||
<div class="oe_row oe_spaced">
|
||||
<h2 class="oe_slogan"
|
||||
style="text-align: center;font-size: 25px;font-weight: 600;margin: 0px !important;color:#145374;">
|
||||
Contact Us
|
||||
</h2>
|
||||
<br/>
|
||||
<br/>
|
||||
<div style="display:flex;padding-top: 20px;justify-content: space-between;">
|
||||
<div style="flex-basis: 18%;"></div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://exp-sa.com" target="_blank">
|
||||
<img src="internet.png" style="width: 100%;border-radius: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://exp-sa.com/" target="_blank">
|
||||
www.exp-sa.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://twitter.com/expcosa/" target="_blank">
|
||||
<img src="twitter.png" style="width: 100%;border-radius: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://twitter.com/expcosa/" target="_blank">
|
||||
exposa
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="https://www.linkedin.com/in/expert-company-52b5b812b/" target="_blank">
|
||||
<img src="linkedin.png" style="width: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="https://www.linkedin.com/in/expert-company-52b5b812b/" target="_blank">
|
||||
exposa
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;">
|
||||
<div style="width:60px;height:60px;background:#fff; border-radius:100%;margin: auto;">
|
||||
<a href="http://info@exp-sa.com/" target="_blank">
|
||||
<img src="mail.png" style="width: 100%;"/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="oe_slogan"
|
||||
style="font-weight: 800;text-align: center;font-size: 14px;width: 100%;margin: 0;margin-top: 14px;color: #000 !important;margin-top: 5px;opacity: 1 !important;line-height: 17px;">
|
||||
<a href="http://info@exp-sa.com/" target="_blank">
|
||||
Info@exp-sa.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div style="flex-basis: 18%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oe_container oe_separator">
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_sla_view_form_reminder_policy" model="ir.ui.view">
|
||||
<field name="name">odex25_helpdesk.sla.form.inherit.reminder.policy</field>
|
||||
<field name="model">odex25_helpdesk.sla</field>
|
||||
<field name="inherit_id" ref="odex25_helpdesk.odex25_helpdesk_sla_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='description']" position="before">
|
||||
<notebook>
|
||||
<page name="Reminder" string="Reminder">
|
||||
<field name="reminder_ids" >
|
||||
<tree editable="bottom" domain="[('type','=','before')]">
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="team_id" invisible="1"/>
|
||||
<field name="reminder_days" string="Reminder Before-Days"/>
|
||||
<field name="reminder_hours" string="Reminder Before-Hours" widget="float_time"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page name="Escalation" string="Escalation">
|
||||
<field name="escalation_ids">
|
||||
<tree editable="bottom" domain="[('type','=','after')]">
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="team_id" invisible="1"/>
|
||||
<field name="reminder_days" string="Reminder After-Days"/>
|
||||
<field name="reminder_hours" string="Reminder After-Hours" widget="float_time"/>
|
||||
<field name="user_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||