Add odex25_helpdesk

This commit is contained in:
expert 2024-06-24 14:07:12 +03:00
parent 697a7d5572
commit 7c506d3ba8
292 changed files with 28539 additions and 0 deletions

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import report

View File

@ -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,
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import portal
from . import rating

View File

@ -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 ''))

View File

@ -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})

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 didnt 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 doesnt 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 arent 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">Drawers 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>

File diff suppressed because it is too large Load Diff

View File

@ -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)
""")

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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'))

View File

@ -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)

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import odex25_helpdesk_sla_report_analysis

View File

@ -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)
)
""")

View File

@ -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','&gt;', (context_today() - datetime.timedelta(days=7)).strftime('%%Y-%%m-%%d'))]"/>
<filter string="Last 30 days" name="last_month" domain="[('create_date','&gt;', (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>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_odex25_helpdesk_tag odex25_helpdesk.tag model_odex25_helpdesk_tag odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1
3 access_odex25_helpdesk_sla odex25_helpdesk.sla model_odex25_helpdesk_sla odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
4 access_odex25_helpdesk_sla_status odex25_helpdesk.sla.status model_odex25_helpdesk_sla_status odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
5 access_odex25_helpdesk_sla_manager odex25_helpdesk.sla.manager model_odex25_helpdesk_sla odex25_helpdesk.group_odex25_helpdesk_manager 1 1 1 1
6 access_odex25_helpdesk_stage odex25_helpdesk.stage model_odex25_helpdesk_stage odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
7 access_odex25_helpdesk_stage_manager odex25_helpdesk.stage.manager model_odex25_helpdesk_stage odex25_helpdesk.group_odex25_helpdesk_manager 1 1 1 1
8 access_odex25_helpdesk_stage_portal odex25_helpdesk.stage.portal odex25_helpdesk.model_odex25_helpdesk_stage base.group_portal 1 0 0 0
9 access_odex25_helpdesk_ticket_portal odex25_helpdesk.ticket.portal odex25_helpdesk.model_odex25_helpdesk_ticket base.group_portal 1 0 0 0
10 access_odex25_helpdesk_ticket odex25_helpdesk.ticket model_odex25_helpdesk_ticket odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1
11 access_odex25_helpdesk_team_public odex25_helpdesk.team model_odex25_helpdesk_team odex25_helpdesk.group_odex25_helpdesk_user 1 0 0 0
12 access_odex25_helpdesk_team_no_group odex25_helpdesk.team model_odex25_helpdesk_team 1 0 0 0
13 access_odex25_helpdesk_team_portal odex25_helpdesk.team.portal odex25_helpdesk.model_odex25_helpdesk_team base.group_portal 1 0 0 0
14 access_odex25_helpdesk_team_manager odex25_helpdesk.team.manager model_odex25_helpdesk_team odex25_helpdesk.group_odex25_helpdesk_manager 1 1 1 1
15 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
16 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
17 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
18 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
19 access_zfp_helpdesk_service_category zfp_helpdesk.zfp_helpdesk odex25_helpdesk.model_service_category odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1
20 access_zfp_helpdesk_service zfp_helpdesk.zfp_helpdesk odex25_helpdesk.model_helpdesk_service odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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,
};
});

View File

@ -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 &amp; 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',
// }
]);
});

View File

@ -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;
}
}
}
}
}
}

View File

@ -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'] &lt;= 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'] &lt;= 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>

View File

@ -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();
});
});
});

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'))

View File

@ -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")

View File

@ -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")

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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&amp;id=%s&amp;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>

View File

@ -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 &amp; 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 &amp; 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>&amp;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 &gt;= 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 &gt;= 50 ? 'badge-success' : 'badge-warning'}">
<i t-attf-class="fa #{record.rating_percentage_satisfaction.raw_value &gt;= 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>

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -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': '',
}

View File

@ -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 "الخدمة"

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import helpdesk_team

View File

@ -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)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_helpdesk_team_member helpdesk.team.member odex25_helpdesk_assignation_method.model_helpdesk_team_member 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -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': '',
}

View File

@ -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 "إغلاق التذكرة"

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import helpdesk

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

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

View File

@ -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>

View File

@ -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 ""

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import odex25_helpdesk

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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>

View File

@ -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

View File

@ -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': '',
}

View File

@ -0,0 +1 @@
from . import controller

View File

@ -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)

View File

@ -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 "ستظهر فرقك هنا."

View File

@ -0,0 +1 @@
from . import odex25_helpdesk_ticket

View File

@ -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()

View File

@ -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>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 access_helpdesk_stage helpdesk.stage odex25_helpdesk.model_odex25_helpdesk_stage odex25_helpdesk.group_odex25_helpdesk_user 1 1 1 1
4 access_helpdesk_tags helpdesk.tag odex25_helpdesk.model_odex25_helpdesk_tag odex25_helpdesk.group_odex25_helpdesk_manager 1 1 1 1
5 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

View File

@ -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>

View File

@ -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),'&amp;',('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&amp;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>

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models

View File

@ -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': '',
}

View File

@ -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>

View File

@ -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>

View File

@ -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 ""

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import helpdesk_reminder_policy

View File

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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_helpdesk_sla_policy odex25_helpdesk_sla_escalation_reminder.sla.policy odex25_helpdesk_sla_escalation_reminder.model_helpdesk_sla_policy 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More