feature/odoo18 #2
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
||||
|
||||
def _create_helpdesk_team(env):
|
||||
team_1 = env.ref('helpdesk.helpdesk_team1', raise_if_not_found=False)
|
||||
env['res.company'].search([('id', '!=', team_1.company_id.id)])._create_helpdesk_team()
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Helpdesk',
|
||||
'version': '1.6',
|
||||
'category': 'Services/Helpdesk',
|
||||
'sequence': 110,
|
||||
'summary': 'Track, prioritize, and solve customer tickets',
|
||||
'website': 'https://www.odoo.com/app/helpdesk',
|
||||
'depends': [
|
||||
'base_setup',
|
||||
'mail',
|
||||
'utm',
|
||||
'rating',
|
||||
'web_tour',
|
||||
'resource',
|
||||
'portal',
|
||||
'digest',
|
||||
],
|
||||
'description': """
|
||||
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/helpdesk_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/digest_data.xml',
|
||||
'data/mail_message_subtype_data.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/helpdesk_data.xml',
|
||||
'data/ir_cron_data.xml',
|
||||
'data/ir_sequence_data.xml',
|
||||
'data/helpdesk_tour.xml',
|
||||
'views/helpdesk_ticket_views.xml',
|
||||
'report/helpdesk_ticket_analysis_views.xml',
|
||||
'report/helpdesk_sla_report_analysis_views.xml',
|
||||
'views/helpdesk_tag_views.xml',
|
||||
'views/helpdesk_stage_views.xml',
|
||||
'views/helpdesk_sla_views.xml',
|
||||
'views/helpdesk_team_views.xml',
|
||||
'views/digest_views.xml',
|
||||
'views/helpdesk_portal_templates.xml',
|
||||
'views/rating_rating_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/mail_activity_views.xml',
|
||||
'views/helpdesk_templates.xml',
|
||||
'views/helpdesk_menus.xml',
|
||||
'wizard/helpdesk_stage_delete_views.xml',
|
||||
],
|
||||
'demo': ['data/helpdesk_demo.xml'],
|
||||
'application': True,
|
||||
'license': 'OEEL-1',
|
||||
'post_init_hook': '_create_helpdesk_team',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'helpdesk/static/src/scss/helpdesk.scss',
|
||||
'helpdesk/static/src/css/portal_helpdesk.css',
|
||||
'helpdesk/static/src/components/**/*',
|
||||
'helpdesk/static/src/views/**/*',
|
||||
'helpdesk/static/src/js/tours/helpdesk.js',
|
||||
('remove', 'helpdesk/static/src/views/helpdesk_ticket_graph/**'),
|
||||
('remove', 'helpdesk/static/src/views/helpdesk_ticket_pivot/**'),
|
||||
],
|
||||
'web.assets_backend_lazy': [
|
||||
'helpdesk/static/src/views/helpdesk_ticket_graph/**',
|
||||
'helpdesk/static/src/views/helpdesk_ticket_pivot/**',
|
||||
],
|
||||
'web.assets_unit_tests': [
|
||||
'helpdesk/static/tests/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'helpdesk/static/tests/tours/**/*',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import portal
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
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 import portal
|
||||
from odoo.addons.portal.controllers.portal import pager as portal_pager
|
||||
from odoo.osv.expression import AND, FALSE_DOMAIN
|
||||
|
||||
|
||||
class CustomerPortal(portal.CustomerPortal):
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(CustomerPortal, self)._prepare_portal_layout_values()
|
||||
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['helpdesk.ticket'].search_count(self._prepare_helpdesk_tickets_domain())
|
||||
if request.env['helpdesk.ticket'].has_access('read')
|
||||
else 0
|
||||
)
|
||||
return values
|
||||
|
||||
def _prepare_helpdesk_tickets_domain(self):
|
||||
return []
|
||||
|
||||
def _ticket_get_page_view_values(self, ticket, access_token, **kwargs):
|
||||
values = {
|
||||
'page_name': 'ticket',
|
||||
'ticket': ticket,
|
||||
'ticket_link_section': [],
|
||||
'ticket_closed': kwargs.get('ticket_closed', False),
|
||||
'preview_object': ticket,
|
||||
}
|
||||
return self._get_page_view_values(ticket, access_token, values, 'my_tickets_history', False, **kwargs)
|
||||
|
||||
def _ticket_get_searchbar_inputs(self):
|
||||
return {
|
||||
'name': {'input': 'name', 'label': _(
|
||||
'Search%(left)s Tickets%(right)s',
|
||||
left=Markup('<span class="nolabel">'),
|
||||
right=Markup('</span>'),
|
||||
), 'sequence': 10},
|
||||
'user_id': {'input': 'user_id', 'label': _('Search in Assigned to'), 'sequence': 20},
|
||||
'partner_id': {'input': 'partner_id', 'label': _('Search in Customer'), 'sequence': 30},
|
||||
'team_id': {'input': 'team_id', 'label': _('Search in Helpdesk Team'), 'sequence': 40},
|
||||
'stage_id': {'input': 'stage_id', 'label': _('Search in Stage'), 'sequence': 50},
|
||||
}
|
||||
|
||||
def _ticket_get_searchbar_groupby(self):
|
||||
return {
|
||||
'none': {'label': _('None'), 'sequence': 10},
|
||||
'user_id': {'label': _('Assigned to'), 'sequence': 20},
|
||||
'team_id': {'label': _('Helpdesk Team'), 'sequence': 30},
|
||||
'stage_id': {'label': _('Stage'), 'sequence': 40},
|
||||
'kanban_state': {'label': _('Status'), 'sequence': 50},
|
||||
'partner_id': {'label': _('Customer'), 'sequence': 60},
|
||||
}
|
||||
|
||||
def _ticket_get_search_domain(self, search_in, search):
|
||||
if search_in == 'name':
|
||||
return ['|', ('name', 'ilike', search), ('ticket_ref', 'ilike', search)]
|
||||
elif search_in == 'user_id':
|
||||
assignees = request.env['res.users'].sudo()._search([('name', 'ilike', search)])
|
||||
return [('user_id', 'in', assignees)]
|
||||
elif search_in in self._ticket_get_searchbar_inputs():
|
||||
return [(search_in, 'ilike', search)]
|
||||
else:
|
||||
return FALSE_DOMAIN
|
||||
|
||||
def _prepare_my_tickets_values(self, page=1, date_begin=None, date_end=None, sortby=None, filterby='all', search=None, groupby='none', search_in='name'):
|
||||
values = self._prepare_portal_layout_values()
|
||||
domain = self._prepare_helpdesk_tickets_domain()
|
||||
|
||||
searchbar_sortings = {
|
||||
'create_date desc': {'label': _('Newest')},
|
||||
'id desc': {'label': _('Reference')},
|
||||
'name': {'label': _('Subject')},
|
||||
'user_id': {'label': _('Assigned to')},
|
||||
'stage_id': {'label': _('Stage')},
|
||||
'date_last_stage_update desc': {'label': _('Last Stage Update')},
|
||||
}
|
||||
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)]},
|
||||
}
|
||||
searchbar_inputs = dict(sorted(self._ticket_get_searchbar_inputs().items(), key=lambda item: item[1]['sequence']))
|
||||
searchbar_groupby = dict(sorted(self._ticket_get_searchbar_groupby().items(), key=lambda item: item[1]['sequence']))
|
||||
|
||||
# default sort by value
|
||||
if not sortby:
|
||||
sortby = 'create_date desc'
|
||||
|
||||
domain = AND([domain, searchbar_filters[filterby]['domain']])
|
||||
|
||||
if date_begin and date_end:
|
||||
domain = AND([domain, [('create_date', '>', date_begin), ('create_date', '<=', date_end)]])
|
||||
|
||||
# search
|
||||
if search and search_in:
|
||||
domain = AND([domain, self._ticket_get_search_domain(search_in, search)])
|
||||
|
||||
# pager
|
||||
tickets_count = request.env['helpdesk.ticket'].search_count(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, 'groupby': groupby, 'filterby': filterby},
|
||||
total=tickets_count,
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
|
||||
order = f'{groupby}, {sortby}' if groupby != 'none' else sortby
|
||||
tickets = request.env['helpdesk.ticket'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
|
||||
request.session['my_tickets_history'] = tickets.ids[:100]
|
||||
|
||||
if not tickets:
|
||||
grouped_tickets = []
|
||||
elif groupby != 'none':
|
||||
grouped_tickets = [request.env['helpdesk.ticket'].concat(*g) for k, g in groupbyelem(tickets, itemgetter(groupby))]
|
||||
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 values
|
||||
|
||||
@http.route(['/my/tickets', '/my/tickets/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def my_helpdesk_tickets(self, page=1, date_begin=None, date_end=None, sortby=None, filterby='all', search=None, groupby='none', search_in='name', **kw):
|
||||
values = self._prepare_my_tickets_values(page, date_begin, date_end, sortby, filterby, search, groupby, search_in)
|
||||
return request.render("helpdesk.portal_helpdesk_ticket", values)
|
||||
|
||||
@http.route([
|
||||
"/helpdesk/ticket/<int:ticket_id>",
|
||||
"/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('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("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('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_closed=1' % (ticket_id, access_token or ''))
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="digest.digest_digest_default" model="digest.digest">
|
||||
<field name="kpi_helpdesk_tickets_closed">True</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data>
|
||||
<record id="digest_tip_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="helpdesk.group_helpdesk_manager" />
|
||||
<field name="tip_description" type="html">
|
||||
<div>
|
||||
<t t-set="record" t-value="object.env['helpdesk.team'].search([('alias_name', '!=', False), ('alias_domain_id', '!=', False)],limit=1)" />
|
||||
<b class="tip_title">Tip: Create tickets from incoming emails</b>
|
||||
<t t-if="record.alias_email">
|
||||
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{record.alias_email}}" target="_blank" style="color: #714B67; text-decoration: none;"><t t-out="record.alias_email" /></a> generate tickets in your pipeline.</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<p class="tip_content">Emails sent to a Helpdesk Team alias generate tickets in your pipeline.</p>
|
||||
</t>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="helpdesk_team1" model="helpdesk.team">
|
||||
<field name="name">Customer Care</field>
|
||||
<field name="alias_name">customer-care</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"/>
|
||||
<field name="member_ids" eval="[Command.link(ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- stage "New" gets created by default with sequence 0-->
|
||||
<record id="stage_new" model="helpdesk.stage">
|
||||
<field name="name">New</field>
|
||||
<field name="sequence">0</field>
|
||||
<field name="team_ids" eval="[(4, ref('helpdesk_team1'))]"/>
|
||||
<field name="template_id" ref="helpdesk.new_ticket_request_email_template"/>
|
||||
</record>
|
||||
<record id="stage_in_progress" model="helpdesk.stage">
|
||||
<field name="name">In Progress</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="team_ids" eval="[(4, ref('helpdesk_team1'))]"/>
|
||||
</record>
|
||||
<record id="stage_on_hold" model="helpdesk.stage">
|
||||
<field name="name">On Hold</field>
|
||||
<field name="sequence">2</field>
|
||||
<field name="team_ids" eval="[(4, ref('helpdesk_team1'))]"/>
|
||||
</record>
|
||||
<record id="stage_solved" model="helpdesk.stage">
|
||||
<field name="name">Solved</field>
|
||||
<field name="team_ids" eval="[(4, ref('helpdesk_team1'))]"/>
|
||||
<field name="sequence">3</field>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
<record id="stage_cancelled" model="helpdesk.stage">
|
||||
<field name="name">Cancelled</field>
|
||||
<field name="sequence">4</field>
|
||||
<field name="team_ids" eval="[(4, ref('helpdesk_team1'))]"/>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(3, ref('helpdesk.group_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<record id="tag_crm" model="helpdesk.tag">
|
||||
<field name="name">CRM</field>
|
||||
</record>
|
||||
<record id="tag_website" model="helpdesk.tag">
|
||||
<field name="name">Website</field>
|
||||
</record>
|
||||
<record id="tag_service" model="helpdesk.tag">
|
||||
<field name="name">Service</field>
|
||||
</record>
|
||||
<record id="tag_repair" model="helpdesk.tag">
|
||||
<field name="name">Repair</field>
|
||||
</record>
|
||||
|
||||
<!-- Set target on user -->
|
||||
<record id="base.user_admin" model="res.users">
|
||||
<field name="helpdesk_target_closed">5</field>
|
||||
<field name="helpdesk_target_rating">4.5</field>
|
||||
<field name="helpdesk_target_success">87</field>
|
||||
</record>
|
||||
|
||||
<!-- helpdesk team -->
|
||||
<record id="helpdesk_team3" model="helpdesk.team">
|
||||
<field name="name">VIP Support</field>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('helpdesk.stage_new'), ref('helpdesk.stage_in_progress'), ref('helpdesk.stage_solved'), ref('helpdesk.stage_cancelled')])]"/>
|
||||
<field name="use_sla" eval="True"/>
|
||||
<field name="use_rating" eval="True"/>
|
||||
<field name="color">10</field>
|
||||
<field name="assign_method">randomly</field>
|
||||
<field name="member_ids" eval="[Command.link(ref('base.user_admin')), Command.link(ref('base.user_demo'))]"/>
|
||||
<field name="privacy_visibility">invited_internal</field>
|
||||
<field name="description" type="html">
|
||||
<p>We provide 24/7 support, Monday through Friday. Ticket responses are usually provided within 2 working days.<br/>
|
||||
Support is mainly provided in English. We can also assist in Spanish, French, and Dutch.</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk.helpdesk_team1" model="helpdesk.team">
|
||||
<field name="use_rating" eval="True"/>
|
||||
<field name="description" type="html">
|
||||
<p>We provide 24/7 support, Monday through Friday. Ticket responses are usually provided within 2 working days.<br/>
|
||||
Support is mainly provided in English. We can also assist in Spanish, French, and Dutch.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SLA's -->
|
||||
<record id="helpdesk_sla_1" model="helpdesk.sla">
|
||||
<field name="name">2 days to start</field>
|
||||
<field name="team_id" ref="helpdesk_team1"/>
|
||||
<field name="stage_id" ref="stage_in_progress"/>
|
||||
<field name="time">16</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_2" model="helpdesk.sla">
|
||||
<field name="name">7 days to finish</field>
|
||||
<field name="team_id" ref="helpdesk_team1"/>
|
||||
<field name="stage_id" ref="stage_solved"/>
|
||||
<field name="time">56</field>
|
||||
<field name="exclude_stage_ids" eval="[Command.link(ref('helpdesk.stage_on_hold'))]"/>
|
||||
</record>
|
||||
<record id="helpdesk_sla_3" model="helpdesk.sla">
|
||||
<field name="name">8 hours to finish</field>
|
||||
<field name="team_id" ref="helpdesk_team3"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="stage_id" ref="stage_solved"/>
|
||||
<field name="time">8</field>
|
||||
<field name="partner_ids" eval="[Command.link(ref('base.res_partner_10')), Command.link(ref('base.res_partner_2'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Tickets -->
|
||||
<record id="helpdesk_ticket_1" model="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="helpdesk.stage_in_progress"/>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_2" model="helpdesk.ticket">
|
||||
<field name="name">Where can I download a catalog?</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_3" model="helpdesk.ticket">
|
||||
<field name="name">Warranty</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="partner_id" ref="base.res_partner_main1" />
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="kanban_state">blocked</field>
|
||||
<field name="description" type="html">
|
||||
<field name="sla_reached_late" eval="True"/>
|
||||
<p>Hello,<br/><br/>
|
||||
I would like to know what kind of warranties you are offering for your products.<br/>
|
||||
Here is my contact number: 123456789<br/><br/>
|
||||
Thank you,<br/>
|
||||
Chester Reed</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- fail the sla status -->
|
||||
<function model="helpdesk.sla.status" name="write">
|
||||
<value model="helpdesk.sla.status" search="[('ticket_id', '=', ref('helpdesk_ticket_3'))]"/>
|
||||
<value eval="{'deadline': DateTime.now() - relativedelta(days=2)}"/>
|
||||
</function>
|
||||
|
||||
<record id="helpdesk_ticket_4" model="helpdesk.ticket">
|
||||
<field name="name">Wood Treatment</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_4" />
|
||||
<field name="stage_id" ref="helpdesk.stage_solved"/>
|
||||
<field name="close_date" eval="DateTime.now()"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
Is the wood from your furniture treated with a particular product? What would you recommend to maintain the quality of a dining table?<br/>
|
||||
Your assistance would be greatly appreciated.<br/><br/>
|
||||
Thanks in Advance,<br/>
|
||||
Azure Interior</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_5" model="helpdesk.ticket">
|
||||
<field name="name">Chair dimensions</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_solved"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=1)"/>
|
||||
<field name="close_date" eval="DateTime.now()"/>
|
||||
<field name="description" type="html">
|
||||
<p>Can you please tell me the dimensions of your “Office chair Black”? Also I am unable to find the information on your official site.<br/>
|
||||
I look forward to your kind response.<br/><br/>
|
||||
Thank you!</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_6" model="helpdesk.ticket">
|
||||
<field name="name">Lost key</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
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?<br/><br/>
|
||||
Thanks in advance for your help.<br/>
|
||||
Kind regards,<br/>
|
||||
Gemini Furniture</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_7" model="helpdesk.ticket">
|
||||
<field name="name">Furniture delivery</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=9)"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
I was wondering if you were delivering the furniture or if we needed to pick it up at your warehouse?<br/>
|
||||
If you do take care of the delivery, are there any extra costs?<br/><br/>
|
||||
Regards,<br/>
|
||||
Deco Addict</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- change the stage to cancelled and add close date to update success rate-->
|
||||
<function model="helpdesk.ticket" name="write">
|
||||
<value model="helpdesk.ticket" search="[('id', '=', ref('helpdesk_ticket_7'))]"/>
|
||||
<value eval="{'stage_id': ref('helpdesk.stage_cancelled'), 'close_date': DateTime.now()}"/>
|
||||
</function>
|
||||
|
||||
<record id="helpdesk_ticket_8" model="helpdesk.ticket">
|
||||
<field name="name">Cabinets in kit</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
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.<br/><br/>
|
||||
Thank you for your help.<br/>
|
||||
Best regards,<br/>
|
||||
Jackson Group</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_9" model="helpdesk.ticket">
|
||||
<field name="name">Missing user manual</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="kanban_state">blocked</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I recently purchased one of your wardrobes in a kit. Unfortunately, I didn’t receive the user manual, so I cannot assemble the item. Could you send me this document?<br/><br/>
|
||||
Thank you.<br/>
|
||||
Kind regards,</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_10" model="helpdesk.ticket">
|
||||
<field name="name">Ugly Chair</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_solved"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="close_date" eval="DateTime.now()"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I purchased a chair from you last week. I now realize it doesn’t go well with the rest of my furniture, so I would like to return it and to get a refund.<br/><br/>
|
||||
Regards,<br/>
|
||||
Deco Addict</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_11" model="helpdesk.ticket">
|
||||
<field name="name">Couch</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
The couch I ordered was scratched during the delivery. Would it be possible to have a gesture of goodwill?<br/>
|
||||
Thank you for considering my request.<br/><br/>
|
||||
Best regards,</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_12" model="helpdesk.ticket">
|
||||
<field name="name">Chair wheels aren’t working</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="partner_id" ref="base.res_partner_main1"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('helpdesk.tag_repair'),ref('helpdesk.tag_service')])]"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=1)"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="description" type="html">
|
||||
<p>The chair I bought last year isn't turning correctly anymore. Are you selling spare parts for the wheels?<br/><br/>
|
||||
Thank you in advance for your help.<br/>
|
||||
Chester Reed</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Fail the sla on ticket -->
|
||||
<function model="helpdesk.sla.status" name="write">
|
||||
<value model="helpdesk.sla.status" search="[('ticket_id', '=', ref('helpdesk_ticket_12'))]"/>
|
||||
<value eval="{'deadline': DateTime.now()}"/>
|
||||
</function>
|
||||
|
||||
<record id="helpdesk_ticket_13" model="helpdesk.ticket">
|
||||
<field name="name">Cabinet Colour and Lock aren't proper</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_10"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('helpdesk.tag_repair'),ref('helpdesk.tag_service')])]"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
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.<br/><br/>
|
||||
Regards,<br/>
|
||||
The Jackson Group</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_14" model="helpdesk.ticket">
|
||||
<field name="name">Lamp Stand is bent</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_new"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
Yesterday I purchased a lamp stand from your site but the product I received is bent.<br/>
|
||||
Would it be possible to get a replacement?<br/><br/>
|
||||
Regards,<br/>
|
||||
Ready Mat</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_15" model="helpdesk.ticket">
|
||||
<field name="name">Table legs are unbalanced</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[(6,0,[ref('helpdesk.tag_repair'),ref('helpdesk.tag_service')])]"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
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?<br/>
|
||||
Kindly do this as early as possible.<br/><br/>
|
||||
Best,<br/>
|
||||
Azure Interior</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_16" model="helpdesk.ticket">
|
||||
<field name="name">Drawer’s slides and handle have a defect</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="kanban_state">done</field>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
I have purchased a "Drawer" from your store but the slides and the handle seem to have a defect.<br/>
|
||||
Would it be possible for you to fix it?<br/><br/>
|
||||
Regards,<br/>
|
||||
Deco</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_17" model="helpdesk.ticket">
|
||||
<field name="name">Want to change the place of the dining area</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I want to change the location of the dining area and would like your advice.<br/>
|
||||
Hope to hear from you soon.<br/><br/>
|
||||
Best,<br/>
|
||||
Gemini Furniture</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_18" model="helpdesk.ticket">
|
||||
<field name="name">Received Product is damaged</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_solved"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=10)"/>
|
||||
<field name="close_date" eval="DateTime.now()- relativedelta(days=5)"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
I ordered a "Table Kit" from your store but the delivered product is damaged. I demand a refund as soon as possible.<br/><br/>
|
||||
Regards,</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_19" model="helpdesk.ticket">
|
||||
<field name="name">Delivered wood panel is not what I ordered</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="priority">2</field>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_solved"/>
|
||||
<field name="create_date" eval="DateTime.now()- relativedelta(days=4)"/>
|
||||
<field name="close_date" eval="DateTime.now()- relativedelta(days=2)"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I ordered a wood panel from your online store, but the delivered product is not what I had ordered.<br/><br/>
|
||||
Could you please replace it with the right product?<br/>
|
||||
Waiting for your response.<br/><br/>
|
||||
Best,<br/>
|
||||
Wood Corner</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_20" model="helpdesk.ticket">
|
||||
<field name="name">Table not delivered on time</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team3"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="stage_id" ref="helpdesk.stage_in_progress"/>
|
||||
<field name="priority">3</field>
|
||||
<field name="tag_ids" eval="[Command.link(ref('helpdesk.tag_service'))]"/>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hi,<br/><br/>
|
||||
A few days ago, I bought a Four Persons Desk. but it's still not delivered?<br/>
|
||||
Kindly do this as early as possible.<br/><br/>
|
||||
Best,<br/>
|
||||
Azure Interior</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_21" model="helpdesk.ticket">
|
||||
<field name="name">Where can I shop the same product?</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="priority">0</field>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_22" model="helpdesk.ticket">
|
||||
<field name="name">What's the best kitchen cabinet varnish for your home?</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_main1"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I would like to know What's the best kitchen cabinet varnish for your home?<br/><br/>
|
||||
Thank you,<br/>
|
||||
Chester Reed</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- fail the sla status -->
|
||||
<function model="helpdesk.sla.status" name="write">
|
||||
<value model="helpdesk.sla.status" search="[('ticket_id', '=', ref('helpdesk_ticket_22'))]"/>
|
||||
<value eval="{'deadline': DateTime.now() - relativedelta(days=1)}"/>
|
||||
</function>
|
||||
<record id="helpdesk_ticket_23" model="helpdesk.ticket">
|
||||
<field name="name">What customization features are available for cabinet?</field>
|
||||
<field name="team_id" ref="helpdesk.helpdesk_team1"/>
|
||||
<field name="priority">1</field>
|
||||
<field name="stage_id" ref="stage_in_progress"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_main1"/>
|
||||
<field name="description" type="html">
|
||||
<p>Hello,<br/><br/>
|
||||
I would like to know What customization features are available for cabinet?<br/><br/>
|
||||
Thank you,<br/>
|
||||
Chester Reed</p>
|
||||
</field>
|
||||
</record>
|
||||
<!-- fail the sla status -->
|
||||
<function model="helpdesk.sla.status" name="write">
|
||||
<value model="helpdesk.sla.status" search="[('ticket_id', '=', ref('helpdesk_ticket_23')), ('status', '=', 'ongoing')]"/>
|
||||
<value eval="{'deadline': DateTime.now() - relativedelta(days=4)}"/>
|
||||
</function>
|
||||
|
||||
<record id="rating_ticket_1" model="rating.rating">
|
||||
<field name="access_token">HELPDESK_1</field>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="rated_partner_id" ref="base.partner_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_18"/>
|
||||
</record>
|
||||
<record id="rating_ticket_2" model="rating.rating">
|
||||
<field name="access_token">HELPDESK_2</field>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="rated_partner_id" ref="base.partner_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_19"/>
|
||||
</record>
|
||||
<record id="rating_ticket_3" model="rating.rating">
|
||||
<field name="access_token">HELPDESK_3</field>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="rated_partner_id" ref="base.partner_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_12"/>
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_5"/>
|
||||
</record>
|
||||
<record id="rating_ticket_4" model="rating.rating">
|
||||
<field name="access_token">HELPDESK_4</field>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="rated_partner_id" ref="base.partner_admin"/>
|
||||
<field name="partner_id" ref="base.res_partner_2"/>
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_10"/>
|
||||
</record>
|
||||
<record id="rating_ticket_5" model="rating.rating">
|
||||
<field name="access_token">HELPDESK_5</field>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="rated_partner_id" ref="base.partner_demo"/>
|
||||
<field name="partner_id" ref="base.res_partner_4"/>
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_4"/>
|
||||
</record>
|
||||
|
||||
<function model="helpdesk.ticket" name="rating_apply"
|
||||
eval="([ref('helpdesk_ticket_18')], 3, 'HELPDESK_1', None, 'Good Service')"/>
|
||||
<function model="helpdesk.ticket" name="rating_apply"
|
||||
eval="([ref('helpdesk_ticket_19')], 5, 'HELPDESK_2', None, 'Awesome Service.\nLove to use your product')"/>
|
||||
<function model="helpdesk.ticket" name="rating_apply"
|
||||
eval="([ref('helpdesk_ticket_5')], 5, 'HELPDESK_3', None, 'Quick response with detailed information.')"/>
|
||||
<function model="helpdesk.ticket" name="rating_apply"
|
||||
eval="([ref('helpdesk_ticket_10')], 3, 'HELPDESK_4', None, 'Quick replacement. \n Love that you did the quick replacement and refund.')"/>
|
||||
<function model="helpdesk.ticket" name="rating_apply"
|
||||
eval="([ref('helpdesk_ticket_4')], 1, 'HELPDESK_5', None, 'Too late. \n dissatisfied answer.')"/>
|
||||
|
||||
<!-- Activities -->
|
||||
<record id="helpdesk_activity_1" model="mail.activity">
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_3"/>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="DateTime.today()"/>
|
||||
<field name="summary">Provide warranty details</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record id="helpdesk_activity_2" model="mail.activity">
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_12"/>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() + relativedelta(days=2))"/>
|
||||
<field name="summary">Provide the details about the spare part of chair</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record id="helpdesk_activity_3" model="mail.activity">
|
||||
<field name="res_id" ref="helpdesk.helpdesk_ticket_11"/>
|
||||
<field name="res_model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="activity_type_id" ref="mail.mail_activity_data_todo"/>
|
||||
<field name="date_deadline" eval="(DateTime.today() - relativedelta(days=2))" />
|
||||
<field name="summary">Give gesture of goodwill</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="user_id" ref="base.user_admin"/>
|
||||
</record>
|
||||
|
||||
<!-- Mail template Auto delete false for demo data -->
|
||||
<record id="helpdesk.rating_ticket_request_email_template" model="mail.template">
|
||||
<field name="auto_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_tour" model="web_tour.tour">
|
||||
<field name="name">helpdesk_tour</field>
|
||||
<field name="sequence">220</field>
|
||||
<field name="rainbow_man_message"><![CDATA[
|
||||
<center><strong><b>Good job!</b> You walked through all steps of this tour.</strong></center>
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="ir_cron_auto_close_ticket" model="ir.cron">
|
||||
<field name="name">Helpdesk Ticket: Automatically close the tickets</field>
|
||||
<field name="model_id" ref="model_helpdesk_team"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_auto_close_tickets()</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="nextcall" eval="(DateTime.now().replace(hour=1, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Sequences for helpdesk.ticket -->
|
||||
<record id="seq_helpdesk_ticket" model="ir.sequence">
|
||||
<field name="name">Helpdesk Ticket</field>
|
||||
<field name="code">helpdesk.ticket</field>
|
||||
<field name="padding">2</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<!-- 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">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">20</field>
|
||||
<field name="res_model">helpdesk.ticket</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="hidden" eval="False"/>
|
||||
</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">helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="description">Stage Changed</field>
|
||||
</record>
|
||||
<record id="mt_ticket_refund_status" model="mail.message.subtype">
|
||||
<field name="name">Refund Status</field>
|
||||
<field name="sequence">11</field>
|
||||
<field name="res_model">helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="description"></field>
|
||||
</record>
|
||||
<record id="mt_ticket_return_status" model="mail.message.subtype">
|
||||
<field name="name">Return Status</field>
|
||||
<field name="sequence">13</field>
|
||||
<field name="res_model">helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="description"></field>
|
||||
</record>
|
||||
<record id="mt_ticket_repair_status" model="mail.message.subtype">
|
||||
<field name="name">Repair Status</field>
|
||||
<field name="sequence">15</field>
|
||||
<field name="res_model">helpdesk.ticket</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="description"></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">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">15</field>
|
||||
<field name="res_model">helpdesk.team</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="internal" eval="True"/>
|
||||
<field name="parent_id" ref="mt_ticket_rated"/>
|
||||
<field name="relation_field">team_id</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<record id="new_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Helpdesk: Ticket Received</field>
|
||||
<field name="model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="subject">{{ object.name }}</field>
|
||||
<field name="email_from">{{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }}</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 '') }}</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="description">Send customers a confirmation email to notify them that their helpdesk ticket has been received and is currently being reviewed by the helpdesk team. Automatically send an email to customers when a ticket reaches a specific stage in a helpdesk team by setting this template on that stage.</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
Dear <t t-out="object.sudo().partner_id.name or object.sudo().partner_name or 'Madam/Sir'">Madam/Sir</t>,<br /><br />
|
||||
Your request
|
||||
<t t-if="hasattr(object.team_id, 'website_id') and object.get_portal_url()">
|
||||
<a t-attf-href="{{ object.team_id.website_id.domain }}/my/ticket/{{ object.id }}/{{ object.access_token }}" t-out="object.name or ''">Table legs are unbalanced</a>
|
||||
</t>
|
||||
has been received and is being reviewed by our <t t-out="object.team_id.name or ''">VIP Support</t> team.<br/><br/>
|
||||
The reference for your ticket is <strong><t t-out="object.ticket_ref or ''">15</t></strong>.<br /><br/>
|
||||
|
||||
To provide any additional information, simply reply to this email.<br/><br/>
|
||||
<t t-if="object.team_id.show_knowledge_base">
|
||||
Don't hesitate to visit our <a t-attf-href="{{ object.team_id.get_knowledge_base_url() }}">Help Center</a>. You might find the answer to your question.
|
||||
<br/><br/>
|
||||
</t>
|
||||
<t t-if="object.team_id.allow_portal_ticket_closing">
|
||||
Feel free to close your ticket if our help is no longer needed. Thank you for your collaboration.<br/><br/>
|
||||
</t>
|
||||
|
||||
<div style="text-align: center; padding: 16px 0px 16px 0px;">
|
||||
<t t-if="hasattr(object.team_id, 'website_id') and object.team_id.use_website_helpdesk_form">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 13px;" t-att-href="'%s%s' % (object.team_id.website_id.domain or '', object.get_portal_url())" target="_blank">View Ticket</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;" t-att-href="object.get_portal_url()" target="_blank">View Ticket</a>
|
||||
</t>
|
||||
<t t-if="hasattr(object.team_id, 'website_id') and object.team_id.allow_portal_ticket_closing">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;" t-att-href="'%s/my/ticket/close/%s/%s' % (object.team_id.website_id.domain or '', object.id, object.access_token)" target="_blank">Close Ticket</a>
|
||||
</t>
|
||||
<t t-elif="object.team_id.allow_portal_ticket_closing">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;" t-att-href="'/my/ticket/close/%s/%s' % (object.id, object.access_token)" target="_blank">Close Ticket</a>
|
||||
</t>
|
||||
<t t-if="object.team_id.use_website_helpdesk_forum or object.team_id.use_website_helpdesk_knowledge or object.team_id.use_website_helpdesk_slides">
|
||||
<a style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;" t-att-href="object.team_id.feature_form_url" target="_blank">Visit Help Center</a>
|
||||
</t><br/><br/>
|
||||
</div>
|
||||
|
||||
Best regards,<br/><br/>
|
||||
<t t-out="object.team_id.name or 'Helpdesk'">Helpdesk</t> Team
|
||||
</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>
|
||||
|
||||
<record id="solved_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Helpdesk: Ticket Closed</field>
|
||||
<field name="model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="subject">Ticket Closed - Reference {{ object.id if object.id else 15 }}</field>
|
||||
<field name="email_from">{{ (object.team_id.alias_email_from or object.company_id.email_formatted or object.user_id.email_formatted or user.email_formatted) }}</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 '') }}</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="description">Set this template on a project's stage to automate email when tasks reach stages</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
Dear <t t-out="object.sudo().partner_id.name or 'Madam/Sir'">Madam/Sir</t>,<br /><br />
|
||||
We would like to inform you that we have closed your ticket (reference <t t-out="object.id or ''">15</t>).
|
||||
We trust that the services provided have met your expectations and that you have found a satisfactory resolution to your issue.<br /><br />
|
||||
However, if you have any further questions or comments, please do not hesitate to reply to this email to re-open your ticket.
|
||||
Our team is always here to help you and we will be happy to assist you with any further concerns you may have.<br /><br />
|
||||
Thank you for choosing our services and for your cooperation throughout this process. We truly value your business and appreciate the opportunity to serve you.<br /><br />
|
||||
Kind regards,<br /><br />
|
||||
<t t-out="object.team_id.name or 'Helpdesk'">Helpdesk</t> Team.
|
||||
</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>
|
||||
|
||||
<record id="rating_ticket_request_email_template" model="mail.template">
|
||||
<field name="name">Helpdesk: Ticket Rating Request</field>
|
||||
<field name="model_id" ref="helpdesk.model_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.team_id.alias_email_from or object.company_id.email_formatted or object._rating_get_operator().email_formatted or user.email_formatted) }}</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 '') }}</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="description">Enable "customer ratings" feature on the helpdesk team</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<t t-set="access_token" t-value="object._rating_get_access_token()"/>
|
||||
<t t-set="partner" t-value="object._rating_get_partner()"/>
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0;">
|
||||
<tbody>
|
||||
<tr><td valign="top" style="font-size: 14px;">
|
||||
<t t-if="partner.name">
|
||||
Hello <t t-out="partner.name or ''">Brandon Freeman</t>,<br/><br/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Hello,<br/>
|
||||
</t>
|
||||
Please take a moment to rate our services related to the ticket "<strong t-out="object.name or ''">Table legs are unbalanced</strong>"
|
||||
<t t-if="object._rating_get_operator().name">
|
||||
assigned to <strong t-out="object._rating_get_operator().name or ''">Mitchell Admin</strong>.<br/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
.<br/><br/>
|
||||
</t>
|
||||
</td></tr>
|
||||
<tr><td style="text-align: center;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin: 32px 0px 32px 0px; display: inline-table;">
|
||||
<tr><td style="font-size: 14px; text-align:center;">
|
||||
<strong>Tell us how you feel about our services</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;margin-top:2rem;">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/rate/{{ access_token }}/5" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||
<img alt="Satisfied" src="/rating/static/src/img/rating_5.png" title="Satisfied"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a t-attf-href="/rate/{{ access_token }}/3" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||
<img alt="Okay" src="/rating/static/src/img/rating_3.png" title="Okay"/>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a t-attf-href="/rate/{{ access_token }}/1" t-att-class="'pe-none' if object._rating_get_operator() else ''">
|
||||
<img alt="Dissatisfied" src="/rating/static/src/img/rating_1.png" title="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 improve continuously.
|
||||
<br/><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 t-out="object.stage_id.name or ''">In Progress</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>
|
||||
<!-- You have been invited to follow the ticket -->
|
||||
<template id="ticket_invitation_follower">
|
||||
<div>
|
||||
Hello <t t-out="partner_name"/>,
|
||||
<br/><br/>
|
||||
<span style="margin-top: 8px;">You have been invited to follow Ticket Document : <t t-out="object.display_name"/>.</span>
|
||||
</div>
|
||||
</template>
|
||||
</data></odoo>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def migrate(cr, version):
|
||||
cr.execute("""
|
||||
UPDATE helpdesk_sla
|
||||
SET time_days = COALESCE(time_days, 0),
|
||||
time_hours = COALESCE(time_hours, 0),
|
||||
time_minutes = COALESCE(time_minutes, 0)
|
||||
""")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import digest
|
||||
from . import ir_module
|
||||
from . import ir_ui_menu
|
||||
from . import helpdesk_team
|
||||
from . import helpdesk_stage
|
||||
from . import helpdesk_sla_status
|
||||
from . import helpdesk_sla
|
||||
from . import helpdesk_ticket
|
||||
from . import helpdesk_tag
|
||||
from . import mail_message
|
||||
from . import res_users
|
||||
from . import res_partner
|
||||
from . import res_company
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_helpdesk_tickets_closed = fields.Boolean('Tickets Closed')
|
||||
kpi_helpdesk_tickets_closed_value = fields.Integer(compute='_compute_kpi_helpdesk_tickets_closed_value', export_string_translation=False)
|
||||
|
||||
def _compute_kpi_helpdesk_tickets_closed_value(self):
|
||||
if not self.env.user.has_group('helpdesk.group_helpdesk_user'):
|
||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||
|
||||
self._calculate_company_based_kpi(
|
||||
'helpdesk.ticket',
|
||||
'kpi_helpdesk_tickets_closed_value',
|
||||
date_field='close_date',
|
||||
)
|
||||
|
||||
def _compute_kpis_actions(self, company, user):
|
||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
||||
res['kpi_helpdesk_tickets_closed'] = 'helpdesk.helpdesk_team_dashboard_action_main&menu_id=%s' % self.env.ref('helpdesk.menu_helpdesk_root').id
|
||||
return res
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.helpdesk.models.helpdesk_ticket import TICKET_PRIORITY
|
||||
|
||||
|
||||
class HelpdeskSLA(models.Model):
|
||||
_name = "helpdesk.sla"
|
||||
_order = "name"
|
||||
_description = "Helpdesk SLA Policies"
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
if 'team_id' in fields_list or 'stage_id' in fields_list:
|
||||
default_team_id = self._context.get('default_team_id')
|
||||
team = self.env['helpdesk.team'].browse(default_team_id)
|
||||
if not default_team_id:
|
||||
defaults['team_id'] = team.id
|
||||
team = self.env['helpdesk.team'].search([], limit=1)
|
||||
stages = team.stage_ids.filtered(lambda x: x.fold)
|
||||
defaults['stage_id'] = stages and stages.ids[0] or team.stage_ids and team.stage_ids.ids[-1]
|
||||
return defaults
|
||||
|
||||
name = fields.Char(required=True, index=True, translate=True)
|
||||
description = fields.Html('SLA Policy Description', translate=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
team_id = fields.Many2one('helpdesk.team', 'Helpdesk Team', required=True)
|
||||
tag_ids = fields.Many2many(
|
||||
'helpdesk.tag', string='Tags')
|
||||
stage_id = fields.Many2one(
|
||||
'helpdesk.stage', 'Target Stage',
|
||||
help='Minimum stage a ticket needs to reach in order to satisfy this SLA.')
|
||||
exclude_stage_ids = fields.Many2many(
|
||||
'helpdesk.stage', string='Excluding Stages', copy=True,
|
||||
domain="[('id', '!=', stage_id.id)]",
|
||||
help="The time spent in these stages won't be taken into account in the calculation of the SLA.")
|
||||
priority = fields.Selection(
|
||||
TICKET_PRIORITY, string='Priority',
|
||||
default='0', required=True)
|
||||
partner_ids = fields.Many2many(
|
||||
'res.partner', string="Customers")
|
||||
company_id = fields.Many2one('res.company', 'Company', related='team_id.company_id', readonly=True, store=True)
|
||||
time = fields.Float('Within', default=0, required=True,
|
||||
help='Maximum number of working hours a ticket should take to reach the target stage, starting from the date it was created.')
|
||||
ticket_count = fields.Integer(compute='_compute_ticket_count')
|
||||
|
||||
def _compute_ticket_count(self):
|
||||
res = self.env['helpdesk.ticket']._read_group(
|
||||
[('sla_ids', 'in', self.ids), ('stage_id.fold', '=', False)],
|
||||
['sla_ids'], ['__count'])
|
||||
sla_data = {sla.id: count for sla, count in res}
|
||||
for sla in self:
|
||||
sla.ticket_count = sla_data.get(sla.id, 0)
|
||||
|
||||
@api.depends('team_id')
|
||||
@api.depends_context('with_team_name')
|
||||
def _compute_display_name(self):
|
||||
if not self._context.get('with_team_name'):
|
||||
return super()._compute_display_name()
|
||||
for sla in self:
|
||||
sla.display_name = f'{sla.name} - {sla.team_id.name}'
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", sla.name)) for sla, vals in zip(self, vals_list)]
|
||||
|
||||
def action_open_helpdesk_ticket(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("helpdesk.helpdesk_ticket_action_main_tree")
|
||||
action.update({
|
||||
'domain': [('sla_ids', 'in', self.ids)],
|
||||
'context': {
|
||||
'search_default_is_open': True,
|
||||
'create': False,
|
||||
},
|
||||
})
|
||||
return action
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import math
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo.osv import expression
|
||||
|
||||
class HelpdeskSLAStatus(models.Model):
|
||||
_name = 'helpdesk.sla.status'
|
||||
_description = "Ticket SLA Status"
|
||||
_table = 'helpdesk_sla_status'
|
||||
_order = 'deadline ASC, sla_stage_id'
|
||||
_rec_name = 'sla_id'
|
||||
|
||||
ticket_id = fields.Many2one('helpdesk.ticket', string='Ticket', required=True, ondelete='cascade', index=True)
|
||||
sla_id = fields.Many2one('helpdesk.sla', required=True, ondelete='cascade')
|
||||
sla_stage_id = fields.Many2one('helpdesk.stage', related='sla_id.stage_id', store=True, export_string_translation=False) # need to be stored for the search in `_sla_reach`
|
||||
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_hours = fields.Float("Exceeded Working Hours", compute='_compute_exceeded_hours', compute_sudo=True, store=True, help="Working hours exceeded for reached SLAs compared with deadline. Positive number means the SLA was reached 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 not status.sla_id.exclude_stage_ids) or (status.status == 'failed'):
|
||||
continue
|
||||
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.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
|
||||
|
||||
avg_hour = working_calendar.hours_per_day or 8 # default to 8 working hours/day
|
||||
time_days = math.floor(status.sla_id.time / avg_hour)
|
||||
if time_days > 0:
|
||||
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 = working_calendar.plan_hours(0, status.ticket_id.create_date)
|
||||
deadline = deadline and deadline.replace(hour=create_dt.hour, minute=create_dt.minute, second=create_dt.second, microsecond=create_dt.microsecond)
|
||||
|
||||
sla_hours = status.sla_id.time % avg_hour
|
||||
|
||||
if 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 and time_days > 0:
|
||||
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 = deadline and working_calendar.plan_hours(sla_hours, deadline, compute_leaves=True)
|
||||
|
||||
@api.depends('deadline', 'reached_datetime')
|
||||
def _compute_status(self):
|
||||
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': ['|', ('deadline', '=', False), '&', ('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_hours(self):
|
||||
for status in self:
|
||||
if status.deadline and status.ticket_id.team_id.resource_calendar_id:
|
||||
reached_datetime = status.reached_datetime or fields.Datetime.now()
|
||||
if reached_datetime <= status.deadline:
|
||||
start_dt = reached_datetime
|
||||
end_dt = status.deadline
|
||||
factor = -1
|
||||
else:
|
||||
start_dt = status.deadline
|
||||
end_dt = 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_hours = duration_data['hours'] * factor
|
||||
else:
|
||||
status.exceeded_hours = 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_id == 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
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.tools.misc import unique
|
||||
|
||||
class HelpdeskStage(models.Model):
|
||||
_name = '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)]
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
name = fields.Char(required=True, translate=True)
|
||||
description = fields.Text(translate=True)
|
||||
sequence = fields.Integer(export_string_translation=False, default=10)
|
||||
fold = fields.Boolean(
|
||||
'Folded in Kanban',
|
||||
help='Tickets in a folded stage are considered as closed.')
|
||||
team_ids = fields.Many2many(
|
||||
'helpdesk.team', relation='team_stage_rel', string='Helpdesk Teams',
|
||||
default=_default_team_ids, required=True)
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Email Template',
|
||||
domain="[('model', '=', 'helpdesk.ticket')]",
|
||||
help="Email automatically sent to the customer when the ticket reaches this stage.\n"
|
||||
"By default, the email will be sent from the email alias of the helpdesk team.\n"
|
||||
"Otherwise it will be sent from the company's email address, or from the catchall (as defined in the System Parameters).")
|
||||
legend_blocked = fields.Char(
|
||||
'Red Kanban Label', default=lambda s: s.env._('Blocked'), translate=True, required=True)
|
||||
legend_done = fields.Char(
|
||||
'Green Kanban Label', default=lambda s: s.env._('Ready'), translate=True, required=True)
|
||||
legend_normal = fields.Char(
|
||||
'Grey Kanban Label', default=lambda s: s.env._('In Progress'), translate=True, required=True)
|
||||
ticket_count = fields.Integer(compute='_compute_ticket_count', export_string_translation=False)
|
||||
|
||||
def _compute_ticket_count(self):
|
||||
res = self.env['helpdesk.ticket']._read_group(
|
||||
[('stage_id', 'in', self.ids)],
|
||||
['stage_id'], ['__count'])
|
||||
stage_data = {stage.id: count for stage, count in res}
|
||||
for stage in self:
|
||||
stage.ticket_count = stage_data.get(stage.id, 0)
|
||||
|
||||
def write(self, vals):
|
||||
if 'active' in vals and not vals['active']:
|
||||
self.env['helpdesk.ticket'].search([('stage_id', 'in', self.ids)]).write({'active': False})
|
||||
return super(HelpdeskStage, self).write(vals)
|
||||
|
||||
def toggle_active(self):
|
||||
res = super().toggle_active()
|
||||
stage_active = self.filtered('active')
|
||||
if stage_active and sum(stage_active.with_context(active_test=False).mapped('ticket_count')) > 0:
|
||||
wizard = self.env['helpdesk.stage.delete.wizard'].create({
|
||||
'stage_ids': stage_active.ids,
|
||||
})
|
||||
|
||||
return {
|
||||
'name': _('Unarchive Tickets'),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'helpdesk.stage.delete.wizard',
|
||||
'views': [(self.env.ref('helpdesk.view_helpdesk_stage_unarchive_wizard').id, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': wizard.id,
|
||||
'target': 'new',
|
||||
}
|
||||
return res
|
||||
|
||||
def action_unlink_wizard(self, stage_view=False):
|
||||
self = self.with_context(active_test=False)
|
||||
# retrieves all the teams with a least 1 ticket in that stage
|
||||
# a ticket can be in a stage even if the team is not assigned to the stage
|
||||
readgroup = self.with_context(active_test=False).env['helpdesk.ticket']._read_group(
|
||||
[('stage_id', 'in', self.ids), ('team_id', '!=', False)],
|
||||
['team_id'])
|
||||
team_ids = list(unique([team.id for [team] in readgroup] + self.team_ids.ids))
|
||||
|
||||
wizard = self.env['helpdesk.stage.delete.wizard'].create({
|
||||
'team_ids': team_ids,
|
||||
'stage_ids': self.ids
|
||||
})
|
||||
|
||||
context = dict(self.env.context)
|
||||
context['stage_view'] = stage_view
|
||||
return {
|
||||
'name': _('Delete Stage'),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'helpdesk.stage.delete.wizard',
|
||||
'views': [(self.env.ref('helpdesk.view_helpdesk_stage_delete_wizard').id, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': wizard.id,
|
||||
'target': 'new',
|
||||
'context': context,
|
||||
}
|
||||
|
||||
def action_open_helpdesk_ticket(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("helpdesk.helpdesk_ticket_action_main_tree")
|
||||
action.update({
|
||||
'domain': [('stage_id', 'in', self.ids)],
|
||||
'context': {
|
||||
'default_stage_id': self.id,
|
||||
},
|
||||
})
|
||||
return action
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from random import randint
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
class HelpdeskTag(models.Model):
|
||||
_name = 'helpdesk.tag'
|
||||
_description = 'Helpdesk Tags'
|
||||
_order = 'name'
|
||||
|
||||
def _get_default_color(self):
|
||||
return randint(1, 11)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
color = fields.Integer('Color', default=_get_default_color)
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "A tag with the same name already exists."),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
existing_tag = self.search([('name', '=ilike', name.strip())], limit=1)
|
||||
if existing_tag:
|
||||
return existing_tag.id, existing_tag.display_name
|
||||
return super().name_create(name)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,913 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, Command, fields, models, tools, _
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.addons.web.controllers.utils import clean_action
|
||||
|
||||
TICKET_PRIORITY = [
|
||||
('0', 'Low priority'),
|
||||
('1', 'Medium priority'),
|
||||
('2', 'High priority'),
|
||||
('3', 'Urgent'),
|
||||
]
|
||||
|
||||
class HelpdeskTicket(models.Model):
|
||||
_name = 'helpdesk.ticket'
|
||||
_description = 'Helpdesk Ticket'
|
||||
_order = 'priority desc, id desc'
|
||||
_primary_email = 'partner_email'
|
||||
_inherit = [
|
||||
'portal.mixin',
|
||||
'mail.thread.cc',
|
||||
'utm.mixin',
|
||||
'rating.mixin',
|
||||
'mail.activity.mixin',
|
||||
'mail.tracking.duration.mixin',
|
||||
]
|
||||
_track_duration_field = 'stage_id'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(HelpdeskTicket, self).default_get(fields)
|
||||
if result.get('team_id') and fields:
|
||||
team = self.env['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
|
||||
|
||||
def _default_team_id(self):
|
||||
team_id = self.env['helpdesk.team'].search([('member_ids', 'in', self.env.uid)], limit=1).id
|
||||
if not team_id:
|
||||
team_id = self.env['helpdesk.team'].search([], limit=1).id
|
||||
return team_id
|
||||
|
||||
@api.model
|
||||
def _read_group_stage_ids(self, stages, domain):
|
||||
# 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)
|
||||
|
||||
name = fields.Char(string='Subject', required=True, index=True, tracking=True)
|
||||
team_id = fields.Many2one('helpdesk.team', string='Helpdesk Team', default=_default_team_id, index=True, tracking=True)
|
||||
use_sla = fields.Boolean(related='team_id.use_sla')
|
||||
team_privacy_visibility = fields.Selection(related='team_id.privacy_visibility', export_string_translation=False)
|
||||
description = fields.Html(sanitize_attributes=False)
|
||||
active = fields.Boolean(default=True)
|
||||
tag_ids = fields.Many2many('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', 'In progress'),
|
||||
('done', 'Ready'),
|
||||
('blocked', 'Blocked')], string='Kanban State',
|
||||
copy=False, default='normal', required=True)
|
||||
kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', 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', export_string_translation=False)
|
||||
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('helpdesk.group_helpdesk_user').id)])
|
||||
properties = fields.Properties(
|
||||
'Properties', definition='team_id.ticket_properties',
|
||||
copy=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Customer', tracking=True, index=True)
|
||||
partner_ticket_ids = fields.Many2many('helpdesk.ticket', compute='_compute_partner_ticket_count', string="Partner Tickets")
|
||||
partner_ticket_count = fields.Integer('Number of other tickets from the same partner', compute='_compute_partner_ticket_count')
|
||||
partner_open_ticket_count = fields.Integer('Number of other open tickets from the same partner', compute='_compute_partner_ticket_count')
|
||||
# Used to submit tickets from a contact form
|
||||
partner_name = fields.Char(string='Customer Name', compute='_compute_partner_name', store=True, readonly=False)
|
||||
partner_email = fields.Char(string='Customer Email', compute='_compute_partner_email', inverse="_inverse_partner_email", store=True, readonly=False)
|
||||
partner_phone = fields.Char(string='Customer Phone', compute='_compute_partner_phone', inverse="_inverse_partner_phone", store=True, readonly=False)
|
||||
commercial_partner_id = fields.Many2one(related="partner_id.commercial_partner_id")
|
||||
closed_by_partner = fields.Boolean('Closed by Partner', readonly=True)
|
||||
priority = fields.Selection(TICKET_PRIORITY, string='Priority', default='0', tracking=True)
|
||||
stage_id = fields.Many2one(
|
||||
'helpdesk.stage', string='Stage', compute='_compute_user_and_stage_ids', store=True,
|
||||
readonly=False, ondelete='restrict', tracking=1, group_expand='_read_group_stage_ids',
|
||||
copy=False, index=True, domain="[('team_ids', '=', team_id)]")
|
||||
fold = fields.Boolean(related="stage_id.fold", export_string_translation=False)
|
||||
date_last_stage_update = fields.Datetime("Last Stage Update", copy=False, readonly=True)
|
||||
ticket_ref = fields.Char(string='Ticket IDs Sequence', copy=False, readonly=True, index=True)
|
||||
# next 4 fields are computed in write (or create)
|
||||
assign_date = fields.Datetime("First assignment date")
|
||||
assign_hours = fields.Float("Time to first assignment (hours)", compute='_compute_assign_hours', store=True, aggregator="avg")
|
||||
close_date = fields.Datetime("Close date", copy=False)
|
||||
close_hours = fields.Float("Time to close (hours)", compute='_compute_close_hours', store=True, aggregator="avg")
|
||||
open_hours = fields.Integer("Open Time (hours)", compute='_compute_open_hours', search='_search_open_hours', aggregator="avg")
|
||||
# SLA relative
|
||||
sla_ids = fields.Many2many('helpdesk.sla', 'helpdesk_sla_status', 'ticket_id', 'sla_id', string="SLAs", copy=False)
|
||||
sla_status_ids = fields.One2many('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_reached = fields.Boolean("Has SLA reached", compute='_compute_sla_reached', compute_sudo=True, store=True)
|
||||
sla_deadline = fields.Datetime("SLA Deadline", compute='_compute_sla_deadline', compute_sudo=True, store=True)
|
||||
sla_deadline_hours = fields.Float("Working Hours until SLA Deadline", compute='_compute_sla_deadline', compute_sudo=True, store=True, aggregator="avg")
|
||||
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', export_string_translation=False)
|
||||
use_coupons = fields.Boolean(related='team_id.use_coupons', string='Use Coupons')
|
||||
use_product_returns = fields.Boolean(related='team_id.use_product_returns', export_string_translation=False)
|
||||
use_product_repairs = fields.Boolean(related='team_id.use_product_repairs', export_string_translation=False)
|
||||
use_rating = fields.Boolean(related='team_id.use_rating', export_string_translation=False)
|
||||
|
||||
is_partner_email_update = fields.Boolean(compute='_compute_is_partner_email_update', export_string_translation=False)
|
||||
is_partner_phone_update = fields.Boolean(compute='_compute_is_partner_phone_update', export_string_translation=False)
|
||||
# customer portal: include comment and (incoming/outgoing) emails in communication history
|
||||
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment', 'email_outgoing'])], export_string_translation=False)
|
||||
|
||||
first_response_hours = fields.Float("Hours to First Response", aggregator="avg")
|
||||
avg_response_hours = fields.Float("Average Hours to Respond", aggregator="avg")
|
||||
oldest_unanswered_customer_message_date = fields.Datetime("Oldest Unanswered Customer Message Date", export_string_translation=False)
|
||||
answered_customer_message_count = fields.Integer('# Exchanges', aggregator="avg")
|
||||
total_response_hours = fields.Float("Total Exchange Time in Hours", aggregator="avg")
|
||||
display_extra_info = fields.Boolean(compute="_compute_display_extra_info", export_string_translation=False)
|
||||
|
||||
@api.depends('stage_id', 'kanban_state')
|
||||
def _compute_kanban_state_label(self):
|
||||
for ticket in self:
|
||||
if ticket.kanban_state == 'normal':
|
||||
ticket.kanban_state_label = ticket.legend_normal
|
||||
elif ticket.kanban_state == 'blocked':
|
||||
ticket.kanban_state_label = ticket.legend_blocked
|
||||
else:
|
||||
ticket.kanban_state_label = ticket.legend_done
|
||||
|
||||
@api.depends('team_id')
|
||||
def _compute_domain_user_ids(self):
|
||||
user_ids = self.env.ref('helpdesk.group_helpdesk_user').users.ids
|
||||
for ticket in self:
|
||||
ticket_user_ids = []
|
||||
ticket_sudo = ticket.sudo()
|
||||
if ticket_sudo.team_id and ticket_sudo.team_id.privacy_visibility == 'invited_internal':
|
||||
ticket_user_ids = ticket_sudo.team_id.message_partner_ids.user_ids.ids
|
||||
ticket.domain_user_ids = [Command.set(user_ids + ticket_user_ids)]
|
||||
|
||||
def _compute_access_url(self):
|
||||
super(HelpdeskTicket, self)._compute_access_url()
|
||||
for ticket in self:
|
||||
ticket.access_url = '/my/ticket/%s' % ticket.id
|
||||
|
||||
@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 helpdesk_sla_status
|
||||
WHERE ticket_id IN %s AND (deadline < reached_datetime OR (deadline < %s AND reached_datetime IS NULL))
|
||||
GROUP BY ticket_id
|
||||
""", (tuple(self.ids), fields.Datetime.now()))
|
||||
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_reached(self):
|
||||
sla_status_read_group = self.env['helpdesk.sla.status']._read_group(
|
||||
[('exceeded_hours', '<', 0), ('ticket_id', 'in', self.ids)],
|
||||
['ticket_id'],
|
||||
)
|
||||
sla_status_ids_per_ticket = {ticket.id for [ticket] in sla_status_read_group}
|
||||
for ticket in self:
|
||||
ticket.sla_reached = ticket.id in sla_status_ids_per_ticket
|
||||
|
||||
@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
|
||||
"""
|
||||
now = fields.Datetime.now()
|
||||
for ticket in self:
|
||||
|
||||
# the current team is invalid, no need to compute new values since the transaction will be rolled back anyway.
|
||||
if not ticket.team_id:
|
||||
continue
|
||||
min_deadline = False
|
||||
for status in ticket.sla_status_ids:
|
||||
if status.reached_datetime or not status.deadline:
|
||||
continue
|
||||
if not min_deadline or status.deadline < min_deadline:
|
||||
min_deadline = status.deadline
|
||||
|
||||
ticket.update({
|
||||
'sla_deadline': min_deadline,
|
||||
'sla_deadline_hours': ticket.team_id.resource_calendar_id.get_work_duration_data\
|
||||
(now, min_deadline, compute_leaves=True)['hours'] if min_deadline else 0.0,
|
||||
})
|
||||
|
||||
@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.depends('partner_email', 'partner_id')
|
||||
def _compute_is_partner_email_update(self):
|
||||
for ticket in self:
|
||||
ticket.is_partner_email_update = ticket._get_partner_email_update()
|
||||
|
||||
@api.depends('partner_phone', 'partner_id')
|
||||
def _compute_is_partner_phone_update(self):
|
||||
for ticket in self:
|
||||
ticket.is_partner_phone_update = ticket._get_partner_phone_update()
|
||||
|
||||
@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), '|', ('sla_deadline', '!=', False), ('sla_deadline', '<', datetime_now)]
|
||||
return [('sla_status_ids.reached_datetime', '<', datetime_now), ('sla_reached', '=', True), ('sla_reached_late', '=', False), '|', ('sla_deadline', '=', False), ('sla_deadline', '>=', datetime_now)] # is success
|
||||
|
||||
@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_name(self):
|
||||
for ticket in self:
|
||||
if ticket.partner_id:
|
||||
ticket.partner_name = ticket.partner_id.name
|
||||
|
||||
@api.depends('partner_id.email')
|
||||
def _compute_partner_email(self):
|
||||
for ticket in self:
|
||||
if ticket.partner_id:
|
||||
ticket.partner_email = ticket.partner_id.email
|
||||
|
||||
def _inverse_partner_email(self):
|
||||
for ticket in self:
|
||||
if ticket._get_partner_email_update():
|
||||
ticket.partner_id.email = ticket.partner_email
|
||||
|
||||
@api.depends('partner_id.phone')
|
||||
def _compute_partner_phone(self):
|
||||
for ticket in self:
|
||||
if ticket.partner_id:
|
||||
ticket.partner_phone = ticket.partner_id.phone
|
||||
|
||||
def _inverse_partner_phone(self):
|
||||
for ticket in self:
|
||||
if (ticket._get_partner_phone_update() or not ticket.partner_id.phone) and ticket.partner_phone:
|
||||
ticket = ticket.sudo()
|
||||
ticket.partner_id.phone = ticket.partner_phone
|
||||
|
||||
@api.depends('partner_id', 'partner_email', 'partner_phone')
|
||||
def _compute_partner_ticket_count(self):
|
||||
for ticket in self:
|
||||
partner_tickets = self.search([("partner_id", "child_of", ticket.partner_id.commercial_partner_id.id)]) if ticket.partner_id else ticket
|
||||
ticket.partner_ticket_ids = partner_tickets
|
||||
partner_tickets = partner_tickets - ticket._origin
|
||||
ticket.partner_ticket_count = len(partner_tickets) if partner_tickets else 0
|
||||
partner_tickets.fetch(['stage_id']) # prevent over-fetching fields, leading to potential out-of-memory error
|
||||
open_ticket = partner_tickets.filtered(lambda ticket: not ticket.stage_id.fold)
|
||||
ticket.partner_open_ticket_count = len(open_ticket)
|
||||
|
||||
@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 and ticket.team_id:
|
||||
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
|
||||
|
||||
def _compute_display_extra_info(self):
|
||||
self.display_extra_info = self.env.user.has_group('base.group_multi_company')
|
||||
|
||||
@api.model
|
||||
def _search_open_hours(self, operator, value):
|
||||
dt = fields.Datetime.now() - 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])
|
||||
|
||||
def _get_partner_email_update(self):
|
||||
self.ensure_one()
|
||||
if self.partner_id.email and self.partner_email and self.partner_email != self.partner_id.email:
|
||||
ticket_email_normalized = tools.email_normalize(self.partner_email) or self.partner_email or False
|
||||
partner_email_normalized = tools.email_normalize(self.partner_id.email) or self.partner_id.email or False
|
||||
return ticket_email_normalized != partner_email_normalized
|
||||
return False
|
||||
|
||||
def _get_partner_phone_update(self):
|
||||
self.ensure_one()
|
||||
if self.partner_id.phone and self.partner_phone and self.partner_phone != self.partner_id.phone:
|
||||
ticket_phone_formatted = self.partner_phone or False
|
||||
partner_phone_formatted = self.partner_id.phone or False
|
||||
return ticket_phone_formatted != partner_phone_formatted
|
||||
return False
|
||||
|
||||
def action_customer_preview(self):
|
||||
self.ensure_one()
|
||||
if self.team_privacy_visibility != 'portal' or not self.partner_id:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'danger',
|
||||
'message': _('At this time, there is no customer preview available to show. The current ticket cannot be accessed by the customer, as it belongs to a helpdesk team that is not publicly available, or there is no customer associated with the ticket.'),
|
||||
}
|
||||
}
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.get_portal_url(),
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM overrides
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.depends('ticket_ref', 'partner_name')
|
||||
@api.depends_context('with_partner')
|
||||
def _compute_display_name(self):
|
||||
display_partner_name = self._context.get('with_partner', False)
|
||||
ticket_with_name = self.filtered('name')
|
||||
for ticket in ticket_with_name:
|
||||
name = ticket.name
|
||||
if ticket.ticket_ref:
|
||||
name += f' (#{ticket.ticket_ref})'
|
||||
if display_partner_name and ticket.partner_name:
|
||||
name += f' - {ticket.partner_name}'
|
||||
ticket.display_name = name
|
||||
return super(HelpdeskTicket, self - ticket_with_name)._compute_display_name()
|
||||
|
||||
@api.model
|
||||
def get_empty_list_help(self, help_message):
|
||||
self = self.with_context(
|
||||
empty_list_help_id=self.env.context.get('default_team_id'),
|
||||
empty_list_help_model='helpdesk.team',
|
||||
empty_list_help_document_name=_("tickets"),
|
||||
)
|
||||
return super(HelpdeskTicket, self).get_empty_list_help(help_message)
|
||||
|
||||
def create_action(self, action_ref, title, search_view_ref):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(action_ref)
|
||||
action = clean_action(action, self.env)
|
||||
if title:
|
||||
action['display_name'] = title
|
||||
if search_view_ref:
|
||||
action['search_view_id'] = self.env.ref(search_view_ref).read()[0]
|
||||
if 'views' not in action:
|
||||
action['views'] = [(False, view) for view in action['view_mode'].split(",")]
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _find_or_create_partner(self, partner_name, partner_email, company=False):
|
||||
parsed_name, parsed_email_normalized = tools.parse_contact_from_email(partner_email)
|
||||
if not parsed_name:
|
||||
parsed_name = partner_name
|
||||
return self.env['res.partner'].with_context(default_company_id=company).find_or_create(
|
||||
tools.formataddr((parsed_name, parsed_email_normalized))
|
||||
)
|
||||
|
||||
@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['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_template_recipients' doesn't keep the name. This is
|
||||
# to avoid intrusive changes in the 'mail' module
|
||||
# TDE TODO: to extract and clean in mail thread
|
||||
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:
|
||||
company = False
|
||||
if vals.get('team_id'):
|
||||
team = self.env['helpdesk.team'].browse(vals.get('team_id'))
|
||||
company = team.company_id.id
|
||||
vals['partner_id'] = self._find_or_create_partner(partner_name, partner_email, company).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}
|
||||
company_per_team_id = {t.id: t.company_id for t in teams}
|
||||
for vals in list_value:
|
||||
company = company_per_team_id.get(vals.get('team_id', False))
|
||||
vals['ticket_ref'] = self.env['ir.sequence'].with_company(company).sudo().next_by_code('helpdesk.ticket')
|
||||
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
|
||||
vals['oldest_unanswered_customer_message_date'] = now
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
tickets = super(HelpdeskTicket, self).create(list_value)
|
||||
|
||||
all_partner_emails = []
|
||||
for ticket in tickets:
|
||||
all_partner_emails += tools.email_split(ticket.email_cc)
|
||||
partners = self.env['res.partner'].search([('email', 'in', all_partner_emails)])
|
||||
partner_per_email = {
|
||||
partner.email: partner
|
||||
for partner in partners
|
||||
if not all(u.share for u in partner.user_ids)
|
||||
}
|
||||
|
||||
# make customer follower
|
||||
for ticket in tickets:
|
||||
partner_ids = []
|
||||
if ticket.partner_id:
|
||||
partner_ids = ticket.partner_id.ids
|
||||
if ticket.email_cc:
|
||||
partners_with_internal_user = self.env['res.partner']
|
||||
for email in tools.email_split(ticket.email_cc):
|
||||
new_partner = partner_per_email.get(email)
|
||||
if new_partner:
|
||||
partners_with_internal_user |= new_partner
|
||||
if partners_with_internal_user:
|
||||
ticket._send_email_notify_to_cc(partners_with_internal_user)
|
||||
partner_ids += partners_with_internal_user.ids
|
||||
if partner_ids:
|
||||
ticket.message_subscribe(partner_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['helpdesk.stage'].browse(vals.get('stage_id')).fold:
|
||||
closed_tickets = self.filtered(lambda ticket: not ticket.close_date)
|
||||
else: # auto reset the 'closed_by_partner' flag
|
||||
vals['closed_by_partner'] = False
|
||||
vals['close_date'] = False
|
||||
|
||||
now = fields.Datetime.now()
|
||||
|
||||
# update last stage date when changing stage
|
||||
if 'stage_id' in vals:
|
||||
vals['date_last_stage_update'] = now
|
||||
if 'kanban_state' not in vals:
|
||||
vals['kanban_state'] = 'normal'
|
||||
|
||||
res = super(HelpdeskTicket, self - assigned_tickets - closed_tickets).write(vals)
|
||||
res &= super(HelpdeskTicket, assigned_tickets - closed_tickets).write(dict(vals, **{
|
||||
'assign_date': now,
|
||||
}))
|
||||
res &= super(HelpdeskTicket, closed_tickets - assigned_tickets).write(dict(vals, **{
|
||||
'close_date': now,
|
||||
'oldest_unanswered_customer_message_date': False,
|
||||
}))
|
||||
res &= super(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']])
|
||||
|
||||
# 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 and self.env['helpdesk.stage'].browse(vals['stage_id']).fold:
|
||||
odoobot_partner_id = self.env['ir.model.data']._xmlid_to_res_id('base.partner_root')
|
||||
for ticket in self:
|
||||
exceeded_hours = ticket.sla_status_ids.mapped('exceeded_hours')
|
||||
if exceeded_hours:
|
||||
min_hours = min([hours for hours in exceeded_hours if hours > 0], default=min(exceeded_hours))
|
||||
message = _("This ticket was successfully closed %s hours before its SLA deadline.", round(abs(min_hours))) if min_hours < 0 \
|
||||
else _("This ticket was closed %s hours after its SLA deadline.", round(min_hours))
|
||||
ticket.message_post(body=message, subtype_xmlid="mail.mt_note", author_id=odoobot_partner_id)
|
||||
return res
|
||||
|
||||
def copy_data(self, default=None):
|
||||
vals_list = super().copy_data(default=default)
|
||||
return [dict(vals, name=self.env._("%s (copy)", ticket.name)) for ticket, vals in zip(self, vals_list)]
|
||||
|
||||
def _unsubscribe_portal_users(self):
|
||||
self.message_unsubscribe(partner_ids=self.message_partner_ids.filtered('user_ids.share').ids)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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', 'tag_ids', 'partner_id']
|
||||
|
||||
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 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)
|
||||
|
||||
# unlink status and create the new ones in 2 operations
|
||||
sla_status_to_remove.unlink()
|
||||
return self.env['helpdesk.sla.status'].create(sla_status_value_list)
|
||||
|
||||
@api.model
|
||||
def _sla_find_false_domain(self):
|
||||
return [('partner_ids', '=', False)]
|
||||
|
||||
def _sla_find_extra_domain(self):
|
||||
self.ensure_one()
|
||||
return [
|
||||
'|',
|
||||
('partner_ids', 'parent_of', self.partner_id.ids),
|
||||
('partner_ids', 'child_of', self.partner_id.ids),
|
||||
]
|
||||
|
||||
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 {<helpdesk.ticket>: <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['helpdesk.ticket'])
|
||||
tickets_map[key] |= ticket
|
||||
# group the SLA to apply, by key
|
||||
if key not in sla_domain_map:
|
||||
sla_domain_map[key] = expression.AND([[
|
||||
('team_id', '=', ticket.team_id.id), ('priority', '=', ticket.priority),
|
||||
('stage_id.sequence', '>=', ticket.stage_id.sequence),
|
||||
], expression.OR([ticket._sla_find_extra_domain(), self._sla_find_false_domain()])])
|
||||
|
||||
result = {}
|
||||
for key, tickets in tickets_map.items(): # only one search per ticket group
|
||||
domain = sla_domain_map[key]
|
||||
slas = self.env['helpdesk.sla'].search(domain)
|
||||
result[tickets] = slas.filtered(lambda s: not s.tag_ids or (tickets.tag_ids & s.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]):
|
||||
result.append({
|
||||
'ticket_id': ticket.id,
|
||||
'sla_id': sla.id,
|
||||
'reached_datetime': fields.Datetime.now() if ticket.stage_id == sla.stage_id else False # in case of SLA on first stage
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
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['helpdesk.stage'].browse(stage_id)
|
||||
stages = self.env['helpdesk.stage'].search([('sequence', '<=', stage.sequence), ('team_ids', 'in', self.mapped('team_id').ids)]) # take previous stages
|
||||
sla_status = self.env['helpdesk.sla.status'].search([('ticket_id', 'in', self.ids)])
|
||||
sla_not_reached = sla_status.filtered(lambda sla: not sla.reached_datetime and sla.sla_stage_id in stages)
|
||||
sla_not_reached.write({'reached_datetime': fields.Datetime.now()})
|
||||
(sla_status - sla_not_reached).filtered(lambda x: x.sla_stage_id not in stages).write({'reached_datetime': False})
|
||||
|
||||
def action_open_helpdesk_ticket(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("helpdesk.helpdesk_ticket_action_main_tree")
|
||||
action.update({
|
||||
'domain': [('id', '!=', self.id), ('id', 'in', self.partner_ticket_ids.ids)],
|
||||
'context': {
|
||||
**ast.literal_eval(action.get('context', {})),
|
||||
'create': False,
|
||||
},
|
||||
})
|
||||
return action
|
||||
|
||||
def action_open_ratings(self):
|
||||
self.ensure_one()
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('helpdesk.rating_rating_action_helpdesk')
|
||||
if self.rating_count == 1:
|
||||
action.update({
|
||||
'view_mode': 'form',
|
||||
'res_id': self.rating_ids[0].id,
|
||||
'views': [(view_id, view_type) for view_id, view_type in action['views'] if view_type == 'form'],
|
||||
})
|
||||
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()._message_get_suggested_recipients()
|
||||
try:
|
||||
if self.partner_id and self.partner_id.email:
|
||||
self._message_add_suggested_recipient(recipients, partner=self.partner_id, reason=_('Customer'))
|
||||
elif self.partner_email:
|
||||
self._message_add_suggested_recipient(recipients, email=self.partner_email, reason=_('Customer Email'))
|
||||
except AccessError: # no read access rights -> just ignore suggested recipients because this implies modifying followers
|
||||
pass
|
||||
return recipients
|
||||
|
||||
def _get_customer_information(self):
|
||||
email_normalized_to_values = super()._get_customer_information()
|
||||
Partner = self.env['res.partner']
|
||||
|
||||
for record in self.filtered('partner_email'):
|
||||
email_normalized = tools.email_normalize(record.partner_email)
|
||||
if not email_normalized:
|
||||
continue
|
||||
values = email_normalized_to_values.setdefault(email_normalized, {})
|
||||
values.update({
|
||||
'name': record.partner_name or tools.parse_contact_from_email(record.partner_email)[0] or record.partner_email,
|
||||
'phone': record.partner_phone,
|
||||
})
|
||||
return email_normalized_to_values
|
||||
|
||||
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_name=msg.get('from'), partner_id=msg.get('author_id'))
|
||||
ticket = super(HelpdeskTicket, self.with_context(mail_notify_author=True)).message_new(msg, custom_values=values)
|
||||
thread_context = self.env['mail.thread']
|
||||
if ticket.company_id:
|
||||
thread_context = thread_context.with_context(default_company_id=ticket.company_id)
|
||||
partner_ids = [x.id for x in thread_context._mail_find_partner_from_emails(ticket._ticket_email_split(msg), records=ticket, force_create=True) if x]
|
||||
customer_ids = [p.id for p in thread_context._mail_find_partner_from_emails(tools.email_split(values['partner_email']), records=ticket, force_create=True) 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(HelpdeskTicket, self).message_update(msg, update_vals=update_vals)
|
||||
|
||||
def _message_compute_subject(self):
|
||||
""" Override the display name by the actual name field for communication."""
|
||||
self.ensure_one()
|
||||
return self.name
|
||||
|
||||
def _message_post_after_hook(self, message, msg_vals):
|
||||
if not self.partner_email:
|
||||
return super()._message_post_after_hook(message, msg_vals)
|
||||
|
||||
if self.partner_id and not self.partner_id.email:
|
||||
self.partner_id.email = self.partner_email
|
||||
|
||||
if 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.
|
||||
email_normalized = tools.email_normalize(self.partner_email)
|
||||
new_partner = message.partner_ids.filtered(
|
||||
lambda partner: partner.email == self.partner_email or (email_normalized and partner.email_normalized == email_normalized)
|
||||
)
|
||||
if new_partner:
|
||||
if new_partner[0].email_normalized:
|
||||
email_domain = ('partner_email', 'in', [new_partner[0].email, new_partner[0].email_normalized])
|
||||
else:
|
||||
email_domain = ('partner_email', '=', new_partner[0].email)
|
||||
self.search([
|
||||
('partner_id', '=', False), email_domain,
|
||||
]).write({'partner_id': new_partner[0].id})
|
||||
# use the sanitized body of the email from the message thread to populate the ticket's description
|
||||
if not self.description and message.subtype_id == self._creation_subtype() and tools.email_normalize(self.partner_email) == tools.email_normalize(message.email_from):
|
||||
self.description = message.body
|
||||
return super(HelpdeskTicket, self)._message_post_after_hook(message, msg_vals)
|
||||
|
||||
def _send_email_notify_to_cc(self, partners_to_notify):
|
||||
self.ensure_one()
|
||||
template_id = self.env['ir.model.data']._xmlid_to_res_id('helpdesk.ticket_invitation_follower', raise_if_not_found=False)
|
||||
if not template_id:
|
||||
return
|
||||
ticket_model_description = self.env['ir.model']._get(self._name).display_name
|
||||
values = {
|
||||
'object': self,
|
||||
}
|
||||
for partner in partners_to_notify:
|
||||
values['partner_name'] = partner.name
|
||||
assignation_msg = self.env['ir.qweb']._render('helpdesk.ticket_invitation_follower', values, minimal_qcontext=True)
|
||||
self.message_notify(
|
||||
subject=_('You have been invited to follow %s', self.display_name),
|
||||
body=assignation_msg,
|
||||
partner_ids=partner.ids,
|
||||
record_name=self.display_name,
|
||||
email_layout_xmlid='mail.mail_notification_layout',
|
||||
model_description=ticket_model_description,
|
||||
mail_auto_delete=True,
|
||||
)
|
||||
|
||||
def _track_template(self, changes):
|
||||
res = super(HelpdeskTicket, self)._track_template(changes)
|
||||
ticket = self[0]
|
||||
if 'stage_id' in changes and ticket.stage_id.template_id and ticket.partner_email and (
|
||||
not self.env.user.partner_id or not ticket.partner_id or ticket.partner_id != self.env.user.partner_id
|
||||
or self.env.user._is_portal() or ticket._context.get('mail_notify_author')
|
||||
):
|
||||
res['stage_id'] = (ticket.stage_id.template_id, {
|
||||
'auto_delete_keep_log': False,
|
||||
'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('helpdesk.mt_ticket_new')
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
if 'stage_id' in init_values:
|
||||
return self.env.ref('helpdesk.mt_ticket_stage')
|
||||
return super(HelpdeskTicket, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_recipients_groups(self, message, model_description, msg_vals=None):
|
||||
"""
|
||||
Give access button to portal and portal customers.
|
||||
If they are notified they should probably have access to the document.
|
||||
"""
|
||||
return super()._notify_get_recipients_groups(
|
||||
message, model_description, msg_vals=msg_vals
|
||||
)
|
||||
|
||||
def _notify_get_reply_to(self, default=None):
|
||||
""" Override to set alias of tickets to their team if any. """
|
||||
aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default)
|
||||
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(HelpdeskTicket, leftover)._notify_get_reply_to(default=default))
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Rating Mixin
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _rating_apply_get_default_subtype_id(self):
|
||||
return self.env['ir.model.data']._xmlid_to_res_id("helpdesk.mt_ticket_rated")
|
||||
|
||||
def _rating_get_parent_field_name(self):
|
||||
return 'team_id'
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Mail gateway
|
||||
# ---------------------------------------------------
|
||||
|
||||
def _mail_get_message_subtypes(self):
|
||||
res = super()._mail_get_message_subtypes()
|
||||
if len(self) == 1 and self.team_id:
|
||||
team = self.team_id
|
||||
optional_subtypes = [('use_credit_notes', self.env.ref('helpdesk.mt_ticket_refund_status')),
|
||||
('use_product_returns', self.env.ref('helpdesk.mt_ticket_return_status')),
|
||||
('use_product_repairs', self.env.ref('helpdesk.mt_ticket_repair_status'))]
|
||||
for field, subtype in optional_subtypes:
|
||||
if not team[field] and subtype in res:
|
||||
res -= subtype
|
||||
return res
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrModuleModule(models.Model):
|
||||
_inherit = 'ir.module.module'
|
||||
|
||||
def module_uninstall(self):
|
||||
helpdesk_modules = self.env['helpdesk.team']._get_field_modules()
|
||||
modules_field = {module: field for field, module in helpdesk_modules.items()}
|
||||
fields_to_reset = [modules_field[name] for name in self.mapped('name') if name in modules_field.keys()]
|
||||
if fields_to_reset:
|
||||
self.env['helpdesk.team'].search([]).write({
|
||||
field: False
|
||||
for field in fields_to_reset
|
||||
})
|
||||
|
||||
return super().module_uninstall()
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrUiMenu(models.Model):
|
||||
_inherit = 'ir.ui.menu'
|
||||
|
||||
def _load_menus_blacklist(self):
|
||||
res = super()._load_menus_blacklist()
|
||||
if not self.env.user.has_group('helpdesk.group_helpdesk_manager'):
|
||||
res.append(self.env.ref('helpdesk.helpdesk_ticket_report_menu_ratings').id)
|
||||
return res
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
messages = super().create(values_list)
|
||||
# We measure the time between the customer's message
|
||||
# or ticket's create date, and the helpdesk response of subtype comment (not note).
|
||||
# If several messages are sent before any response,
|
||||
# in both directions, we take the first one.
|
||||
|
||||
# EVENT | CUSTOMER | HELPDESK | MEASURED
|
||||
# create | x | | ↑
|
||||
# msg | x | | |
|
||||
# note | | x | |
|
||||
# msg | | x | ↓
|
||||
# msg | | x |
|
||||
# note | x | |
|
||||
# msg | x | | ↑
|
||||
# msg | | x | ↓
|
||||
# ...
|
||||
if not any(values.get('model') == 'helpdesk.ticket' for values in values_list):
|
||||
return messages
|
||||
|
||||
comment_subtype = self.env.ref('mail.mt_comment')
|
||||
filtered_messages = messages.filtered(
|
||||
lambda m: m.model == 'helpdesk.ticket' and m.subtype_id == comment_subtype
|
||||
)
|
||||
if not filtered_messages:
|
||||
return messages
|
||||
|
||||
tickets = self.env['helpdesk.ticket'].sudo().search(
|
||||
[('close_date', '=', False), ('id', 'in', filtered_messages.mapped('res_id'))]
|
||||
)
|
||||
ticket_per_id = {t.id: t for t in tickets}
|
||||
if not tickets:
|
||||
return messages
|
||||
|
||||
for message in filtered_messages.sorted(lambda m: m.date):
|
||||
ticket = ticket_per_id.get(message.res_id)
|
||||
if not ticket:
|
||||
continue
|
||||
oldest_unanswered_customer_message_date = ticket.oldest_unanswered_customer_message_date
|
||||
is_helpdesk_msg = any(not user.share for user in message.author_id.user_ids)
|
||||
|
||||
if not oldest_unanswered_customer_message_date and not is_helpdesk_msg:
|
||||
# customer initiated an exchange
|
||||
ticket.oldest_unanswered_customer_message_date = message.date
|
||||
|
||||
elif oldest_unanswered_customer_message_date and is_helpdesk_msg:
|
||||
# internal user responded to the customer
|
||||
ticket.oldest_unanswered_customer_message_date = False
|
||||
calendar = ticket.team_id.resource_calendar_id or self.env.company.resource_calendar_id
|
||||
if not calendar:
|
||||
continue
|
||||
|
||||
duration_data = calendar.get_work_duration_data(oldest_unanswered_customer_message_date, message.date, compute_leaves=True)
|
||||
delta_hours = duration_data['hours']
|
||||
if not ticket.answered_customer_message_count:
|
||||
ticket.first_response_hours = delta_hours
|
||||
ticket.answered_customer_message_count += 1
|
||||
ticket.total_response_hours += delta_hours
|
||||
ticket.avg_response_hours = ticket.total_response_hours / ticket.answered_customer_message_count
|
||||
else:
|
||||
# a new message is received, `write_date` should be updated
|
||||
ticket.write({})
|
||||
|
||||
return messages
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command, api, models, _
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
company = super().create(values)
|
||||
company._create_helpdesk_team()
|
||||
return company
|
||||
|
||||
def _create_helpdesk_team(self):
|
||||
results = []
|
||||
stage_ids = []
|
||||
for xml_id in ['stage_new', 'stage_in_progress', 'stage_solved', 'stage_cancelled', 'stage_on_hold']:
|
||||
record = self.env.ref(f'helpdesk.{xml_id}', False)
|
||||
if record:
|
||||
stage_ids.append(record.id)
|
||||
team_name = _('Customer Care')
|
||||
for company in self:
|
||||
company = company.with_company(company)
|
||||
results += [{
|
||||
'name': team_name,
|
||||
'company_id': company.id,
|
||||
'use_sla': False,
|
||||
'stage_ids': [Command.set(stage_ids)],
|
||||
'alias_name': "%s-%s" % (team_name, company.name),
|
||||
}]
|
||||
# use sudo as the user could have the right to create a company
|
||||
# but not to create a team for other company.
|
||||
return self.env['helpdesk.team'].sudo().create(results)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
ticket_count = fields.Integer("Tickets", compute='_compute_ticket_count')
|
||||
sla_ids = fields.Many2many(
|
||||
'helpdesk.sla', 'helpdesk_sla_res_partner_rel',
|
||||
'res_partner_id', 'helpdesk_sla_id', string='SLA Policies',
|
||||
help="SLA Policies that will automatically apply to the tickets submitted by this customer.")
|
||||
|
||||
def _compute_ticket_count(self):
|
||||
all_partners_subquery = self.with_context(active_test=False)._search([('id', 'child_of', self.ids)])
|
||||
|
||||
# group tickets by partner, and account for each partner in self
|
||||
groups = self.env['helpdesk.ticket']._read_group(
|
||||
[('partner_id', 'in', all_partners_subquery)],
|
||||
groupby=['partner_id'], aggregates=['__count'],
|
||||
)
|
||||
self.ticket_count = 0
|
||||
for partner, count in groups:
|
||||
while partner:
|
||||
if partner in self:
|
||||
partner.ticket_count += count
|
||||
partner = partner.with_context(prefetch_fields=False).parent_id
|
||||
|
||||
def action_open_helpdesk_ticket(self):
|
||||
self.ensure_one()
|
||||
action = {
|
||||
**self.env["ir.actions.actions"]._for_xml_id("helpdesk.helpdesk_ticket_action_main_tree"),
|
||||
'display_name': _("%(partner_name)s's Tickets", partner_name=self.name),
|
||||
'context': {},
|
||||
}
|
||||
all_child = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
|
||||
search_domain = [('partner_id', 'in', (self | all_child).ids)]
|
||||
if self.ticket_count <= 1:
|
||||
ticket_id = self.env['helpdesk.ticket'].search(search_domain, limit=1)
|
||||
action['res_id'] = ticket_id.id
|
||||
action['views'] = [(view_id, view_type) for view_id, view_type in action['views'] if view_type == "form"]
|
||||
else:
|
||||
action['domain'] = search_domain
|
||||
action['views'] = [
|
||||
(self.env['ir.model.data']._xmlid_to_res_id('helpdesk.helpdesk_tickets_view_tree_res_partner'), view_type) if view_type == 'list' else
|
||||
(view_id, view_type)
|
||||
for view_id, view_type in action['views']
|
||||
]
|
||||
return action
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
helpdesk_target_closed = fields.Integer(export_string_translation=False, default=1)
|
||||
helpdesk_target_rating = fields.Float(export_string_translation=False, default=4.5)
|
||||
helpdesk_target_success = fields.Float(export_string_translation=False, default=85)
|
||||
|
||||
_sql_constraints = [
|
||||
('target_closed_not_zero', 'CHECK(helpdesk_target_closed > 0)', 'You cannot have negative targets'),
|
||||
('target_rating_not_zero', 'CHECK(helpdesk_target_rating > 0)', 'You cannot have negative targets'),
|
||||
('target_success_not_zero', 'CHECK(helpdesk_target_success > 0)', 'You cannot have negative targets'),
|
||||
]
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + [
|
||||
'helpdesk_target_closed',
|
||||
'helpdesk_target_rating',
|
||||
'helpdesk_target_success',
|
||||
]
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + [
|
||||
'helpdesk_target_closed',
|
||||
'helpdesk_target_rating',
|
||||
'helpdesk_target_success',
|
||||
]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import helpdesk_sla_report_analysis
|
||||
from . import helpdesk_ticket_analysis
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.addons.helpdesk.models.helpdesk_ticket import TICKET_PRIORITY
|
||||
from odoo.addons.rating.models.rating_data import RATING_LIMIT_MIN
|
||||
|
||||
|
||||
class HelpdeskSLAReport(models.Model):
|
||||
_name = 'helpdesk.sla.report.analysis'
|
||||
_description = "SLA Status Analysis"
|
||||
_auto = False
|
||||
_order = 'create_date DESC'
|
||||
|
||||
ticket_id = fields.Many2one('helpdesk.ticket', string='Ticket', readonly=True)
|
||||
description = fields.Text(readonly=True)
|
||||
tag_ids = fields.Many2many('helpdesk.tag', relation='helpdesk_tag_helpdesk_ticket_rel',
|
||||
column1='helpdesk_ticket_id', column2='helpdesk_tag_id',
|
||||
string='Tags', readonly=True)
|
||||
ticket_ref = fields.Char(string='Ticket IDs Sequence', readonly=True)
|
||||
name = fields.Char(string='Subject', readonly=True)
|
||||
create_date = fields.Datetime("Ticket Creation 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)
|
||||
partner_name = fields.Char(string='Customer Name', readonly=True)
|
||||
partner_email = fields.Char(string='Customer Email', readonly=True)
|
||||
partner_phone = fields.Char(string='Customer Phone', readonly=True)
|
||||
stage_id = fields.Many2one('helpdesk.stage', string="Stage", readonly=True)
|
||||
ticket_open_hours = fields.Float("Hours Open", aggregator="avg", readonly=True)
|
||||
ticket_closed = fields.Boolean("Ticket Closed", readonly=True)
|
||||
ticket_close_hours = fields.Integer("Working Hours to Close", aggregator="avg", readonly=True)
|
||||
ticket_assignation_hours = fields.Integer("Working Hours to Assign", aggregator="avg", readonly=True)
|
||||
close_date = fields.Datetime("Closing Date", readonly=True)
|
||||
sla_id = fields.Many2one('helpdesk.sla', string='SLA', readonly=True)
|
||||
sla_ids = fields.Many2many('helpdesk.sla', 'helpdesk_sla_status', 'ticket_id', 'sla_id', string="SLAs", copy=False)
|
||||
sla_status_ids = fields.One2many('helpdesk.sla.status', 'ticket_id', string="SLA Status")
|
||||
sla_stage_id = fields.Many2one('helpdesk.stage', string="SLA Stage", readonly=True)
|
||||
sla_deadline = fields.Datetime("SLA Deadline", aggregator='min', readonly=True)
|
||||
sla_status = fields.Selection([('failed', 'SLA Failed'), ('reached', 'SLA Success'), ('ongoing', 'SLA in Progress')], string="Status", readonly=True)
|
||||
sla_fail = fields.Boolean("SLA Status Failed", aggregator='bool_or', readonly=True)
|
||||
sla_success = fields.Boolean("SLA Status Success", aggregator='bool_or', readonly=True)
|
||||
sla_exceeded_hours = fields.Integer("Working Hours until SLA Deadline", aggregator='avg', readonly=True, help="Day to reach the stage of the SLA, without taking the working calendar into account")
|
||||
sla_status_failed = fields.Integer("Number of SLAs Failed", readonly=True, aggregator="avg")
|
||||
active = fields.Boolean("Active", readonly=True)
|
||||
rating_last_value = fields.Float("Rating (1-5)", aggregator="avg", readonly=True)
|
||||
rating_avg = fields.Float('Average Rating', readonly=True, aggregator='avg')
|
||||
team_id = fields.Many2one('helpdesk.team', string='Helpdesk Team', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
message_is_follower = fields.Boolean(related='ticket_id.message_is_follower', export_string_translation=False)
|
||||
kanban_state = fields.Selection([
|
||||
('normal', 'Grey'),
|
||||
('done', 'Green'),
|
||||
('blocked', 'Red')], string='Kanban State', readonly=True)
|
||||
avg_response_hours = fields.Float("Average Hours to Respond", aggregator="avg", readonly=True)
|
||||
first_response_hours = fields.Float("Hours to First Response", aggregator="avg", readonly=True)
|
||||
|
||||
def _select(self):
|
||||
return """
|
||||
SELECT DISTINCT T.id as id,
|
||||
T.id AS ticket_id,
|
||||
T.description,
|
||||
T.ticket_ref AS ticket_ref,
|
||||
T.name AS name,
|
||||
T.create_date AS create_date,
|
||||
T.team_id,
|
||||
T.active AS active,
|
||||
T.stage_id AS stage_id,
|
||||
T.user_id,
|
||||
T.partner_id,
|
||||
T.partner_name AS partner_name,
|
||||
T.partner_email AS partner_email,
|
||||
T.partner_phone AS partner_phone,
|
||||
T.company_id,
|
||||
T.kanban_state AS kanban_state,
|
||||
NULLIF(T.rating_last_value, 0) AS rating_last_value,
|
||||
AVG(rt.rating) as rating_avg,
|
||||
T.priority AS priority,
|
||||
NULLIF(T.close_hours, 0) AS ticket_close_hours,
|
||||
CASE
|
||||
WHEN EXTRACT(EPOCH FROM (COALESCE(T.assign_date, NOW() AT TIME ZONE 'UTC') - T.create_date)) / 3600 < 1 THEN NULL
|
||||
ELSE EXTRACT(EPOCH FROM (COALESCE(T.assign_date, NOW() AT TIME ZONE 'UTC') - T.create_date)) / 3600
|
||||
END AS ticket_open_hours,
|
||||
NULLIF(T.assign_hours, 0) AS ticket_assignation_hours,
|
||||
NULLIF(T.avg_response_hours, 0) AS avg_response_hours,
|
||||
NULLIF(T.first_response_hours, 0) AS first_response_hours,
|
||||
T.close_date AS close_date,
|
||||
STAGE.fold AS ticket_closed,
|
||||
SLA.stage_id as sla_stage_id,
|
||||
SLA_S.deadline AS sla_deadline,
|
||||
SLA.id as sla_id,
|
||||
SLA_S.exceeded_hours AS sla_exceeded_hours,
|
||||
SLA_S.reached_datetime >= SLA_S.deadline OR (SLA_S.reached_datetime IS NULL AND SLA_S.deadline < NOW() AT TIME ZONE 'UTC') AS sla_fail,
|
||||
CASE
|
||||
WHEN SLA_S.reached_datetime IS NOT NULL AND SLA_S.deadline IS NOT NULL AND SLA_S.reached_datetime >= SLA_S.deadline THEN 1
|
||||
WHEN SLA_S.reached_datetime IS NULL AND SLA_S.deadline IS NOT NULL AND SLA_S.deadline < NOW() AT TIME ZONE 'UTC' THEN 1
|
||||
ELSE NULL
|
||||
END AS sla_status_failed,
|
||||
CASE
|
||||
WHEN SLA_S.reached_datetime IS NOT NULL AND (SLA_S.deadline IS NULL OR SLA_S.reached_datetime < SLA_S.deadline) THEN 'reached'
|
||||
WHEN (SLA_S.reached_datetime IS NOT NULL AND SLA_S.deadline IS NOT NULL AND SLA_S.reached_datetime >= SLA_S.deadline) OR
|
||||
(SLA_S.reached_datetime IS NULL AND SLA_S.deadline IS NOT NULL AND SLA_S.deadline < NOW() AT TIME ZONE 'UTC') THEN 'failed'
|
||||
WHEN SLA_S.reached_datetime IS NULL AND (SLA_S.deadline IS NULL OR SLA_S.deadline > NOW() AT TIME ZONE 'UTC') THEN 'ongoing'
|
||||
END AS sla_status,
|
||||
CASE
|
||||
WHEN (SLA_S.deadline IS NOT NULL AND SLA_S.deadline > NOW() AT TIME ZONE 'UTC') THEN TRUE ELSE FALSE
|
||||
END AS sla_success
|
||||
"""
|
||||
|
||||
def _group_by(self):
|
||||
return """
|
||||
t.id,
|
||||
STAGE.fold,
|
||||
SLA.stage_id,
|
||||
SLA_S.deadline,
|
||||
SLA_S.reached_datetime,
|
||||
SLA.id,
|
||||
SLA_S.exceeded_hours
|
||||
"""
|
||||
|
||||
def _from(self):
|
||||
return f"""
|
||||
helpdesk_ticket T
|
||||
LEFT JOIN rating_rating rt ON rt.res_id = t.id
|
||||
AND rt.res_model = 'helpdesk.ticket'
|
||||
AND rt.consumed = True
|
||||
AND rt.rating >= {RATING_LIMIT_MIN}
|
||||
LEFT JOIN helpdesk_stage STAGE ON T.stage_id = STAGE.id
|
||||
RIGHT JOIN helpdesk_sla_status SLA_S ON T.id = SLA_S.ticket_id
|
||||
LEFT JOIN helpdesk_sla SLA ON SLA.id = SLA_S.sla_id
|
||||
"""
|
||||
|
||||
def _where(self):
|
||||
return """
|
||||
T.active = true
|
||||
"""
|
||||
|
||||
def _order_by(self):
|
||||
return """
|
||||
id, sla_stage_id
|
||||
"""
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
%s
|
||||
FROM %s
|
||||
WHERE %s
|
||||
GROUP BY %s
|
||||
ORDER BY %s
|
||||
)""" % (self._table, self._select(), self._from(),
|
||||
self._where(), self._group_by(), self._order_by()))
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_view_pivot" model="ir.ui.view">
|
||||
<field name="name">helpdesk.sla.report.analysis.pivot</field>
|
||||
<field name="model">helpdesk.sla.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="SLA Status Analysis" disable_linking="1" sample="1">
|
||||
<field name="team_id" type="row"/>
|
||||
<field name="sla_id" type="row"/>
|
||||
<field name="sla_status_failed"/>
|
||||
<field name="sla_status" type="col"/>
|
||||
<field name="ticket_close_hours" widget="float_time"/>
|
||||
<field name="ticket_assignation_hours" widget="float_time"/>
|
||||
<field name="sla_exceeded_hours" widget="float_time"/>
|
||||
<field name="rating_avg" type="measure" invisible="1"/>
|
||||
<field name="avg_response_hours" widget="float_time"/>
|
||||
<field name="ticket_open_hours" widget="float_time"/>
|
||||
<field name="first_response_hours" widget="float_time"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_view_pivot_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.sla.report.analysis.pivot.dashboard</field>
|
||||
<field name="model">helpdesk.sla.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_sla_report_analysis_view_pivot"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_view_graph" model="ir.ui.view">
|
||||
<field name="name">helpdesk.sla.report.analysis.graph</field>
|
||||
<field name="model">helpdesk.sla.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="SLA Status Analysis" disable_linking="1" sample="1">
|
||||
<field name="ticket_assignation_hours" widget="float_time"/>
|
||||
<field name="ticket_close_hours" widget="float_time"/>
|
||||
<field name="sla_id"/>
|
||||
<field name="sla_exceeded_hours" widget="float_time"/>
|
||||
<field name="sla_status"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_graph_analysis_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.sla.report.graph.analysis.dashboard</field>
|
||||
<field name="model">helpdesk.sla.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_sla_report_analysis_view_graph"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//graph" position="attributes">
|
||||
<attribute name="stacked">False</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="helpdesk_sla_report_analysis_view_cohort" model="ir.ui.view">-->
|
||||
<!-- <field name="name">helpdesk.sla.report.analysis.cohort</field>-->
|
||||
<!-- <field name="model">helpdesk.sla.report.analysis</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <cohort string="SLA Status Analysis" date_start="create_date" date_stop="close_date" disable_linking="True" interval="day" sample="1"/>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_view_search" model="ir.ui.view">
|
||||
<field name="name">helpdesk.sla.report.analysis.search</field>
|
||||
<field name="model">helpdesk.sla.report.analysis</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="helpdesk_tickets_view_search_base"/>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<search position="attributes"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="helpdesk_sla_report_analysis_action" model="ir.actions.act_window">
|
||||
<field name="name">SLA Status Analysis</field>
|
||||
<field name="res_model">helpdesk.sla.report.analysis</field>
|
||||
<field name="path">sla-status-analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="search_view_id" ref="helpdesk_sla_report_analysis_view_search"/>
|
||||
<field name="domain">['|', ('company_id', '=', False), ('company_id', 'in', allowed_company_ids)]</field>
|
||||
<field name="context">{'search_default_month': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet!
|
||||
</p><p>
|
||||
Track the performance of your teams, the success rate of your tickets, and how quickly you reach your service level agreements (SLAs).
|
||||
</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="helpdesk_sla_report_analysis_view_pivot"/>
|
||||
<field name="act_window_id" ref="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="helpdesk_sla_report_analysis_view_graph"/>
|
||||
<field name="act_window_id" ref="helpdesk_sla_report_analysis_action"/>
|
||||
</record>
|
||||
|
||||
<!-- <record id="action_appraisal_view_report_cohort" model="ir.actions.act_window.view">-->
|
||||
<!-- <field name="sequence" eval="10"/>-->
|
||||
<!-- <field name="view_mode">cohort</field>-->
|
||||
<!-- <field name="view_id" ref="helpdesk_sla_report_analysis_view_cohort"/>-->
|
||||
<!-- <field name="act_window_id" ref="helpdesk_sla_report_analysis_action"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_dashboard_action" model="ir.actions.act_window">
|
||||
<field name="name">SLA Status Analysis</field>
|
||||
<field name="res_model">helpdesk.sla.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="context">{'search_default_month': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet!
|
||||
</p><p>
|
||||
Track the performance of your teams, the success rate of your tickets, and how quickly you reach your service level agreements (SLAs).
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_dashboard_pivot_view" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="helpdesk_sla_report_analysis_view_pivot_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_sla_report_analysis_dashboard_action"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_dashboard_graph_view" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="helpdesk_sla_report_graph_analysis_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_sla_report_analysis_dashboard_action"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, tools
|
||||
from odoo.addons.helpdesk.models.helpdesk_ticket import TICKET_PRIORITY
|
||||
from odoo.addons.rating.models.rating_data import RATING_LIMIT_MIN
|
||||
|
||||
|
||||
class HelpdeskTicketReport(models.Model):
|
||||
_name = 'helpdesk.ticket.report.analysis'
|
||||
_description = "Ticket Analysis"
|
||||
_auto = False
|
||||
_order = 'create_date DESC'
|
||||
|
||||
ticket_id = fields.Many2one('helpdesk.ticket', string='Ticket', readonly=True)
|
||||
description = fields.Text(readonly=True)
|
||||
tag_ids = fields.Many2many('helpdesk.tag', relation='helpdesk_tag_helpdesk_ticket_rel',
|
||||
column1='helpdesk_ticket_id', column2='helpdesk_tag_id',
|
||||
string='Tags', readonly=True)
|
||||
ticket_ref = fields.Char(string='Ticket IDs Sequence', readonly=True)
|
||||
name = fields.Char(string='Subject', readonly=True)
|
||||
sla_fail = fields.Boolean(related="ticket_id.sla_fail", readonly=True)
|
||||
sla_success = fields.Boolean("SLA Status Success", aggregator='bool_or', readonly=True)
|
||||
sla_ids = fields.Many2many('helpdesk.sla', 'helpdesk_sla_status', 'ticket_id', 'sla_id', string="SLAs", copy=False)
|
||||
sla_status_ids = fields.One2many('helpdesk.sla.status', 'ticket_id', string="SLA Status")
|
||||
create_date = fields.Datetime("Ticket Creation 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)
|
||||
partner_name = fields.Char(string='Customer Name', readonly=True)
|
||||
partner_email = fields.Char(string='Customer Email', readonly=True)
|
||||
partner_phone = fields.Char(string='Customer Phone', readonly=True)
|
||||
stage_id = fields.Many2one('helpdesk.stage', string="Stage", readonly=True)
|
||||
sla_deadline = fields.Datetime("Ticket Deadline", readonly=True)
|
||||
ticket_deadline_hours = fields.Float("Working Hours until SLA Deadline", aggregator="avg", readonly=True)
|
||||
ticket_close_hours = fields.Float("Working Hours to Close", aggregator="avg", readonly=True)
|
||||
ticket_open_hours = fields.Float("Hours Open", aggregator="avg", readonly=True)
|
||||
ticket_assignation_hours = fields.Float("Working Hours to Assign", aggregator="avg", readonly=True)
|
||||
close_date = fields.Datetime("Closing Date", readonly=True)
|
||||
assign_date = fields.Datetime("First assignment date", readonly=True)
|
||||
rating_last_value = fields.Float("Rating (1-5)", aggregator="avg", readonly=True)
|
||||
active = fields.Boolean("Active", readonly=True)
|
||||
team_id = fields.Many2one('helpdesk.team', string='Helpdesk Team', readonly=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', readonly=True)
|
||||
message_is_follower = fields.Boolean(related='ticket_id.message_is_follower', export_string_translation=False)
|
||||
kanban_state = fields.Selection([
|
||||
('normal', 'Grey'),
|
||||
('done', 'Green'),
|
||||
('blocked', 'Red')], string='Kanban State', readonly=True)
|
||||
first_response_hours = fields.Float("Hours to First Response", aggregator="avg", readonly=True)
|
||||
avg_response_hours = fields.Float("Average Hours to Respond", aggregator="avg", readonly=True)
|
||||
rating_avg = fields.Float('Average Rating', readonly=True, aggregator='avg')
|
||||
|
||||
def _select(self):
|
||||
select_str = """
|
||||
SELECT T.id AS id,
|
||||
T.id AS ticket_id,
|
||||
T.description,
|
||||
T.ticket_ref AS ticket_ref,
|
||||
T.name AS name,
|
||||
T.create_date AS create_date,
|
||||
T.priority AS priority,
|
||||
T.user_id AS user_id,
|
||||
T.partner_id AS partner_id,
|
||||
T.partner_name AS partner_name,
|
||||
T.partner_email AS partner_email,
|
||||
T.partner_phone AS partner_phone,
|
||||
T.stage_id AS stage_id,
|
||||
T.sla_deadline AS sla_deadline,
|
||||
NULLIF(T.sla_deadline_hours, 0) AS ticket_deadline_hours,
|
||||
NULLIF(T.close_hours, 0) AS ticket_close_hours,
|
||||
CASE
|
||||
WHEN EXTRACT(EPOCH FROM (COALESCE(T.close_date, NOW() AT TIME ZONE 'UTC') - T.create_date)) / 3600 < 1 THEN NULL
|
||||
ELSE EXTRACT(EPOCH FROM (COALESCE(T.close_date, NOW() AT TIME ZONE 'UTC') - T.create_date)) / 3600
|
||||
END AS ticket_open_hours,
|
||||
NULLIF(T.assign_hours, 0) AS ticket_assignation_hours,
|
||||
T.close_date AS close_date,
|
||||
T.assign_date AS assign_date,
|
||||
CASE WHEN ht.use_rating AND COUNT(rt.id) > 0 THEN T.rating_last_value ELSE NULL END as rating_last_value,
|
||||
CASE WHEN ht.use_rating AND COUNT(rt.id) > 0 THEN AVG(rt.rating) ELSE NULL END as rating_avg,
|
||||
T.active AS active,
|
||||
T.team_id AS team_id,
|
||||
T.company_id AS company_id,
|
||||
T.kanban_state AS kanban_state,
|
||||
NULLIF(T.first_response_hours, 0) AS first_response_hours,
|
||||
NULLIF(T.avg_response_hours, 0) AS avg_response_hours,
|
||||
CASE
|
||||
WHEN (T.sla_deadline IS NOT NULL AND T.sla_deadline > NOW() AT TIME ZONE 'UTC') THEN TRUE ELSE FALSE
|
||||
END AS sla_success
|
||||
"""
|
||||
return select_str
|
||||
|
||||
def _group_by(self):
|
||||
return """
|
||||
t.id,
|
||||
ht.use_rating
|
||||
"""
|
||||
|
||||
def _from(self):
|
||||
from_str = f"""
|
||||
helpdesk_ticket T
|
||||
LEFT JOIN rating_rating rt ON rt.res_id = t.id
|
||||
AND rt.res_model = 'helpdesk.ticket'
|
||||
AND rt.consumed = True
|
||||
AND rt.rating >= {RATING_LIMIT_MIN}
|
||||
INNER JOIN helpdesk_team ht ON ht.id = t.team_id
|
||||
"""
|
||||
return from_str
|
||||
|
||||
def init(self):
|
||||
tools.drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
%s
|
||||
FROM %s
|
||||
GROUP BY %s
|
||||
)""" % (self._table, self._select(), self._from(), self._group_by()))
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_ticket_view_pivot_analysis" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.pivot</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Ticket Analysis" display_quantity="1" disable_linking="1" sample="1">
|
||||
<field name="team_id" type="row"/>
|
||||
<field name="ticket_open_hours" widget="float_time"/>
|
||||
<field name="ticket_assignation_hours" widget="float_time" type="measure"/>
|
||||
<field name="first_response_hours" widget="float_time" type="measure"/>
|
||||
<field name="avg_response_hours" widget="float_time" type="measure"/>
|
||||
<field name="ticket_close_hours" widget="float_time" type="measure"/>
|
||||
<field name="ticket_deadline_hours" widget="float_time"/>
|
||||
<field name="rating_avg" type="measure" invisible="1"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_pivot_analysis_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.pivot.dashboard</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_pivot_analysis"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_id']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_pivot_7days_analysis_inherit_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.pivot.7days.analysis.inherit</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_pivot_analysis"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_id']" position="replace">
|
||||
<field name="close_date" interval="day" type="row"/>
|
||||
</xpath>
|
||||
<field name="ticket_close_hours" position="replace"/>
|
||||
<field name="ticket_open_hours" position="replace"/>
|
||||
<field name="ticket_assignation_hours" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_pivot_analysis_success_inherit_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.pivot.analysis.success.inherit</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk_ticket_view_pivot_7days_analysis_inherit_dashboard"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='close_date']" position="replace">
|
||||
<field name="ticket_id" type="row"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_pivot_analysis_7dayssuccess_inherit_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.pivot.analysis.7dayssuccess.inherit</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk_ticket_view_pivot_7days_analysis_inherit_dashboard"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='close_date']" position="attributes">
|
||||
<attribute name="name">create_date</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_pivot_analysis_inherit_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.pivot.analysis.inherit</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk_ticket_view_pivot_analysis_7dayssuccess_inherit_dashboard"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//pivot" position="attributes">
|
||||
<attribute name="display_quantity">1</attribute>
|
||||
</xpath>
|
||||
<field name="create_date" position="after">
|
||||
<field name="ticket_open_hours" widget="float_time" type="measure"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_list_analysis" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.list</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Tickets" multi_edit="1" sample="1">
|
||||
<field name="ticket_id" string="Name" readonly="1"/>
|
||||
<field name="team_id" optional="show" readonly="1" column_invisible="context.get('default_team_id', False)"/>
|
||||
<field name="team_id" optional="hide" readonly="1" column_invisible="not context.get('default_team_id', False)"/>
|
||||
<field name="user_id" optional="show" widget="many2one_avatar_user"/>
|
||||
<field name="partner_id" optional="show"/>
|
||||
<field name="priority" optional="show" widget="priority"/>
|
||||
<field name="stage_id" optional="show" readonly="1"/>
|
||||
<field name="sla_deadline" optional="show" widget="remaining_days"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show" readonly="1" column_invisible="context.get('default_team_id', False)"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide" readonly="1" column_invisible="not context.get('default_team_id', False)"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_graph_analysis" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.graph</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Tickets Analysis" sample="1" disable_linking="1">
|
||||
<field name="team_id"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="ticket_close_hours" widget="float_time"/>
|
||||
<field name="avg_response_hours" widget="float_time"/>
|
||||
<field name="ticket_assignation_hours" widget="float_time"/>
|
||||
<field name="first_response_hours" widget="float_time"/>
|
||||
<field name="ticket_deadline_hours" widget="float_time"/>
|
||||
<field name="ticket_open_hours" widget="float_time"/>
|
||||
<field name="rating_avg" type="measure" invisible="1"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_graph_analysis_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.graph.analysis.dashboard</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_graph_analysis"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//graph" position="attributes">
|
||||
<attribute name="stacked">False</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_view_graph_analysis_inherit_dashboard" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.view.graph.analysis.inherit</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_graph_analysis"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//graph" position="attributes">
|
||||
<attribute name="stacked">False</attribute>
|
||||
<attribute name="order">DESC</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='team_id']" position="replace">
|
||||
<field name="create_date" interval="day"/>
|
||||
</xpath>
|
||||
<field name="stage_id" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="helpdesk_ticket_report_view_cohort" model="ir.ui.view">-->
|
||||
<!-- <field name="name">helpdesk.ticket.report.analysis.cohort</field>-->
|
||||
<!-- <field name="model">helpdesk.ticket.report.analysis</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <cohort string="Tickets Analysis" date_start="create_date" date_stop="close_date" disable_linking="True" interval="day" sample="1"/>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="helpdesk_ticket_report_analysis_view_tree" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.list</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Tickets Analysis">
|
||||
<field name="ticket_id"/>
|
||||
<field name="priority" optional="show" widget="priority"/>
|
||||
<field name="team_id" optional="show"/>
|
||||
<field name="partner_id" widget="res_partner_many2one" optional="show"/>
|
||||
<field name="stage_id" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_report_analysis_view_search" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.report.analysis.search</field>
|
||||
<field name="model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="helpdesk_tickets_view_search_base"/>
|
||||
<field name="priority">20</field>
|
||||
<field name="arch" type="xml">
|
||||
<search position="attributes"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="helpdesk_ticket_analysis_action" model="ir.actions.act_window">
|
||||
<field name="name">Tickets Analysis</field>
|
||||
<field name="res_model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="path">tickets-analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="search_view_id" ref="helpdesk_ticket_report_analysis_view_search"/>
|
||||
<field name="domain">['|', ('company_id', '=', False), ('company_id', 'in', allowed_company_ids)]</field>
|
||||
<field name="context">{
|
||||
'search_default_group_by_create_date': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet!
|
||||
</p><p>
|
||||
Get statistics on your tickets and how long it takes to assign and resolve them.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_helpdesk_ticket_analysis_graph" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="helpdesk_ticket_view_graph_analysis"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_analysis_action"/>
|
||||
</record>
|
||||
|
||||
<record id="action_helpdesk_ticket_analysis_pivot" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="helpdesk_ticket_view_pivot_analysis"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_analysis_action"/>
|
||||
</record>
|
||||
|
||||
<!-- <record id="action_helpdesk_ticket_analysis_cohort" model="ir.actions.act_window.view">-->
|
||||
<!-- <field name="sequence" eval="10"/>-->
|
||||
<!-- <field name="view_mode">cohort</field>-->
|
||||
<!-- <field name="view_id" ref="helpdesk_ticket_report_view_cohort"/>-->
|
||||
<!-- <field name="act_window_id" ref="helpdesk_ticket_analysis_action"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="helpdesk_ticket_analysis_dashboard_action" model="ir.actions.act_window">
|
||||
<field name="name">Ticket Analysis</field>
|
||||
<field name="res_model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="context">{
|
||||
'search_default_group_by_create_date': 1,
|
||||
}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No data yet!
|
||||
</p><p>
|
||||
Get statistics on your tickets and how long it takes to assign and resolve them.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_helpdesk_ticket_analysis_dashboard_graph_view" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="5"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="helpdesk_ticket_view_graph_analysis_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_analysis_dashboard_action"/>
|
||||
</record>
|
||||
|
||||
<record id="action_helpdesk_ticket_analysis_dashboard_pivot_view" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="helpdesk_ticket_view_pivot_analysis_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_analysis_dashboard_action"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_action_7dayssuccess" model="ir.actions.act_window">
|
||||
<field name="name">Success Rate Analysis</field>
|
||||
<field name="res_model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="domain" eval="[('close_date', '>=', (DateTime.today() - relativedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))]"/>
|
||||
<field name="search_view_id" ref="helpdesk.helpdesk_ticket_report_analysis_view_search"/>
|
||||
<field name="context">{
|
||||
'search_default_is_close': True,
|
||||
'search_default_my_ticket': True,
|
||||
'search_default_sla_success': True,
|
||||
'search_default_closed_on': 'custom_closed_on_last_7_days',
|
||||
'pivot_measures': ['__count__'],
|
||||
}</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="helpdesk_ticket_action_7dayssuccess_pivot" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="19"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="helpdesk.helpdesk_ticket_view_pivot_analysis_7dayssuccess_inherit_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_action_7dayssuccess"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_action_7dayssuccess_graph" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="29"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_action_7dayssuccess"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_action_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Ticket Analysis</field>
|
||||
<field name="res_model">helpdesk.ticket.report.analysis</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
<field name="search_view_id" ref="helpdesk.helpdesk_ticket_report_analysis_view_search"/>
|
||||
<field name="domain">[('stage_id.fold', '=', False)]</field>
|
||||
<field name="context">{
|
||||
'search_default_my_ticket': True,
|
||||
'search_default_is_open': True,
|
||||
}</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="helpdesk_ticket_action_dashboard_pivot" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="16"/>
|
||||
<field name="view_mode">pivot</field>
|
||||
<field name="view_id" ref="helpdesk.helpdesk_ticket_view_pivot_analysis_inherit_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_action_dashboard"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_action_dashboard_graph" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="25"/>
|
||||
<field name="view_mode">graph</field>
|
||||
<field name="view_id" ref="helpdesk.helpdesk_ticket_view_graph_analysis_inherit_dashboard"/>
|
||||
<field name="act_window_id" ref="helpdesk_ticket_action_dashboard"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_helpdesk_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_services_helpdesk"/>
|
||||
</record>
|
||||
|
||||
<record id="group_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_helpdesk_user')), (4, ref('mail.group_mail_canned_response_admin'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</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="group_use_rating" model="res.groups">
|
||||
<field name="name">Show Customer Ratings</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_auto_assignment" model="res.groups">
|
||||
<field name="name">Auto Assigment</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('helpdesk.group_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<data noupdate="1">
|
||||
<!-- Manager gets all team access rights -->
|
||||
<record id="helpdesk_manager_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Administrator</field>
|
||||
<field name="model_id" ref="model_helpdesk_team"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_manager_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket Administrator</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
<!-- user only gets to read his own teams (or open teams) -->
|
||||
<record id="helpdesk_user_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk User</field>
|
||||
<field name="model_id" ref="model_helpdesk_team"/>
|
||||
<field name="domain_force">['|',
|
||||
('privacy_visibility', '!=', 'invited_internal'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_user'))]"/>
|
||||
</record>
|
||||
<record id="helpdesk_ticket_user_rule" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket User</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket"/>
|
||||
<field name="domain_force">['|',
|
||||
'|',
|
||||
('team_id.privacy_visibility', '!=', 'invited_internal'),
|
||||
('team_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
<!-- Split by company on tickets, teams and SLAs -->
|
||||
<record id="helpdesk_ticket_company_rule" model="ir.rule">
|
||||
<field name="name">Ticket: multi-company</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
<record id="helpdesk_team_company_rule" model="ir.rule">
|
||||
<field name="name">Team: multi-company</field>
|
||||
<field name="model_id" ref="model_helpdesk_team"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="helpdesk_sla_company_rule" model="ir.rule">
|
||||
<field name="name">SLA: multi-company</field>
|
||||
<field name="model_id" ref="model_helpdesk_sla"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
<record id="helpdesk_portal_ticket_rule" model="ir.rule">
|
||||
<field name="name">Tickets: portal users: following</field>
|
||||
<field name="model_id" ref="helpdesk.model_helpdesk_ticket"/>
|
||||
<field name="domain_force">[
|
||||
('team_privacy_visibility', '=', 'portal'),
|
||||
'|',
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('team_id.message_partner_ids', 'in', [user.partner_id.id])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_rule_manager" model="ir.rule">
|
||||
<field name="name">Helpdesk SLA Report: multi-company</field>
|
||||
<field name="model_id" ref="model_helpdesk_sla_report_analysis"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_sla_report_analysis_rule_user" model="ir.rule">
|
||||
<field name="name">Helpdesk SLA Report: Helpdesk Ticket User</field>
|
||||
<field name="model_id" ref="model_helpdesk_sla_report_analysis"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
('team_id.privacy_visibility', '!=', 'invited_internal'),
|
||||
'|',
|
||||
('team_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('ticket_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_report_analysis_rule_multi_company" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket Report: multi-company</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket_report_analysis"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_report_analysis_rule_user" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket Report: Helpdesk Ticket User</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket_report_analysis"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
('team_id.privacy_visibility', '!=', 'invited_internal'),
|
||||
'|',
|
||||
('team_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('ticket_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="helpdesk_ticket_report_analysis_rule_manager" model="ir.rule">
|
||||
<field name="name">Helpdesk Ticket Report: Helpdesk Ticket Administrator</field>
|
||||
<field name="model_id" ref="model_helpdesk_ticket_report_analysis"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('group_helpdesk_manager'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_helpdesk_tag,helpdesk.tag,model_helpdesk_tag,helpdesk.group_helpdesk_user,1,1,1,1
|
||||
access_helpdesk_tag_internal_user,helpdesk.tag,model_helpdesk_tag,base.group_user,1,0,0,0
|
||||
access_helpdesk_sla_internal_user,helpdesk.sla,model_helpdesk_sla,base.group_user,1,0,0,0
|
||||
access_helpdesk_sla_status_internal_user,helpdesk.sla.status,model_helpdesk_sla_status,base.group_user,1,0,0,0
|
||||
access_helpdesk_sla_manager,helpdesk.sla.manager,model_helpdesk_sla,helpdesk.group_helpdesk_manager,1,1,1,1
|
||||
access_helpdesk_stage_internal_user,helpdesk.stage,model_helpdesk_stage,base.group_user,1,0,0,0
|
||||
access_helpdesk_stage_manager,helpdesk.stage.manager,model_helpdesk_stage,helpdesk.group_helpdesk_manager,1,1,1,1
|
||||
access_helpdesk_stage_portal,helpdesk.stage.portal,helpdesk.model_helpdesk_stage,base.group_portal,1,0,0,0
|
||||
access_helpdesk_ticket_portal,helpdesk.ticket.portal,helpdesk.model_helpdesk_ticket,base.group_portal,1,0,0,0
|
||||
access_helpdesk_ticket,helpdesk.ticket,model_helpdesk_ticket,helpdesk.group_helpdesk_user,1,1,1,1
|
||||
access_helpdesk_ticket_internal_user,helpdesk.ticket_on_internal_user,model_helpdesk_ticket,base.group_user,1,0,0,0
|
||||
access_helpdesk_team_public_public,helpdesk.team,model_helpdesk_team,base.group_public,1,0,0,0
|
||||
access_helpdesk_team_public_portal,helpdesk.team,model_helpdesk_team,base.group_portal,1,0,0,0
|
||||
access_helpdesk_team_public_employee,helpdesk.team,model_helpdesk_team,base.group_user,1,0,0,0
|
||||
access_helpdesk_team_manager,helpdesk.team.manager,model_helpdesk_team,helpdesk.group_helpdesk_manager,1,1,1,1
|
||||
access_helpdesk_sla_report_analysis_manager,access_helpdesk_sla_report_analysis_manager,model_helpdesk_sla_report_analysis,helpdesk.group_helpdesk_manager,1,0,0,0
|
||||
access_helpdesk_sla_report_analysis_user,access_helpdesk_sla_report_analysis_user,model_helpdesk_sla_report_analysis,helpdesk.group_helpdesk_user,1,0,0,0
|
||||
access_mail_activity_type_helpdesk_manager,mail.activity.type.helpdesk.manager,mail.model_mail_activity_type,helpdesk.group_helpdesk_manager,1,1,1,1
|
||||
access_helpdesk_ticket_report_analysis_manager,helpdesk.ticket.report.analysis.manager,model_helpdesk_ticket_report_analysis,helpdesk.group_helpdesk_manager,1,0,0,0
|
||||
access_helpdesk_ticket_report_analysis_user,helpdesk.ticket.report.analysis.user,model_helpdesk_ticket_report_analysis,helpdesk.group_helpdesk_user,1,0,0,0
|
||||
access_helpdesk_stage_delete_wizard,helpdesk.stage.delete.wizard,model_helpdesk_stage_delete_wizard,helpdesk.group_helpdesk_manager,1,1,1,0
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue