diff --git a/odex25_helpdesk/odex25_helpdesk/__init__.py b/odex25_helpdesk/odex25_helpdesk/__init__.py new file mode 100644 index 000000000..29fd41cc7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models +from . import report diff --git a/odex25_helpdesk/odex25_helpdesk/__manifest__.py b/odex25_helpdesk/odex25_helpdesk/__manifest__.py new file mode 100644 index 000000000..015eec16b --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/__manifest__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Helpdesk', + 'version': '1.3', + 'sequence': 110, + 'summary': 'Track, prioritize, and solve customer tickets', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': [ + 'base_setup', + 'mail', + 'utm', + 'rating', + 'web_tour', + 'resource', + 'portal', + 'digest', + ], + 'description': """ +Odex25 helpdesk - Ticket Management App +================================ + +Features: + + - Process tickets through different stages to solve them. + - Add priorities, types, descriptions and tags to define your tickets. + - Use the chatter to communicate additional information and ping co-workers on tickets. + - Enjoy the use of an adapted dashboard, and an easy-to-use kanban view to handle your tickets. + - Make an in-depth analysis of your tickets through the pivot view in the reports menu. + - Create a team and define its members, use an automatic assignment method if you wish. + - Use a mail alias to automatically create tickets and communicate with your customers. + - Add Service Level Agreement deadlines automatically to your tickets. + - Get customer feedback by using ratings. + - Install additional features easily using your team form view. + + """, + 'data': [ + 'security/odex25_helpdesk_security.xml', + 'security/ir.model.access.csv', + 'data/digest_data.xml', + 'data/mail_data.xml', + # 'data/odex25_helpdesk_data.xml', + 'views/odex25_helpdesk_views.xml', + 'views/odex25_helpdesk_views.xml', + 'views/zfp_config_setting.xml', + 'views/odex25_helpdesk_team_views.xml', + 'views/assets.xml', + 'views/digest_views.xml', + 'views/odex25_helpdesk_portal_templates.xml', + # 'views/res_partner_views.xml', + 'views/mail_activity_views.xml', + 'report/odex25_helpdesk_sla_report_analysis_views.xml', + ], + 'qweb': [ + "static/src/xml/odex25_helpdesk_team_templates.xml", + ], + # 'demo': ['data/odex25_helpdesk_demo.xml'], + 'application': True, +} diff --git a/odex25_helpdesk/odex25_helpdesk/controllers/__init__.py b/odex25_helpdesk/odex25_helpdesk/controllers/__init__.py new file mode 100644 index 000000000..38a97908f --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import portal +from . import rating diff --git a/odex25_helpdesk/odex25_helpdesk/controllers/portal.py b/odex25_helpdesk/odex25_helpdesk/controllers/portal.py new file mode 100644 index 000000000..0a9a5662c --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/controllers/portal.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- + +from operator import itemgetter + +from odoo import http +from odoo.exceptions import AccessError, MissingError, UserError +from odoo.http import request +from odoo.tools.translate import _ +from odoo.tools import groupby as groupbyelem +from odoo.addons.portal.controllers.portal import pager as portal_pager, CustomerPortal +from odoo.osv.expression import OR + + +class CustomerPortal(CustomerPortal): + + def _prepare_portal_layout_values(self): + values = super(CustomerPortal, self)._prepare_portal_layout_values() + if values.get('sales_user', False): + values['title'] = _("Salesperson") + return values + + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if 'ticket_count' in counters: + values['ticket_count'] = request.env['odex25_helpdesk.ticket'].search_count([]) + return values + + def _ticket_get_page_view_values(self, ticket, access_token, **kwargs): + values = { + 'page_name': 'ticket', + 'ticket': ticket, + } + return self._get_page_view_values(ticket, access_token, values, 'my_tickets_history', False, **kwargs) + + @http.route(['/my/tickets', '/my/tickets/page/'], type='http', auth="user", website=True) + def my_odex25_helpdesk_tickets(self, page=1, date_begin=None, date_end=None, sortby=None, filterby='all', search=None, groupby='none', search_in='content', **kw): + values = self._prepare_portal_layout_values() + + searchbar_sortings = { + 'date': {'label': _('Newest'), 'order': 'create_date desc'}, + 'name': {'label': _('Subject'), 'order': 'name'}, + 'stage': {'label': _('Stage'), 'order': 'stage_id'}, + 'reference': {'label': _('Reference'), 'order': 'id'}, + 'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'}, + } + searchbar_filters = { + 'all': {'label': _('All'), 'domain': []}, + 'assigned': {'label': _('Assigned'), 'domain': [('user_id', '!=', False)]}, + 'unassigned': {'label': _('Unassigned'), 'domain': [('user_id', '=', False)]}, + 'open': {'label': _('Open'), 'domain': [('close_date', '=', False)]}, + 'closed': {'label': _('Closed'), 'domain': [('close_date', '!=', False)]}, + 'last_message_sup': {'label': _('Last message is from support')}, + 'last_message_cust': {'label': _('Last message is from customer')}, + } + searchbar_inputs = { + 'content': {'input': 'content', 'label': _('Search (in Content)')}, + 'message': {'input': 'message', 'label': _('Search in Messages')}, + 'customer': {'input': 'customer', 'label': _('Search in Customer')}, + 'id': {'input': 'id', 'label': _('Search in Reference')}, + 'status': {'input': 'status', 'label': _('Search in Stage')}, + 'all': {'input': 'all', 'label': _('Search in All')}, + } + searchbar_groupby = { + 'none': {'input': 'none', 'label': _('None')}, + 'stage': {'input': 'stage_id', 'label': _('Stage')}, + } + + # default sort by value + if not sortby: + sortby = 'date' + order = searchbar_sortings[sortby]['order'] + + if filterby in ['last_message_sup', 'last_message_cust']: + discussion_subtype_id = request.env.ref('mail.mt_comment').id + messages = request.env['mail.message'].search_read([('model', '=', 'odex25_helpdesk.ticket'), ('subtype_id', '=', discussion_subtype_id)], fields=['res_id', 'author_id'], order='date desc') + last_author_dict = {} + for message in messages: + if message['res_id'] not in last_author_dict: + last_author_dict[message['res_id']] = message['author_id'][0] + + ticket_author_list = request.env['odex25_helpdesk.ticket'].search_read(fields=['id', 'partner_id']) + ticket_author_dict = dict([(ticket_author['id'], ticket_author['partner_id'][0] if ticket_author['partner_id'] else False) for ticket_author in ticket_author_list]) + + last_message_cust = [] + last_message_sup = [] + for ticket_id in last_author_dict.keys(): + if last_author_dict[ticket_id] == ticket_author_dict[ticket_id]: + last_message_cust.append(ticket_id) + else: + last_message_sup.append(ticket_id) + + if filterby == 'last_message_cust': + domain = [('id', 'in', last_message_cust)] + else: + domain = [('id', 'in', last_message_sup)] + + else: + domain = searchbar_filters[filterby]['domain'] + + if date_begin and date_end: + domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] + + # search + if search and search_in: + search_domain = [] + if search_in in ('id', 'all'): + search_domain = OR([search_domain, [('id', 'ilike', search)]]) + if search_in in ('content', 'all'): + search_domain = OR([search_domain, ['|', ('name', 'ilike', search), ('description', 'ilike', search)]]) + if search_in in ('customer', 'all'): + search_domain = OR([search_domain, [('partner_id', 'ilike', search)]]) + if search_in in ('message', 'all'): + discussion_subtype_id = request.env.ref('mail.mt_comment').id + search_domain = OR([search_domain, [('message_ids.body', 'ilike', search), ('message_ids.subtype_id', '=', discussion_subtype_id)]]) + if search_in in ('status', 'all'): + search_domain = OR([search_domain, [('stage_id', 'ilike', search)]]) + domain += search_domain + + # pager + tickets_count = len(request.env['odex25_helpdesk.ticket'].search(domain)) + pager = portal_pager( + url="/my/tickets", + url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'search_in': search_in, 'search': search}, + total=tickets_count, + page=page, + step=self._items_per_page + ) + + tickets = request.env['odex25_helpdesk.ticket'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) + request.session['my_tickets_history'] = tickets.ids[:100] + + if groupby == 'stage': + grouped_tickets = [request.env['odex25_helpdesk.ticket'].concat(*g) for k, g in groupbyelem(tickets, itemgetter('stage_id'))] + else: + grouped_tickets = [tickets] + + values.update({ + 'date': date_begin, + 'grouped_tickets': grouped_tickets, + 'page_name': 'ticket', + 'default_url': '/my/tickets', + 'pager': pager, + 'searchbar_sortings': searchbar_sortings, + 'searchbar_filters': searchbar_filters, + 'searchbar_inputs': searchbar_inputs, + 'searchbar_groupby': searchbar_groupby, + 'sortby': sortby, + 'groupby': groupby, + 'search_in': search_in, + 'search': search, + 'filterby': filterby, + }) + return request.render("odex25_helpdesk.portal_odex25_helpdesk_ticket", values) + + @http.route([ + "/odex25_helpdesk/ticket/", + "/odex25_helpdesk/ticket//", + '/my/ticket/', + '/my/ticket//' + ], type='http', auth="public", website=True) + def tickets_followup(self, ticket_id=None, access_token=None, **kw): + try: + ticket_sudo = self._document_check_access('odex25_helpdesk.ticket', ticket_id, access_token) + except (AccessError, MissingError): + return request.redirect('/my') + + values = self._ticket_get_page_view_values(ticket_sudo, access_token, **kw) + return request.render("odex25_helpdesk.tickets_followup", values) + + @http.route([ + '/my/ticket/close/', + '/my/ticket/close//', + ], type='http', auth="public", website=True) + def ticket_close(self, ticket_id=None, access_token=None, **kw): + try: + ticket_sudo = self._document_check_access('odex25_helpdesk.ticket', ticket_id, access_token) + except (AccessError, MissingError): + return request.redirect('/my') + + if not ticket_sudo.team_id.allow_portal_ticket_closing: + raise UserError(_("The team does not allow ticket closing through portal")) + + if not ticket_sudo.closed_by_partner: + closing_stage = ticket_sudo.team_id._get_closing_stage() + if ticket_sudo.stage_id != closing_stage: + ticket_sudo.write({'stage_id': closing_stage[0].id, 'closed_by_partner': True}) + else: + ticket_sudo.write({'closed_by_partner': True}) + body = _('Ticket closed by the customer') + ticket_sudo.with_context(mail_create_nosubscribe=True).message_post(body=body, message_type='comment', subtype_xmlid='mail.mt_note') + + return request.redirect('/my/ticket/%s/%s' % (ticket_id, access_token or '')) diff --git a/odex25_helpdesk/odex25_helpdesk/controllers/rating.py b/odex25_helpdesk/odex25_helpdesk/controllers/rating.py new file mode 100644 index 000000000..bfa11c6d8 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/controllers/rating.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +import datetime + +from odoo import http +from odoo.http import request +from odoo.osv.expression import AND + + +class Websiteodex25_helpdesk(http.Controller): + + @http.route(['/odex25_helpdesk/rating', '/odex25_helpdesk/rating/'], type='http', auth="public", website=True, sitemap=True) + def page(self, team=False, **kw): + # to avoid giving any access rights on Helpdesk team to the public user, let's use sudo + # and check if the user should be able to view the team (team managers only if it's not published or has no rating) + user = request.env.user + team_domain = [('id', '=', team.id)] if team else [] + if user.has_group('odex25_helpdesk.group_heldpesk_manager'): + domain = AND([[('use_rating', '=', True)], team_domain]) + else: + domain = AND([[('use_rating', '=', True), ('portal_show_rating', '=', True)], team_domain]) + teams = request.env['odex25_helpdesk.team'].search(domain) + team_values = [] + for team in teams: + tickets = request.env['odex25_helpdesk.ticket'].sudo().search([('team_id', '=', team.id)]) + domain = [ + ('res_model', '=', 'odex25_helpdesk.ticket'), ('res_id', 'in', tickets.ids), + ('consumed', '=', True), ('rating', '>=', 1), + ] + ratings = request.env['rating.rating'].sudo().search(domain, order="id desc", limit=100) + + yesterday = (datetime.date.today()-datetime.timedelta(days=-1)).strftime('%Y-%m-%d 23:59:59') + stats = {} + any_rating = False + for x in (7, 30, 90): + todate = (datetime.date.today()-datetime.timedelta(days=x)).strftime('%Y-%m-%d 00:00:00') + domdate = domain + [('create_date', '<=', yesterday), ('create_date', '>=', todate)] + stats[x] = {1: 0, 3: 0, 5: 0} + rating_stats = request.env['rating.rating'].sudo().read_group(domdate, [], ['rating']) + total = sum(st['rating_count'] for st in rating_stats) + for rate in rating_stats: + any_rating = True + stats[x][rate['rating']] = (rate['rating_count'] * 100) / total + values = { + 'team': team, + 'ratings': ratings if any_rating else False, + 'stats': stats, + } + team_values.append(values) + return request.render('odex25_helpdesk.team_rating_page', {'page_name': 'rating', 'teams': team_values}) diff --git a/odex25_helpdesk/odex25_helpdesk/data/digest_data.xml b/odex25_helpdesk/odex25_helpdesk/data/digest_data.xml new file mode 100644 index 000000000..171c8292e --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/data/digest_data.xml @@ -0,0 +1,27 @@ + + + + + True + + + + + + Tip: Create tickets from incoming emails + 1800 + + +
+ % set record = object.env['odex25_helpdesk.team'].search([('alias_name', '!=', False)],limit=1) + Tip: Create tickets from incoming emails + % if record and record.alias_domain +

Emails sent to ${record.alias_id.display_name} generate tickets in your pipeline.

+ % else +

Emails sent to a Helpdesk Team alias generate tickets in your pipeline.

+ % endif +
+
+
+
+
diff --git a/odex25_helpdesk/odex25_helpdesk/data/mail_data.xml b/odex25_helpdesk/odex25_helpdesk/data/mail_data.xml new file mode 100644 index 000000000..deb125225 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/data/mail_data.xml @@ -0,0 +1,184 @@ + + + + + + Handle Ticket + fa-ticket + + + + + + Ticket Created + 0 + odex25_helpdesk.ticket + + + Ticket created + + + Ticket Rated + 5 + odex25_helpdesk.ticket + + + Ticket rated + + + Stage Changed + 10 + odex25_helpdesk.ticket + + + Stage Changed + + + + + Ticket Created + 0 + odex25_helpdesk.team + + + team_id + + + Ticket Rated + 5 + odex25_helpdesk.team + + + team_id + + + Ticket Stage Changed + 10 + odex25_helpdesk.team + + + + team_id + + + + Ticket: Reception Acknowledgment + + ${object.display_name} + ${(object.user_id.email_formatted or user.email_formatted) | safe} + ${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe} + ${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''} + +
+ Dear ${object.sudo().partner_id.name or 'Madam/Sir'},

+ Your request + % if object.get_portal_url(): + ${object.name} + % endif + has been received and is being reviewed by our ${object.team_id.name or ''} team. + The reference of your ticket is ${object.id}.

+ + + + To add additional comments, reply to this email.

+ + Thank you,

+ ${object.team_id.name or 'Helpdesk'} Team. +
+
+ ${object.partner_id.lang or object.user_id.lang or user.lang} + +
+ + + Ticket: Solved + + ${object.display_name} + ${(object.user_id.email_formatted or user.email_formatted) | safe} + ${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe} + ${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''} + +
+ Dear ${object.sudo().partner_id.name or 'Madam/Sir'},

+ This automatic message informs you that we have closed your ticket (reference ${object.id}). + We hope that the services provided have met your expectations. + If you have any more questions or comments, don't hesitate to reply to this e-mail to re-open your ticket.

+ Thank you for your cooperation.
+ Kind regards,

+ ${object.team_id.name or 'Helpdesk'} Team. +
+
+ ${object.partner_id.lang or object.user_id.lang or user.lang} + +
+ + + Ticket: Rating Request (requires rating enabled on team) + + ${object.company_id.name or object.user_id.company_id.name or 'Helpdesk'}: Service Rating Request + ${object.rating_get_rated_partner_id().email_formatted | safe} + ${(object.partner_email if not object.sudo().partner_id.email or object.sudo().partner_id.email != object.partner_email else '') | safe} + ${object.partner_id.id if object.sudo().partner_id.email and object.sudo().partner_id.email == object.partner_email else ''} + +
+ % set access_token = object.rating_get_access_token() + % set partner = object.rating_get_partner_id() + + + + + + +
+ % if partner.name: + Hello ${partner.name},
+ % else: + Hello,
+ % endif + Please take a moment to rate our services related to the ticket "${object.name}" + % if object.rating_get_rated_partner_id().name: + assigned to ${object.rating_get_rated_partner_id().name}.
+ % else: + .
+ % endif +
+ + + +
+ Tell us how you feel about our service
+ (click on one of these smileys) +
+ + + + + + +
+ + Satisfied + + + + Not satisfied + + + + Highly Dissatisfied + +
+
+
+ We appreciate your feedback. It helps us to improve continuously. +
This customer survey has been sent because your ticket has been moved to the stage ${object.stage_id.name} +
+
+
+ ${object.partner_id.lang or object.user_id.lang or user.lang} + +
+
+
diff --git a/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_data.xml b/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_data.xml new file mode 100644 index 000000000..ca2004aed --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_data.xml @@ -0,0 +1,83 @@ + + + + + Customer Care + support + + + + + + + New + 0 + + + + + + In Progress + 1 + + + + + Solved + + 2 + + + + + Cancelled + 3 + + + + + + + Question + + + Issue + + + + + Share + + + form + code + action = records.action_share() + + + + + Status Per Deadline + odex25_helpdesk.sla.report.analysis + { + 'pivot_column_groupby': ['sla_deadline:day'], + 'pivot_row_groupby': ['team_id', 'ticket_id', 'sla_id'] + } + [] + + + + + + Failed SLA Stage per Month + odex25_helpdesk.sla.report.analysis + { + 'pivot_measures': ['__count'], + 'pivot_column_groupby': ['create_date:month'], + 'pivot_row_groupby': ['team_id', 'sla_stage_id'] + } + [] + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_demo.xml b/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_demo.xml new file mode 100644 index 000000000..b17232eef --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/data/odex25_helpdesk_demo.xml @@ -0,0 +1,384 @@ + + + + CRM + + + Website + + + Service + + + Repair + + + + + + + + + + Done + 2 + + + + + + + + VIP Support + + + + + + + + 2 days to start + + + 2 + + + + 7 days to finish + + + 7 + + + + Assigned within 24 hrs + + + 1 + 3 + + + assigning + + + + + Kitchen collapsing + + 3 + + + + + Where can I download a catalog ? + 0 + + + + + Warranty + + + + 2 + + + +Hello, + +I would like to know what kind of warranties you are offering for your products. + +Here is my contact number: 123456789 + +Thank you, +Chester Reed + + + + + + + + + + Wood Treatment + + + + + + +Hello, + +Is the wood from your furniture treated with a particular product? What would you recommend to maintain the quality of a dining table? + +Your assistance would be greatly appreciated. + +Thanks in Advance, +Azure Interior + + + + Chair dimensions + + + + + + +Can you please tell me the dimensions of your “Office chair Black”? Also I am unable to find the information on your official site. + +I look forward to your kind response. + +Thank you! + + + + Lost key + + + + + + +Hello, + +I bought a locker a few years ago and I, unfortunately, lost the key. I cannot retrieve the documents I had left in there without damaging the furniture item. What solution do you offer? + +Thanks in advance for your help. +Kind regards, +Gemini Furniture + + + + Furniture delivery + + + + + +Hi, + +I was wondering if you were delivering the furniture or if we needed to pick it up at your warehouse? +If you do take care of the delivery, are there any extra costs? + +Regards, +Deco Addict + + + + Cabinets in kit + + + + + +Hello, + +I would like to know if your cabinets come in a kit? They seem quite large and I am not sure they will fit through my front door. + +Thank you for your help. +Best regards, +Jackson Group + + + + Missing user manual + + + + + +Hello, + +I recently purchased one of your wardrobes in a kit. Unfortunately, I didn’t receive the user manual, so I cannot assemble the item. Could you send me this document? + +Thank you. +Kind regards, + + + + Ugly Chair + + + + + + +Hello, + +I purchased a chair from you last week. I now realize it doesn’t go well with the rest of my furniture, so I would like to return it and to get a refund. + +Regards, +Deco Addict + + + + Couch + + + + + + +Hello, + +The couch I ordered was scratched during the delivery. Would it be possible to have a gesture of goodwill? + +Thank you for considering my request. +Best regards, + + + + Chair wheels aren’t working + + + + + 3 + + + +The chair I bought last year isn't turning correctly anymore. Are you selling spare parts for the wheels? + +Thank you in advance for your help. +Chester Reed + + + + + + + + + + Cabinet Colour and Lock aren't proper + + + + + + 3 + + +Hi, + +I purchased a "Cabinet With Doors" from your store a few days ago. The lock is not working properly and the color is wrong. This is unacceptable! I am asking for a product that corresponds to my order and that matches the quality you are advertising. + +Regards, +The Jackson Group + + + + Lamp Stand is bent + + + + + + 2 + +Hello, + +Yesterday I purchased a lamp stand from your site but the product I received is bent. + +Would it be possible to get a replacement? + +Regards, +Ready Mat + + + + Table legs are unbalanced + + + + + + 3 + + +Hi, + +A few days ago, I bought a Four Persons Desk. While assembling it in my office, I found that the legs of the table were not properly balanced. Could you please come and fix this? + +Kindly do this as early as possible. + +Best, +Azure Interior + + + + Drawer’s slides and handle have a defect + + + + + + +Hi, + +I have purchased a "Drawer" from your store but the slides and the handle seem to have a defect. + +Would it be possible for you to fix it? + +Regards, +Deco + + + + Want to change the place of the dining area + + + + + + +Hello, + +I want to change the location of the dining area and would like your advice. + +Hope to hear from you soon. + +Best, +Gemini Furniture + + + + Received Product is damaged + + + + + + +Hi, + +I ordered a "Table Kit" from your store but the delivered product is damaged. I demand a refund as soon as possible. + +Regards, + + + + Delivered wood panel is not what I ordered + + + + + + +Hello, + +I ordered a wood panel from your online store, but the delivered product is not what I had ordered. + +Could you please replace it with the right product? +Waiting for your response. + +Best, +Wood Corner + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/i18n/ar.po b/odex25_helpdesk/odex25_helpdesk/i18n/ar.po new file mode 100644 index 000000000..056288563 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/i18n/ar.po @@ -0,0 +1,3489 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk +# +# Translators: +# Sherif Abd Ekmoniem , 2020 +# Yihya Hugirat , 2020 +# Sadig Adam , 2020 +# Ghaith Gammar , 2020 +# Zuhair Hammadi , 2020 +# Talal Kamal , 2020 +# Akram Alfusayal , 2020 +# amrnegm , 2020 +# Martin Trigaux, 2020 +# hoxhe Aits , 2020 +# Ali zuaby , 2020 +# Osoul , 2020 +# Mohammed Ibrahim , 2020 +# Osama Ahmaro , 2020 +# amal ahmed , 2020 +# Shaima Safar , 2020 +# Amer Hazaa , 2020 +# Mostafa Hanafy , 2020 +# Nisrine Tagri , 2020 +# Tasneem Sarhan , 2020 +# Talal Albahra , 2020 +# Mustafa Rawi , 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-29 14:04+0000\n" +"PO-Revision-Date: 2020-09-07 08:20+0000\n" +"Last-Translator: Mustafa Rawi , 2020\n" +"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: odex25_helpdesk +#: model:mail.template,subject:odex25_helpdesk.rating_ticket_request_email_template +msgid "" +"${object.company_id.name or object.user_id.company_id.name or 'Helpdesk'}: " +"Service Rating Request" +msgstr "" +"${object.company_id.name or object.user_id.company_id.name or 'مكتب الدعم'}:" +" طلب تقييم الخدمة" + +#. module: odex25_helpdesk +#: model:mail.template,subject:odex25_helpdesk.new_ticket_request_email_template +#: model:mail.template,subject:odex25_helpdesk.solved_ticket_request_email_template +msgid "${object.display_name}" +msgstr "${object.display_name}" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "${record.alias_id.display_name}" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "% else" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "% endif" +msgstr "% endif" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "" +"% set record = object.env['odex25_helpdesk.team'].search([('alias_name', '!=', False)],limit=1)\n" +" Tip: Create tickets from incoming emails\n" +" % if record and record.alias_domain" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "&times;" +msgstr "&times;" + +#. module: odex25_helpdesk +#: model:mail.template,body_html:odex25_helpdesk.rating_ticket_request_email_template +msgid "" +"\n" +"
\n" +" % set access_token = object.rating_get_access_token()\n" +" % set partner = object.rating_get_partner_id()\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
\n" +" % if partner.name:\n" +" Hello ${partner.name},
\n" +" % else:\n" +" Hello,
\n" +" % endif\n" +" Please take a moment to rate our services related to the ticket \"${object.name}\"\n" +" % if object.rating_get_rated_partner_id().name:\n" +" assigned to ${object.rating_get_rated_partner_id().name}.
\n" +" % else:\n" +" .
\n" +" % endif\n" +"
\n" +" \n" +" \n" +" \n" +"
\n" +" Tell us how you feel about our service
\n" +" (click on one of these smileys)\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
\n" +" \n" +" \"Satisfied\"\n" +" \n" +" \n" +" \n" +" \"Not\n" +" \n" +" \n" +" \n" +" \"Highly\n" +" \n" +"
\n" +"
\n" +"
\n" +" We appreciate your feedback. It helps us to improve continuously.\n" +"
This customer survey has been sent because your ticket has been moved to the stage ${object.stage_id.name}\n" +"
\n" +"
\n" +" " +msgstr "" + +#. module: odex25_helpdesk +#: model:mail.template,body_html:odex25_helpdesk.solved_ticket_request_email_template +msgid "" +"\n" +"
\n" +" Dear ${object.sudo().partner_id.name or 'Madam/Sir'},

\n" +" This automatic message informs you that we have closed your ticket (reference ${object.id}).\n" +" We hope that the services provided have met your expectations.\n" +" If you have any more questions or comments, don't hesitate to reply to this e-mail to re-open your ticket.

\n" +" Thank you for your cooperation.
\n" +" Kind regards,

\n" +" ${object.team_id.name or 'Helpdesk'} Team.\n" +"
\n" +" " +msgstr "" +"\n" +"
\n" +" عزيزنا ${object.sudo().partner_id.name or 'Madam/Sir'}،

\n" +" هذه رسالة تلقائية لإخطاركم أننا أغلقنا تذكرتكم (رقم الإشارة: ${object.id}).\n" +" نأمل أن تكون خدماتنا قد ارتقت لمستوى توقعاتكم.\n" +" إذا كان لديكم أية تساؤلات أو تعليقات، لا تترددوا في الرد على هذه الرسالة لإعادة فتح التذكرة.

\n" +" شكرًا على تعاونكم.
\n" +" مع أطيب تحياتنا،

\n" +" فريق ${object.team_id.name or 'Helpdesk'}.\n" +"
\n" +" " + +#. module: odex25_helpdesk +#: model:mail.template,body_html:odex25_helpdesk.new_ticket_request_email_template +msgid "" +"\n" +"
\n" +" Dear ${object.sudo().partner_id.name or 'Madam/Sir'},

\n" +" Your request\n" +" % if object.get_portal_url():\n" +" ${object.name}\n" +" % endif\n" +" has been received and is being reviewed by our ${object.team_id.name or ''} team.\n" +" The reference of your ticket is ${object.id}.

\n" +"\n" +"
\n" +" View the ticket
\n" +"
\n" +"\n" +" To add additional comments, reply to this email.

\n" +"\n" +" Thank you,

\n" +" ${object.team_id.name or 'Helpdesk'} Team.\n" +"
\n" +" " +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "" +"Stars mark the ticket priority. You can change it directly " +"from here!" +msgstr "" +"عدد النجوم يحدد أولوية التذكرة. يمكنك تغييرهم مباشرة من هنا!" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +msgid "Tickets in stage:" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "No description" +msgstr "لا يوجد وصف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +msgid "" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +msgid "" +"&nbsp;" +msgstr "" +"&nbsp;" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +" Enable the External Email " +"Servers feature in the General Settings and indicate an alias domain" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Status:" +msgstr "الحالة:" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "" +"\n" +" If the issue has been solved, you can close the request.\n" +" " +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Close this ticket" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "" +"" +msgstr "" +"" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.view_partner_form_inherit_odex25_helpdesk +msgid " Tickets" +msgstr " التذاكر" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Assigned to" +msgstr "مُسند إلى" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Description" +msgstr "الوصف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Managed by" +msgstr "يديره" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Reported by" +msgstr "قُدم التقرير بواسطة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Reported on" +msgstr "قُدم التقرير عن" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_defaults +msgid "" +"A Python dictionary that will be evaluated to provide default values when " +"creating new records for this alias." +msgstr "" +"قاموس بايثون سيتم تقديره لتوفير قيم افتراضية عند إنشاء سجلات جديدة لهذا " +"اللقب." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__description +msgid "About Team" +msgstr "تعريف بالفريق" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__access_warning +msgid "Access warning" +msgstr "تحذير من خطأ بالوصول" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_needaction +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_needaction +msgid "Action Needed" +msgstr "إجراء مطلوب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__active +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__active +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__active +msgid "Active" +msgstr "نشط" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_ids +msgid "Activities" +msgstr "الأنشطة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "زخرفة استثناء النشاط" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_state +msgid "Activity State" +msgstr "حالة النشاط" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_type_icon +msgid "Activity Type Icon" +msgstr "أيقونة نوع النشاط" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.mail_activity_type_action_config_odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_menu_config_activity_type +msgid "Activity Types" +msgstr "أنواع النشاطات" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_stage_action +msgid "" +"Adapt your pipeline to your workflow and easily track the progress of your " +"tickets." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "Add a description..." +msgstr "إضافة وصف..." + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "" +"Add columns to configure stages for your tickets.
e.g. " +"Awaiting Customer Feedback, Customer Followup, ..." +msgstr "" +"إضافة أعمدة لإعداد مراحل تذاكرك.
مثلًا: بانتظار ملاحظات " +"العميل، ردود العميل, ..." + +#. module: odex25_helpdesk +#: model:res.groups,name:odex25_helpdesk.group_odex25_helpdesk_manager +msgid "Administrator" +msgstr "مدير" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "After Sales" +msgstr "بعد البيع" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_id +msgid "Alias" +msgstr "لقب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_contact +msgid "Alias Contact Security" +msgstr "لقب الاتصال الآمن" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_name +msgid "Alias Name" +msgstr "اسم اللقب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_domain +msgid "Alias domain" +msgstr "نطاق اللقب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_model_id +msgid "Aliased Model" +msgstr "الكائن الملقب" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__priority__0 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__priority__0 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__priority__0 +#, python-format +msgid "All" +msgstr "الكل" + +#. module: odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_menu_all +msgid "All Tickets" +msgstr "كافة التذاكر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__allow_portal_ticket_closing +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Allow customers to close their tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Allow product returns from tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +"Allow your customers to easily rate your services. Activate this option will" +" add a default email template on non folded closing stages" +msgstr "" +"اسمح لعملائك بتقييم خدماتك بسهولة. تفعيل هذا الخيار سيضيف قالب بريد إلكتروني" +" افتراضي في مراحل الإغلاق غير المطوية" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__is_self_assigned +msgid "Am I assigned" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Applies To Stage" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Apply on" +msgstr "يُطبق على" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Archive" +msgstr "أرشفة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Archived" +msgstr "مؤرشف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Are you sure you wish to proceed?" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Assign To Me" +msgstr "إسنادها لي" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Assign the ticket to someone." +msgstr "إسناد التذكرة لشخص." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "Assign to me" +msgstr "إسنادها لي" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Assigned" +msgstr "إسناد" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__user_id +msgid "Assigned To" +msgstr "أسندت إلى" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__user_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Assigned to" +msgstr "مسند إلى" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +msgid "Assignee" +msgstr "المُسند إليه" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__target_type__assigning +msgid "Assigning Ticket" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__assign_method +msgid "Assignment Method" +msgstr "طريقة التعيين" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "" +"At each stage employees can block or make task/issue ready for next stage.\n" +" You can define here labels that will be displayed for the state instead\n" +" of the default labels." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_attachment_count +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_attachment_count +msgid "Attachment Count" +msgstr "عدد المرفقات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__template_id +msgid "" +"Automated email sent to the ticket's customer when the ticket reaches this " +"stage." +msgstr "" +"رسالة الرد الآلي المرسلة للعميل مُقدم التذكرة عند وصولها لهذه المرحلة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__assign_method +msgid "" +"Automatic assignment method for new tickets:\n" +"\tManually: manual\n" +"\tRandomly: randomly but everyone gets the same amount\n" +"\tBalanced: to the person with the least amount of open tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_progress_data +msgid "Average" +msgstr "متوسط" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Avg 7 Days Happy Rating" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Avg 7 Success Rate" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Avg 7 days" +msgstr "متوسط 7 أيام" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Avg Open Hours" +msgstr "متوسط ساعات الفتح" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_progress_data +msgid "Bad" +msgstr "سيئ" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_team__assign_method__balanced +msgid "Balanced" +msgstr "متوازن" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#: model:odex25_helpdesk.stage,legend_blocked:odex25_helpdesk.stage_cancelled +#: model:odex25_helpdesk.stage,legend_blocked:odex25_helpdesk.stage_done +#: model:odex25_helpdesk.stage,legend_blocked:odex25_helpdesk.stage_in_progress +#: model:odex25_helpdesk.stage,legend_blocked:odex25_helpdesk.stage_new +#: model:odex25_helpdesk.stage,legend_blocked:odex25_helpdesk.stage_solved +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_1 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_10 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_11 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_12 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_13 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_14 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_15 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_16 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_17 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_18 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_19 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_2 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_3 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_4 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_5 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_6 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_7 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_8 +#: model:odex25_helpdesk.ticket,legend_blocked:odex25_helpdesk.odex25_helpdesk_ticket_9 +#, python-format +msgid "Blocked" +msgstr "محجوب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__campaign_id +msgid "Campaign" +msgstr "الحملة" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.stage,name:odex25_helpdesk.stage_cancelled +msgid "Cancelled" +msgstr "ملغي" + +#. module: odex25_helpdesk +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_odex25_helpdesk_team_not_portal_show_rating_if_not_use_rating +msgid "Cannot show ratings in portal if not using them" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Channels" +msgstr "القنوات" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Click here to view this team's tickets." +msgstr "اضغط هنا لعرض تذاكر هذا الفريق." + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "" +"Click these cards to open their form view, or drag & drop them " +"through the different stages of this team." +msgstr "" +"اضغط على هذه البطاقات لفتح واجهة عروضهم، أو اسحبهم وأسقطهم لنقلهم عبر" +" المراحل المختلفة لهذا الفريق." + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Click to set" +msgstr "انقر لضبط" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Close" +msgstr "اغلاق" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__close_date +msgid "Close date" +msgstr "تاريخ الإقفال" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Close the ticket" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Close ticket" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#, python-format +msgid "Closed" +msgstr "مغلق" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "Closed Ticket" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#, python-format +msgid "Closed Tickets" +msgstr "التذاكر المقفلة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_7days_analysis +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_close_analysis +msgid "Closed Tickets Analysis" +msgstr "تحليل التذاكر المقفلة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__closed_by_partner +msgid "Closed by Partner" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__is_close +msgid "Closing Stage" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__color +msgid "Color" +msgstr "اللون" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__color +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__color +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__color +msgid "Color Index" +msgstr "معرف اللون" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__kanban_state_label +msgid "Column Status" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__company_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__company_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__company_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__company_id +msgid "Company" +msgstr "شركة" + +#. module: odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_menu_config +msgid "Configuration" +msgstr "إعدادات التكوين" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Configure SLA Policies" +msgstr "إعداد سياسات اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Configure a custom domain" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.action_upcoming_sla_fail_all_tickets +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_sla +msgid "Congratulations!" +msgstr "تهانينا!" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_res_partner +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Contact" +msgstr "جهة الاتصال" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_coupons +msgid "Coupons" +msgstr "الكوبونات" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.email_template_action_odex25_helpdesk +msgid "Create a new template" +msgstr "إنشاء قالب جديد" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_7days_analysis +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_7dayssuccess +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_close_analysis +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_success +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_team_performance +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_analysis_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_team_analysis_action +msgid "Create tickets to get statistics." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__create_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__create_uid +msgid "Created by" +msgstr "أنشئ بواسطة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__create_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__create_date +msgid "Created on" +msgstr "أنشئ في" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Creation Date" +msgstr "تاريخ الإنشاء" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Current stage of this ticket" +msgstr "المرحلة الحالية لهذه التذكرة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_bounced_content +msgid "Custom Bounced Message" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk_ticket.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__partner_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__partner_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#, python-format +msgid "Customer" +msgstr "الموظف / العميل" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.team,name:odex25_helpdesk.odex25_helpdesk_team1 +msgid "Customer Care" +msgstr "خدمة العملاء" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk_ticket.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__partner_email +#, python-format +msgid "Customer Email" +msgstr "البريد الإلكتروني" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__partner_name +msgid "Customer Name" +msgstr "اسم العميل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__access_url +msgid "Customer Portal URL" +msgstr "رابط بوابة العميل" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "Customer Tickets" +msgstr "تذاكر العميل" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Daily Target" +msgstr "الهدف اليومي" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js:0 +#, python-format +msgid "Dashboard" +msgstr "لوحة المعلومات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla_status__reached_datetime +msgid "Datetime at which the SLA stage was reached for the first time" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_exceeded_days +msgid "Day to reach SLA" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_exceeded_days +msgid "" +"Day to reach the stage of the SLA, without taking the working calendar into " +"account" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__time_days +msgid "Days" +msgstr "الأيام" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__time_days +msgid "Days to reach given stage based on ticket creation date" +msgstr "عدد الأيام المتبقية للوصول لمرحلة معينة حسب تاريخ إنشاء التذكرة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__deadline +msgid "Deadline" +msgstr "الموعد النهائي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_defaults +msgid "Default Values" +msgstr "القيم الافتراضية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Delete" +msgstr "حذف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__description +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__description +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +msgid "Description" +msgstr "الوصف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Description for customer portal" +msgstr "وصف بوابة العميل" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Description of the policy..." +msgstr "وصف السياسة..." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Description of the ticket..." +msgstr "وصف التذكرة..." + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_digest_digest +msgid "Digest" +msgstr "الملخص" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Discard" +msgstr "تجاهل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_digest_digest__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_partner__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__portal_show_rating +msgid "Display Rating on Customer Portal" +msgstr "عرض التقييم على بوابة العميل" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/digest.py:0 +#, python-format +msgid "Do not have access, skip this data for user's digest email" +msgstr "لا تملك صلاحيات الوصول. تخطِ هذه البيانات لبريد الملخص للمستخدم." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Documentation" +msgstr "التوثيق" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Documents" +msgstr "المستندات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__domain_user_ids +msgid "Domain User" +msgstr "" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.stage,name:odex25_helpdesk.stage_done +msgid "Done" +msgstr "المنتهية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Dropdown menu" +msgstr "القائمة المنسدلة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Edit" +msgstr "تحرير" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__template_id +msgid "Email Template" +msgstr "قالب البريد الإلكتروني" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_alias +msgid "Email alias" +msgstr "لقب البريد الإلكتروني" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__email_cc +msgid "Email cc" +msgstr "البريد الإلكتروني cc" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__email +msgid "Email on Customer" +msgstr "البريد الإلكتروني" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "Emails sent to" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "" +"Emails sent to a Helpdesk Team alias generate tickets in your pipeline." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_website_helpdesk_slides +msgid "Enable eLearning" +msgstr "تفعيل eLearning" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "" +"Enter a subject or title for this ticket.
(e.g. Problem with " +"installation, Wrong order, Can't understand bill, etc.)" +msgstr "" +"أدخل موضوع أو عنوان لهذه التذكرة.
(مثلًا: مشكلة أثناء التثبيت، طلب " +"خطأ، فاتورة غير مفهومة، إلخ.)" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Enter the customer. Feel free to create it on the fly." +msgstr "أدخل اسم العميل. يمكنك إنشاء واحد جديد سريعًا." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__exceeded_days +msgid "Excedeed Working Days" +msgstr "أيام عمل متقطعة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__exclude_stage_ids +msgid "Exclude Stages" +msgstr "استبعاد المراحل" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__sla_status__failed +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_status__status__failed +msgid "Failed" +msgstr "فشل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_fail +msgid "Failed SLA Policy" +msgstr "حدث فشل باتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.filters,name:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_filter_stage_failed +msgid "Failed SLA Stage per Month" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "Failed Ticket" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +msgid "First Assignment Date" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__assign_date +msgid "First assignment date" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__fold +msgid "Folded in Kanban" +msgstr "مطوي في عرض كانبان" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +"Follow this team to automatically track the events associated to tickets of " +"this team." +msgstr "تابع هذا الفريق لتتمكن من تتبع الفعاليات المرتبطة بتذاكرهم تلقائيًا." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_follower_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_follower_ids +msgid "Followers" +msgstr "المتابعون" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_channel_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_channel_ids +msgid "Followers (Channels)" +msgstr "المتابعون (القنوات)" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_partner_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_partner_ids +msgid "Followers (Partners)" +msgstr "المتابعون (الشركاء)" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Future Activities" +msgstr "الأنشطة المستقبلية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Generate credit notes from tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Get in touch with your website visitors" +msgstr "تواصل مع زوار موقعك" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Grant coupons from tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__kanban_state__done +msgid "Green" +msgstr "أخضر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__legend_done +msgid "Green Kanban Label" +msgstr "بطاقة عنوان كانبان خضراء" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__kanban_state__normal +msgid "Grey" +msgstr "رمادي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__legend_normal +msgid "Grey Kanban Label" +msgstr "بطاقة عنوان كانبان رمادية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Group By" +msgstr "تجميع حسب" + +#. module: odex25_helpdesk +#: model:mail.activity.type,name:odex25_helpdesk.mail_act_odex25_helpdesk_handle +msgid "Handle Ticket" +msgstr "التعامل مع التذكرة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_progress_data +msgid "Happy" +msgstr "سعيد" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Happy Rating" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__has_external_mail_server +msgid "Has External Mail Server" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_reached_late +msgid "Has SLA reached late" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_website_helpdesk_forum +msgid "Help Center" +msgstr "مركز المساعدة" + +#. module: odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.menu_odex25_helpdesk_root +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.digest_digest_view_form +msgid "Helpdesk" +msgstr "مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main +msgid "Helpdesk Overview" +msgstr "نظرة عامة على مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_sla +msgid "Helpdesk SLA Policies" +msgstr "سياسات اتفاق مستوى الخدمة لمكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_stage +msgid "Helpdesk Stage" +msgstr "مرحلة مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_tag +msgid "Helpdesk Tags" +msgstr "وسوم مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_team +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__team_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_tree +msgid "Helpdesk Team" +msgstr "فريق مكتب المساعدة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Helpdesk Team..." +msgstr "فريق مكتب المساعدة..." + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_team_action +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_team_menu +msgid "Helpdesk Teams" +msgstr "فرق مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_ticket +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_analysis_action +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_graph_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_pivot_analysis +msgid "Helpdesk Ticket Analysis" +msgstr "تحليل تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_ticket_type +msgid "Helpdesk Ticket Type" +msgstr "نوع تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_graph_main +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_pivot_main +msgid "Helpdesk Tickets" +msgstr "تذاكر مكتب المساعدة" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "High Priority (" +msgstr "أولوية عالية (" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__priority__2 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__priority__2 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__priority__2 +msgid "High priority" +msgstr "أولوية عالية" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__time_hours +msgid "Hours" +msgstr "الساعات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__time_hours +msgid "Hours to reach given stage based on ticket creation date" +msgstr "عدد الساعات المتبقية للوصول لمرحلة معينة حسب تاريخ إنشاء التذكرة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "How to assign newly created tickets to the right person" +msgstr "كيفية إسناد الرسائل المُنشأة حديثًا للشخص الصحيح" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_digest_digest__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_partner__id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_parent_thread_id +msgid "" +"ID of the parent record holding the alias (example: project holding the task" +" creation alias)" +msgstr "" +"مُعرف السجل الأصلي الذي يحتوي اللقب (مثال: 'المشروع' يحمل لقب إنشاء المهمة)" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_exception_icon +msgid "Icon" +msgstr "الأيقونة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "الأيقونة للإشارة إلى استثناء النشاط" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_needaction +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_unread +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_needaction +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_unread +msgid "If checked, new messages require your attention." +msgstr "إذا كان محددًا، فهناك رسائل جديدة تحتاج لرؤيتها." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_has_error +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_has_sms_error +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_error +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "إذا كان محددًا، فقد حدث خطأ في تسليم بعض الرسائل." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__closed_by_partner +msgid "" +"If checked, this means the ticket was closed through the customer portal by " +"the customer." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_bounced_content +msgid "" +"If set, this content will automatically be sent out to unauthorized users " +"instead of the default message." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "In #{kanban_getcolorname(record.color.raw_value)}" +msgstr "في #{kanban_getcolorname(record.color.raw_value)}" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__use_website_helpdesk_livechat +msgid "" +"In Channel: You can create a new ticket by typing /Helpdesk [ticket title]. " +"You can search ticket by typing /odex25_helpdesk_search [Keyword1],[Keyword2],." +msgstr "" +"في القناة: يمكنك إنشاء تذكرة جديدة بكتابة /Helpdesk [عنوان التذكرة]. كما " +"يمكنك البحث عن تذكرة عبر كتابة /odex25_helpdesk_search [الكلمة الدلالية1]،[الكلمة " +"الدلالية2]." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#: model:odex25_helpdesk.stage,legend_normal:odex25_helpdesk.stage_cancelled +#: model:odex25_helpdesk.stage,legend_normal:odex25_helpdesk.stage_done +#: model:odex25_helpdesk.stage,legend_normal:odex25_helpdesk.stage_in_progress +#: model:odex25_helpdesk.stage,legend_normal:odex25_helpdesk.stage_new +#: model:odex25_helpdesk.stage,legend_normal:odex25_helpdesk.stage_solved +#: model:odex25_helpdesk.stage,name:odex25_helpdesk.stage_in_progress +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_1 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_10 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_11 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_12 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_13 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_14 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_15 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_16 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_17 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_18 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_19 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_2 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_3 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_4 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_5 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_6 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_7 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_8 +#: model:odex25_helpdesk.ticket,legend_normal:odex25_helpdesk.odex25_helpdesk_ticket_9 +#, python-format +msgid "In Progress" +msgstr "قيد التنفيذ" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Incoming emails create tickets" +msgstr "الرسائل الواردة تنشئ تذاكر" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +"Individuals to whom the tickets will be automatically assigned. Keep empty " +"for everyone to be part of the team." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_is_follower +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_is_follower +msgid "Is Follower" +msgstr "متابع" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.ticket.type,name:odex25_helpdesk.type_incident +msgid "Issue" +msgstr "المشكلة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_blocked +msgid "Kanban Blocked Explanation" +msgstr "تفسير حالة متوقف في واجهة كانبان" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_normal +msgid "Kanban Ongoing Explanation" +msgstr "تفسير حالة قيد التقدم في واجهة كانبان" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__kanban_state +msgid "Kanban State" +msgstr "حالة كانبان" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_done +msgid "Kanban Valid Explanation" +msgstr "تفسير حالة صالح في واجهة كانبان" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_digest_digest__kpi_odex25_helpdesk_tickets_closed_value +msgid "Kpi Helpdesk Tickets Closed Value" +msgstr "قيمة مؤشر أداء تذاكر الدعم المقفلة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_data +msgid "Last 3 months" +msgstr "آخر 3 أشهر" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_data +msgid "Last 30 days" +msgstr "آخر 30 يوم" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_data +msgid "Last 7 days" +msgstr "آخر 7 أيام" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_digest_digest____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_partner____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__date_last_stage_update +#, python-format +msgid "Last Stage Update" +msgstr "آخر تحديث للمرحلة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__write_uid +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__write_uid +msgid "Last Updated by" +msgstr "آخر تحديث بواسطة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__write_date +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__write_date +msgid "Last Updated on" +msgstr "آخر تحديث في" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Last message is from customer" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Last message is from support" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Late Activities" +msgstr "الأنشطة المتأخرة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_data +msgid "Latest Feedbacks" +msgstr "أحدث الملاحظات" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Let's create your first ticket." +msgstr "فلننشئ تذكرتك الأولى." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__email_cc +msgid "List of cc from incoming emails." +msgstr "قائمة cc من رسائل البريد الإلكتروني الواردة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_website_helpdesk_livechat +msgid "Live chat" +msgstr "دردشة حية" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__priority__1 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__priority__1 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__priority__1 +msgid "Low priority" +msgstr "أولوية منخفضة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_main_attachment_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_main_attachment_id +msgid "Main Attachment" +msgstr "المرفق الرئيسي" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_action_main +msgid "" +"Make sure tickets are handled in a timely manner by using SLA Policies.
" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_team__assign_method__manual +msgid "Manually" +msgstr "يدويًا" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__medium_id +msgid "Medium" +msgstr "وسط" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_has_error +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_error +msgid "Message Delivery error" +msgstr "خطأ في تسليم الرسائل" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.tickets_followup +msgid "Message and communication history" +msgstr "رسالة وتاريخ الاتصالات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_ids +msgid "Messages" +msgstr "الرسائل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__priority +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__priority +msgid "Minimum Priority" +msgstr "الأولوية الأدنى" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__stage_id +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla_status__sla_stage_id +msgid "Minimum stage a ticket needs to reach in order to satisfy this SLA." +msgstr "أدنى مرحلة يجب أن تصل لها التذكرة لتحقيق اتفاق مستوى الخدمة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__time_minutes +msgid "Minutes" +msgstr "الدقائق" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__time_minutes +msgid "Minutes to reach given stage based on ticket creation date" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Failed Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My High Priority Failed Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My High Priority Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My High Priority Tickets Analysis" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Open Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Open Tickets Analysis" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Performance" +msgstr "أدائي" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "My Ticket" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_menu_my +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#, python-format +msgid "My Tickets" +msgstr "تذاكري" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Urgent Failed Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Urgent Tickets" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "My Urgent Tickets Analysis" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#: model:odex25_helpdesk.stage,name:odex25_helpdesk.stage_new +#, python-format +msgid "New" +msgstr "جديد" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Newest" +msgstr "الأحدث" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "الموعد النهائي للنشاط التالي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_summary +msgid "Next Activity Summary" +msgstr "ملخص النشاط التالي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_type_id +msgid "Next Activity Type" +msgstr "نوع النشاط التالي" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_action_main +msgid "No SLA policies found. Let's create one!" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_7days_analysis +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_close_analysis +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_success +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_team_performance +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_analysis_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_team_analysis_action +msgid "No data yet !" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_7dayssuccess +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard +msgid "No data yet!" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_stage_action +msgid "No stages found. Let's create one!" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_tag_action +msgid "No tags found. Let's create one!" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_team_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main +msgid "No teams found" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create +msgid "No tickets found" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_main_my +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_team +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_unassigned +msgid "No tickets found. Let's create one!" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_type_action +msgid "No types found. Let's create one!" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "None" +msgstr "لا شيء" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_needaction_counter +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_needaction_counter +msgid "Number of Actions" +msgstr "عدد الإجراءات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__attachment_number +msgid "Number of Attachments" +msgstr "عدد المرفقات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__partner_ticket_count +msgid "Number of closed tickets from the same partner" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_has_error_counter +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_error_counter +msgid "Number of errors" +msgstr "عدد الاخطاء" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_needaction_counter +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "عدد الرسائل التي تتطلب إجراء" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_has_error_counter +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "عدد الرسائل الحادث بها خطأ في التسليم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__message_unread_counter +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__message_unread_counter +msgid "Number of unread messages" +msgstr "عدد الرسائل الجديدة" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__sla_status__ongoing +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_status__status__ongoing +msgid "Ongoing" +msgstr "جاري" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__ticket_type_id +msgid "" +"Only apply the SLA to a specific ticket type. If left empty it will apply to" +" all types." +msgstr "" +"تطبيق اتفاق مستوى الخدمة على نوع تذاكر معين فقط. إذا تُركت هذه الخانة فارغة " +"سيتم تطبيقه على كافة الأنواع." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__tag_ids +msgid "" +"Only apply the SLA to tickets with specific tags. If left empty it will " +"apply to all tags." +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#, python-format +msgid "Open" +msgstr "فتح" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +msgid "Open Tickets" +msgstr "فتح تذاكر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_open_hours +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__open_hours +msgid "Open Time (hours)" +msgstr "وقت الفتح (بالساعات)" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_force_thread_id +msgid "" +"Optional ID of a thread (record) to which all incoming messages will be " +"attached, even if they did not reply to it. If set, this will disable the " +"creation of new records completely." +msgstr "" +"معرف اختياري لمناقشة (سجل) سيتم إرفاق كافة الرسائل الواردة به، حتى لو لم " +"يردوا عليه. إذا تم تعيين قيمة له، سيعطل هذا إنشاء السجلات الجديدة بالكامل." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_page +msgid "Our Customer Satisfaction" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_my_home_menu_odex25_helpdesk +msgid "Our Ratings" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__legend_blocked +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_blocked +msgid "" +"Override the default value displayed for the blocked state for kanban " +"selection, when the task or issue is in that stage." +msgstr "" +"تجاوز القيمة الافتراضية المعروضة لحالة \"محظورة\" باختيارات كانبان، عندما " +"تكون المهمة أو المشكلة في تلك المرحلة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__legend_done +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_done +msgid "" +"Override the default value displayed for the done state for kanban " +"selection, when the task or issue is in that stage." +msgstr "" +"تجاوز القيمة الافتراضية المعروضة لحالة \"منتهية\" باختيارات كانبان، عندما " +"تكون المهمة أو المشكلة في تلك المرحلة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__legend_normal +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__legend_normal +msgid "" +"Override the default value displayed for the normal state for kanban " +"selection, when the task or issue is in that stage." +msgstr "" +"تجاوز القيمة الافتراضية المعروضة لحالة \"عادية\" باختيارات كانبان، عندما " +"تكون المهمة أو المشكلة في تلك المرحلة." + +#. module: odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_menu_team_dashboard +msgid "Overview" +msgstr "نظرة عامة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_user_id +msgid "Owner" +msgstr "المالك" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_parent_model_id +msgid "Parent Model" +msgstr "الكائن الأصل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_parent_thread_id +msgid "Parent Record Thread ID" +msgstr "معرف مناقشة السجل الأصلي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_parent_model_id +msgid "" +"Parent model holding the alias. The model holding the alias reference is not" +" necessarily the model given by alias_model_id (example: project " +"(parent_model) and task (model))" +msgstr "" +"الكائن الأب الذي يحتفظ بلقب البريد الإلكتروني. الكائن الذي يحتفظ بالإشارة " +"إلى لقب البريد الإلكتروني لا يشترط بالضرورة أن يكون هو الكائن المحدد في " +"الحقل alias_model_id (مثال: المشروع (الكائن الأب) والمهمة (الكائن))" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__rating_percentage_satisfaction +msgid "Percentage of happy ratings" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Performance" +msgstr "الأداء" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_team_performance +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_graph_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_pivot_analysis +msgid "Performance Analysis" +msgstr "تحليل الأداء" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js:0 +#, python-format +msgid "Please enter an integer value" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_contact +msgid "" +"Policy to post a message on the document using the mailgateway.\n" +"- everyone: everyone can post\n" +"- partners: only authenticated partners\n" +"- followers: only followers of the related document or members of following channels\n" +msgstr "" +"سياسة لنشر رسالة على المستند باستخدام بوابة البريد الإلكتروني.\n" +"- الجميع: يمكن للجميع النشر\n" +"- الشركاء: الشركاء المعتمدون فقط\n" +"- المتابعون: فقط متابعو الوثيقة ذات الصلة أو أعضاء القنوات التالية.\n" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__access_url +msgid "Portal Access URL" +msgstr "رابط الوصول لبوابة العملاء" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__priority +#: model:ir.model.fields,field_description:odex25_helpdesk.field_helpdesk_service__priority +msgid "Priority" +msgstr "الأولوية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Productivity & Visibility" +msgstr "الإنتاجية والرؤية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Publish this team's ratings on your website" +msgstr "نشر تقييمات هذا الفريق على موقعك" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.ticket.type,name:odex25_helpdesk.type_question +msgid "Question" +msgstr "السؤال" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Question and answer section on your website" +msgstr "قسم الأسئلة والأجوبة على موقعك" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_team__assign_method__randomly +msgid "Random" +msgstr "عشوائي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_ids +msgid "Rating" +msgstr "التقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_avg +msgid "Rating Average" +msgstr "متوسط التقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_last_feedback +msgid "Rating Last Feedback" +msgstr "آخر ملاحظات التقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_last_image +msgid "Rating Last Image" +msgstr "آخر صورة للتقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_last_value +msgid "Rating Last Value" +msgstr "آخر قيمة للتقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__rating_percentage_satisfaction +msgid "Rating Satisfaction" +msgstr "نسبة الرضا" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_count +msgid "Rating count" +msgstr "عدد التقييمات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__rating_ids +msgid "Ratings" +msgstr "تقييمات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_rating +msgid "Ratings on tickets" +msgstr "التقييم على التذاكر" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Reach In" +msgstr "الوصول في" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Reach Stage" +msgstr "الوصول للمرحلة" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__sla_status__reached +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_status__status__reached +msgid "Reached" +msgstr "وصل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__reached_datetime +msgid "Reached Date" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__target_type__stage +msgid "Reaching Stage" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#: model:odex25_helpdesk.stage,legend_done:odex25_helpdesk.stage_cancelled +#: model:odex25_helpdesk.stage,legend_done:odex25_helpdesk.stage_done +#: model:odex25_helpdesk.stage,legend_done:odex25_helpdesk.stage_in_progress +#: model:odex25_helpdesk.stage,legend_done:odex25_helpdesk.stage_new +#: model:odex25_helpdesk.stage,legend_done:odex25_helpdesk.stage_solved +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_1 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_10 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_11 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_12 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_13 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_14 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_15 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_16 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_17 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_18 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_19 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_2 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_3 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_4 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_5 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_6 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_7 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_8 +#: model:odex25_helpdesk.ticket,legend_done:odex25_helpdesk.odex25_helpdesk_ticket_9 +#, python-format +msgid "Ready" +msgstr "جاهز" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__rating_last_feedback +msgid "Reason of the rating" +msgstr "سبب التقييم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__alias_force_thread_id +msgid "Record Thread ID" +msgstr "معرف مناقشة السجل" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Record timesheets on your tickets" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__kanban_state__blocked +msgid "Red" +msgstr "أحمر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__legend_blocked +msgid "Red Kanban Label" +msgstr "بطاقة عنوان كانبان حمراء" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +msgid "Ref" +msgstr "رقم الإشارة" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Reference" +msgstr "رقم الإشارة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_credit_notes +msgid "Refunds" +msgstr "المرتجعات" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__use_odex25_helpdesk_sale_timesheet +msgid "Reinvoice the time spent on ticket through tasks." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Reinvoice time to your customer through tasks" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Repair broken products" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_product_repairs +msgid "Repairs" +msgstr "تعديلات" + +#. module: odex25_helpdesk +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_report_menu_main +msgid "Reporting" +msgstr "التقارير" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_user_id +msgid "Responsible User" +msgstr "المستخدم المسؤول" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Restore" +msgstr "استعادة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_product_returns +msgid "Returns" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_id +msgid "SLA" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_deadline +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_deadline +msgid "SLA Deadline" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#, python-format +msgid "SLA Failed" +msgstr "حدث فشل باتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +msgid "SLA Issues" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_sla_action +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_sla_action_main +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_sla +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_sla_menu_main +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_tree +msgid "SLA Policies" +msgstr "سياسات اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "SLA Policy" +msgstr "سياسة اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__description +msgid "SLA Policy Description" +msgstr "وصف سياسة اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__name +msgid "SLA Policy Name" +msgstr "اسم سياسة اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_reached_datetime +msgid "SLA Reached Date" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_stage_id +msgid "SLA Stage" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_status_ids +msgid "SLA Status" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_action +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_sla_report_analysis +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_report_menu_sla_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_graph +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_pivot +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "SLA Status Analysis" +msgstr "تحليل حالة اتفاقية مستوى الخدمة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "SLA Status Deadline" +msgstr "الموعد النهائي لحالة SLA" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_status_failed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "SLA Status Failed" +msgstr "فشل حالة SLA" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +msgid "SLA Success" +msgstr "نجاح SLA" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +msgid "SLA in Progress" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_ids +msgid "SLAs" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_has_sms_error +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_has_sms_error +msgid "SMS Delivery error" +msgstr "خطأ في تسليم الرسائل القصيرة" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Salesperson" +msgstr "مسئول المبيعات" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Sample" +msgstr "عينة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Save this page and refresh to activate the feature" +msgstr "احفظ هذه الصفحة وقم بتحديثها لتفعيل الخاصية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Save this page and refresh to activate the feature." +msgstr "احفظ هذه الصفحة وقم بتحديثها لتفعيل الخاصية." + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Save this ticket and the modifications you've made to it." +msgstr "حفظ هذه التذكرة والتعديلات التي قمت بها عليها." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search (in Content)" +msgstr "البحث (في المحتوى)" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_search +msgid "Search SLA Policies" +msgstr "البحث في سياسات اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search in All" +msgstr "البحث في الكل" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search in Customer" +msgstr "البحث في العملاء" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search in Messages" +msgstr "البحث في الرسائل" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search in Reference" +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Search in Stage" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__access_token +msgid "Security Token" +msgstr "رمز الحماية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +msgid "See Customer Satisfaction" +msgstr "رؤية نسبة رضا العميل" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Self-Service" +msgstr "خدمة ذاتية" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Sell & Track Hours" +msgstr "ساعات البيع والتتبع" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Send emails to" +msgstr "إرسال رسائل بريد إلكتروني إلى" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Send emails to:" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__sequence +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__sequence +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__sequence +msgid "Sequence" +msgstr "المسلسل" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Set the calendar used to compute SLA target" +msgstr "إعداد التقويم المُستخدم لحساب اتفاق مستوى الخدمة المستهدف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Set up your Service Level Agreements to track performance" +msgstr "ضبط اتفاق مستوى الخدمة الخاص بك لمتابعة الأداء" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +msgid "Settings" +msgstr "الإعدادات" + +#. module: odex25_helpdesk +#: model:ir.actions.server,name:odex25_helpdesk.model_odex25_helpdesk_ticket_action_share +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_kanban +msgid "Share" +msgstr "مشاركة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Share presentation and videos, and organize into courses" +msgstr "مشاركة العروض والفيديوهات، وتنظيمهم في دورات" + +#. module: odex25_helpdesk +#: model:res.groups,name:odex25_helpdesk.group_use_sla +msgid "Show SLA Policies" +msgstr "عرض سياسات اتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Show all records which has next action date is before today" +msgstr "عرض كافة السجلات المُعين لها تاريخ إجراء تالي يسبق تاريخ اليوم الجاري" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__sla_id +msgid "Sla" +msgstr "" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.stage,name:odex25_helpdesk.stage_solved +msgid "Solved" +msgstr "تم حلّها" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__source_id +msgid "Source" +msgstr "المصدر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__team_ids +msgid "" +"Specific team that uses this stage. Other teams will not be able to see or " +"use this stage." +msgstr "" +"الفريق المحدد الذي يستخدم هذه المرحلة. لن تتمكن الفرق الأخرى من رؤية أو " +"استخدام هذه المرحلة." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__stage_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_tree +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +#, python-format +msgid "Stage" +msgstr "المرحلة" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,description:odex25_helpdesk.mt_ticket_stage +#: model:mail.message.subtype,name:odex25_helpdesk.mt_ticket_stage +msgid "Stage Changed" +msgstr "تم تغيير المرحلة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "Stage Description and Tooltips" +msgstr "وصف وتلميحات المرحلة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__name +msgid "Stage Name" +msgstr "اسم المرحلة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "Stage Search" +msgstr "البحث في المراحل" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_stage_action +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__stage_ids +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_stage_menu +msgid "Stages" +msgstr "المراحل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__stage_ids +msgid "" +"Stages the team will use. This team's tickets will only be able to be in " +"these stages." +msgstr "" +"المراحل التي سيستخدمها الفريق. سيُسمح بوضع تذاكر هذا الفريق في هذه المراحل " +"فقط." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__sla_status +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__status +msgid "Status" +msgstr "الحالة" + +#. module: odex25_helpdesk +#: model:ir.filters,name:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_filter_status_per_deadline +msgid "Status Per Deadline" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"الحالة على أساس الأنشطة\n" +"المتأخرة: تاريخ الاستحقاق مر\n" +"اليوم: تاريخ النشاط هو اليوم\n" +"المخطط: الأنشطة المستقبلية." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__name +#, python-format +msgid "Subject" +msgstr "العنوان" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Subject..." +msgstr "الموضوع..." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "Submit tickets with an online form" +msgstr "تقديم التذاكر بنموذج أونلاين" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Success Rate" +msgstr "نسبة النجاح" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_7dayssuccess +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_success +msgid "Success Rate Analysis" +msgstr "تحليل نسبة النجاح" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_success +msgid "Success SLA Policy" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tag_view_tree +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_type_view_tree +msgid "Tag" +msgstr "الوسم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_tag__name +msgid "Tag Name" +msgstr "اسم الوسم" + +#. module: odex25_helpdesk +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_odex25_helpdesk_tag_name_uniq +msgid "Tag name already exists !" +msgstr "اسم الوسم موجود بالفعل!" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__tag_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__tag_ids +msgid "Tags" +msgstr "الوسوم" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_tag_action +msgid "Tags are perfect to organize your tickets." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Target" +msgstr "الهدف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__odex25_helpdesk_target_rating +msgid "Target Customer Rating" +msgstr "التقييم المستهدف من العميل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__stage_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__sla_stage_id +msgid "Target Stage" +msgstr "المرحلة المستهدفة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__odex25_helpdesk_target_success +msgid "Target Success Rate" +msgstr "معدل تحقيق الهدف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__odex25_helpdesk_target_closed +msgid "Target Tickets to Close" +msgstr "التذاكر المطلوب إقفالها" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__target_type +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__target_type +msgid "Target Type" +msgstr "نوع الهدف" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "Task in progress. Click to block or set as done." +msgstr "المهمة قيد التنفيذ. اضغط لحظرها أو تعيينها كمكتملة." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "Task is blocked. Click to unblock or set as done." +msgstr "المهمة محظورة. اضغط لإلغاء حظرها أو تعيينها كمكتملة." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__team_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__team_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_stage__team_ids +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Team" +msgstr "فريق" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__member_ids +msgid "Team Members" +msgstr "أعضاء الفريق" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__visibility_member_ids +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +"Team Members to whom this team will be visible. Keep empty for everyone to " +"see this team." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_search +msgid "Team Search" +msgstr "فريق البحث" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__visibility_member_ids +msgid "Team Visibility" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_team_action +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main +msgid "" +"Teams regroup tickets for people sharing the same expertise or from the same" +" area." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.email_template_action_odex25_helpdesk +msgid "Templates" +msgstr "قوالب" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__exclude_stage_ids +msgid "" +"The amount of time the ticket spends in this stage will not be taken into " +"account when evaluating the status of the SLA Policy." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__sla_deadline +msgid "The closest deadline of all SLA applied on this ticket" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_model_id +msgid "" +"The model (Odoo Document Kind) to which this alias corresponds. Any incoming" +" email that does not reply to an existing record will cause the creation of " +"a new record of this model (e.g. a Project Task)" +msgstr "" +"الكائن (مستندات أودو) الذي يقترن معه هذا اللقب. أي رسالة واردة لا ترد على " +"سجل موجود ستقوم بإنشاء سجل جديد من نفس نوع هذا الكائن (مثلًا: مهمة مشروع)" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_name +msgid "" +"The name of the email alias, e.g. 'jobs' if you want to catch emails for " +"" +msgstr "" +"اسم لقب البريد الإلكتروني، مثلًا: 'وظائف' إذا كنت جمع الرسائل المرسلة " +"لـ" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__alias_user_id +msgid "" +"The owner of records created upon receiving emails on this alias. If this " +"field is not set the system will attempt to find the right owner based on " +"the sender (From) address, or will use the Administrator account if no " +"system user is found for that address." +msgstr "" +"مالك السجلات المنشأة عند استلام رسائل بريد إلكتروني على هذا اللقب. إذا لم " +"يتم تعيين قيمة لهذا الحقل، سيحاول النظام معرفة المالك الصحيح حسب عنوان " +"البريد الإلكتروني للمرسل، أو سيستخدم حساب المشرف إذا لم يجد حسابًا مرتبطًا " +"بعنوان البريد الإلكتروني." + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "The team does not allow ticket closing through portal" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +msgid "There are currently no Ticket for your account." +msgstr "لا توجد حاليًا تذاكر لحسابك." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.team_rating_page +msgid "There are no ratings yet." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__assign_hours +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__close_hours +msgid "This duration is based on the working calendar of the team" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__open_hours +msgid "This duration is not based on the working calendar of the team" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__campaign_id +msgid "" +"This is a name that helps you keep track of your different campaign efforts," +" e.g. Fall_Drive, Christmas_Special" +msgstr "" +"يساعدك هذا الاسم علي تتبع جهود حملتك المختلفة، مثلًا: fall_drive " +"،christmas_special" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__medium_id +msgid "This is the method of delivery, e.g. Postcard, Email, or Banner Ad" +msgstr "" +"هذه هي طريقة التسليم، مثلًا: بطاقة بريدية، أو البريد الإلكتروني أو لافتة " +"إعلانية" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__source_id +msgid "" +"This is the source of the link, e.g. Search Engine, another domain, or name " +"of email list" +msgstr "" +"هذا هو مصدر الرابط، مثلًا: محرك بحث، أو نطاق آخر، أو اسم في قائمة البريد " +"الإلكتروني" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__use_odex25_helpdesk_timesheet +msgid "This required to have project module installed." +msgstr "يتطلب هذا تثبيت موديول المشروع." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__fold +msgid "" +"This stage is folded in the kanban view when there are no records in that " +"stage to display." +msgstr "" +"يتم طي هذه المرحلة عند استخدام طريقة كانبان للعرض عندما لا توجد سجلات يمكن " +"عرضها في هذه المرحلة." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "This step is done. Click to block or set in progress." +msgstr "هذه الخطوة مكتملة. اضغط لحظرها أو تعيينها قيد التنفيذ." + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Three stars, maximum score" +msgstr "ثلاثة نجوم، أقصى درجة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_status__ticket_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_activity +msgid "Ticket" +msgstr "التذكرة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_dashboard +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_team_analysis_action +msgid "Ticket Analysis" +msgstr "تحليل التذكرة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_closed +msgid "Ticket Closed" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__create_date +msgid "Ticket Create Date" +msgstr "" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,name:odex25_helpdesk.mt_team_ticket_new +#: model:mail.message.subtype,name:odex25_helpdesk.mt_ticket_new +msgid "Ticket Created" +msgstr "تم إنشاء التذكرة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "Ticket Creation Date" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_deadline +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_report_analysis_view_search +msgid "Ticket Deadline" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_failed +msgid "Ticket Failed" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Ticket ID" +msgstr "مُعرف التذكرة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_tree +msgid "Ticket Name" +msgstr "" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,name:odex25_helpdesk.mt_team_ticket_rated +#: model:mail.message.subtype,name:odex25_helpdesk.mt_ticket_rated +msgid "Ticket Rated" +msgstr "تم تقييم التذكرة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_odex25_helpdesk_sla_status +msgid "Ticket SLA Status" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_stage_id +msgid "Ticket Stage" +msgstr "" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,name:odex25_helpdesk.mt_team_ticket_stage +msgid "Ticket Stage Changed" +msgstr "تم تغيير مرحلة التذكرة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_tag_action +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_tag_menu +msgid "Ticket Tags" +msgstr "وسوم التذكرة" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_type_action +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__ticket_type_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_type_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__ticket_type_id +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_type_menu +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Ticket Type" +msgstr "نوع التذكرة" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#, python-format +msgid "Ticket closed by the customer" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__allow_portal_ticket_closing +msgid "Ticket closing" +msgstr "إغلاق التذكرة" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,description:odex25_helpdesk.mt_ticket_new +msgid "Ticket created" +msgstr "تم إنشاء التذكرة" + +#. module: odex25_helpdesk +#: model:mail.message.subtype,description:odex25_helpdesk.mt_ticket_rated +msgid "Ticket rated" +msgstr "تم تقييم التذكرة" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: model:ir.actions.act_window,name:odex25_helpdesk.action_upcoming_sla_fail_all_tickets +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_main_my +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_sla +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_team +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_ticket_action_unassigned +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__ticket_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_partner__ticket_count +#: model:ir.model.fields,field_description:odex25_helpdesk.field_res_users__ticket_count +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_menu_main +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_ticket_report_menu +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_cohort +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_tree +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_odex25_helpdesk_ticket +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_my_home_odex25_helpdesk_ticket +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.portal_my_home_menu_odex25_helpdesk +#, python-format +msgid "Tickets" +msgstr "تذاكر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_digest_digest__kpi_odex25_helpdesk_tickets_closed +msgid "Tickets Closed" +msgstr "تم قفل التذكرة" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Tickets Closed Avg 7 Days" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Tickets Closed Today" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Tickets Search" +msgstr "البحث في التذاكر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_stage__is_close +msgid "" +"Tickets in this stage are considered as done. This is used notably when " +"computing SLAs and KPIs on tickets." +msgstr "" +"تُعتبر التذاكر في هذه المرحلة منتهية. يُستخدم هذا بصورة ملحوظة عند احتساب " +"سياسات اتفاق مستوى الخدمة وقيم المؤشر الرئيسي للتذاكر." + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla__priority +msgid "Tickets under this priority will not be taken into account." +msgstr "لن توضع التذاكر ذات هذه الأولوية في الحساب." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_odex25_helpdesk_sale_timesheet +msgid "Time Reinvoicing" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_close_hours +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__close_hours +msgid "Time to close (hours)" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla_report_analysis__ticket_assignation_hours +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__assign_hours +msgid "Time to first assignment (hours)" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_odex25_helpdesk_timesheet +msgid "Timesheet on Ticket" +msgstr "سجل النشاط على التذكرة" + +#. module: odex25_helpdesk +#: model:digest.tip,name:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "Tip: Create tickets from incoming emails" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_main_my +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_team +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_unassigned +msgid "" +"To get things done, use activities and status on tickets.
\n" +" Chat in real time or by email to collaborate efficiently." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_my_ticket_action_no_create +msgid "" +"To get things done, use activities and status on tickets.
\n" +" Chat in real time or by email to collaborate efficiently." +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Today" +msgstr "اليوم" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Today Activities" +msgstr "نشاطات اليوم" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Today Happy Rating" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Today's Success Rate" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_twitter +msgid "Twitter" +msgstr "تويتر" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Two stars, with a maximum of three" +msgstr "نجمتان، بحد أقصى ثلاثة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket_type__name +msgid "Type" +msgstr "النوع" + +#. module: odex25_helpdesk +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_odex25_helpdesk_ticket_type_name_uniq +msgid "Type name already exists !" +msgstr "اسم النوع موجود بالفعل!" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "نوع النشاط الاستثنائي المسجل." + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_type_action +msgid "Types are perfect to categorize your tickets." +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__portal_rating_url +msgid "URL to Submit an Issue" +msgstr "رابط الإبلاغ عن مشكلة" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/controllers/portal.py:0 +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +#, python-format +msgid "Unassigned" +msgstr "غير مكلف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__unassigned_tickets +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_kanban +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_search_analysis_closed +msgid "Unassigned Tickets" +msgstr "التذاكر غير المسندة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_unread +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_unread +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Unread Messages" +msgstr "الرسائل الجديدة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__message_unread_counter +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__message_unread_counter +msgid "Unread Messages Counter" +msgstr "عدد الرسائل الجديدة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_tickets_view_search +msgid "Upcoming SLA Fail" +msgstr "فشل مترقب باتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__upcoming_sla_fail_tickets +msgid "Upcoming SLA Fail Tickets" +msgstr "تذاكر الفشل المترقب باتفاق مستوى الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla__priority__3 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_sla_report_analysis__priority__3 +#: model:ir.model.fields.selection,name:odex25_helpdesk.selection__odex25_helpdesk_ticket__priority__3 +msgid "Urgent" +msgstr "عاجل" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml:0 +#, python-format +msgid "Urgent (" +msgstr "عاجل (" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__use_coupons +msgid "Use Coupons" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__use_credit_notes +msgid "Use Credit Notes" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__use_product_repairs +msgid "Use Repairs" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__use_product_returns +msgid "Use Returns" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "Use the breadcrumbs to go back to the Kanban view." +msgstr "استخدم فتات الخبز للعودة لواجهة عرض كانبان." + +#. module: odex25_helpdesk +#: model:res.groups,name:odex25_helpdesk.group_odex25_helpdesk_user +msgid "User" +msgstr "المستخدم" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_res_users +msgid "Users" +msgstr "المستخدمون" + +#. module: odex25_helpdesk +#: model:odex25_helpdesk.team,name:odex25_helpdesk.odex25_helpdesk_team3 +msgid "VIP Support" +msgstr "" + +#. module: odex25_helpdesk +#. openerp-web +#: code:addons/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js:0 +#, python-format +msgid "" +"Want to boost your customer satisfaction?
Click Helpdesk to " +"start." +msgstr "" +"أتريد رفع نسبة رضا العميل؟
اضغط على مكتب المساعدة لتبدأ." + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__use_website_helpdesk_form +msgid "Website Form" +msgstr "نموذج الموقع" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__website_message_ids +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__website_message_ids +msgid "Website Messages" +msgstr "رسائل الموقع" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_team__website_message_ids +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_ticket__website_message_ids +msgid "Website communication history" +msgstr "سجل تواصل الموقع" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "" +"With random assignation, every user gets the same number of tickets. With " +"balanced assignation, tickets are assigned to the user with the least amount" +" of open tickets." +msgstr "" +"من خلال الإسناد العشوائي، ينال كل مستخدم نفس عدد التذاكر. من خلال الإسناد " +"المتزن، يتم إسناد التذاكر للمستخدم صاحب أقل عدد من التذاكر المفتوحة." + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "Within" +msgstr "" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__resource_calendar_id +msgid "Working Hours" +msgstr "ساعات العمل" + +#. module: odex25_helpdesk +#: model:ir.model.fields,help:odex25_helpdesk.field_odex25_helpdesk_sla_status__exceeded_days +msgid "" +"Working days exceeded for reached SLAs compared with deadline. Positive " +"number means the SLA was eached after the deadline." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_stage_view_form +msgid "" +"You can also add a description to help your coworkers understand the meaning" +" and purpose of the stage." +msgstr "يمكنك أيضًا إضافة وصف لمساعدة زملائك على فهم معنى وغرض المرحلة." + +#. module: odex25_helpdesk +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_res_users_target_closed_not_zero +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_res_users_target_rating_not_zero +#: model:ir.model.constraint,message:odex25_helpdesk.constraint_res_users_target_success_not_zero +msgid "You cannot have negative targets" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.action_upcoming_sla_fail_all_tickets +#: model_terms:ir.actions.act_window,help:odex25_helpdesk.odex25_helpdesk_ticket_action_sla +msgid "You completed all your tickets on time." +msgstr "" + +#. module: odex25_helpdesk +#: code:addons/odex25_helpdesk/models/odex25_helpdesk.py:0 +#, python-format +msgid "You must have team members assigned to change the assignment method." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "days
" +msgstr "أيام
" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "e.g. Close urgent tickets within 36 hours" +msgstr "مثلًا: إغلاق التذاكر العاجلة خلال 36 ساعة" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "eLearning" +msgstr "eLearning" + +#. module: odex25_helpdesk +#: model_terms:digest.tip,tip_description:odex25_helpdesk.digest_tip_odex25_helpdesk_0 +msgid "generate tickets in your pipeline." +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "hours
" +msgstr "ساعات
" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_sla_view_form +msgid "minutes
" +msgstr "" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "team search" +msgstr "فريق البحث" + +#. module: odex25_helpdesk +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_team_view_form +msgid "to create tickets" +msgstr "لإنشاء تذاكر" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_helpdesk_service__category_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__category_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__category_id +msgid "Category" +msgstr "التصنيف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_helpdesk_service__name +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_sla__service_id +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__service_id +msgid "Service" +msgstr "الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model,name:odex25_helpdesk.model_hr_employee +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__employee_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Employee" +msgstr "الموظف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__assistance_user_id +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk.odex25_helpdesk_ticket_view_form +msgid "Assistance" +msgstr "المساعد" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__work_email +msgid "Work Email" +msgstr "إيميل الموظف" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__work_location +msgid "Work Location" +msgstr "مكان العمل" + + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__department_id +msgid "Administrative Structure" +msgstr "الهيكل الإداري" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__deb_name +msgid "Department Name" +msgstr "القسم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__project_no +msgid "Project Number" +msgstr "رقم المشروع" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__on_behalf +msgid "On behalf of" +msgstr "نيابة عن" + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_category_action +#: model:ir.model.fields,field_description:odex25_helpdesk.field_service_category__name +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_menu_service_category +msgid "Service Category" +msgstr "تصنيف الخدمة" + + +#. module: odex25_helpdesk +#: model:ir.actions.act_window,name:odex25_helpdesk.helpdesk_service_action +#: model:ir.actions.act_window,name:odex25_helpdesk.odex25_helpdesk_service_action +#: model:ir.ui.menu,name:odex25_helpdesk.odex25_helpdesk_menu_service +msgid "Helpdesk Service" +msgstr "الخدمة" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__is_internal_team +msgid "Internal Team" +msgstr "فريق داخلي" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_team__is_vip_team +msgid "VIP Team" +msgstr "VIP فريق" + +#. module: odex25_helpdesk +#: model:res.groups,name:odex25_helpdesk.group_odex25_helpdesk_assignment +msgid "Assignment User" +msgstr "مستخدم التعيين" + +#. module: odex25_helpdesk +#: model:res.groups,name:odex25_helpdesk.group_odex25_helpdesk_on_behalf +msgid "On Behalf User" +msgstr "نيابة عن المستخدم" + +#. module: odex25_helpdesk +#: model:ir.model.fields,field_description:odex25_helpdesk.field_odex25_helpdesk_ticket__phone_no +msgid "Phone Number" +msgstr "رقم الهاتف" + diff --git a/odex25_helpdesk/odex25_helpdesk/migrations/10.0.1.2/pre-nonulls.py b/odex25_helpdesk/odex25_helpdesk/migrations/10.0.1.2/pre-nonulls.py new file mode 100644 index 000000000..d68aa3c73 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/migrations/10.0.1.2/pre-nonulls.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +def migrate(cr, version): + cr.execute(""" + UPDATE odex25_helpdesk_sla + SET time_days = COALESCE(time_days, 0), + time_hours = COALESCE(time_hours, 0), + time_minutes = COALESCE(time_minutes, 0) + """) diff --git a/odex25_helpdesk/odex25_helpdesk/models/__init__.py b/odex25_helpdesk/odex25_helpdesk/models/__init__.py new file mode 100644 index 000000000..f7b7cf4e3 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import digest +from . import odex25_helpdesk +from . import odex25_helpdesk_ticket +from . import res_users +from . import res_partner +from . import res_setting diff --git a/odex25_helpdesk/odex25_helpdesk/models/digest.py b/odex25_helpdesk/odex25_helpdesk/models/digest.py new file mode 100644 index 000000000..2a08cfdaf --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/digest.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models, _ +from odoo.exceptions import AccessError + + +class Digest(models.Model): + _inherit = 'digest.digest' + + kpi_odex25_helpdesk_tickets_closed = fields.Boolean('Tickets Closed') + kpi_odex25_helpdesk_tickets_closed_value = fields.Integer(compute='_compute_kpi_odex25_helpdesk_tickets_closed_value') + + def _compute_kpi_odex25_helpdesk_tickets_closed_value(self): + if not self.env.user.has_group('odex25_helpdesk.group_odex25_helpdesk_user'): + raise AccessError(_("Do not have access, skip this data for user's digest email")) + for record in self: + start, end, company = record._get_kpi_compute_parameters() + closed_ticket = self.env['odex25_helpdesk.ticket'].search_count([ + ('close_date', '>=', start), + ('close_date', '<', end), + ('company_id', '=', company.id) + ]) + record.kpi_odex25_helpdesk_tickets_closed_value = closed_ticket + + def _compute_kpis_actions(self, company, user): + res = super(Digest, self)._compute_kpis_actions(company, user) + res['kpi_odex25_helpdesk_tickets_closed'] = 'odex25_helpdesk.odex25_helpdesk_team_dashboard_action_main&menu_id=%s' % self.env.ref('odex25_helpdesk.menu_odex25_helpdesk_root').id + return res diff --git a/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk.py b/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk.py new file mode 100644 index 000000000..f1015aa26 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- + +import ast +import datetime + +from dateutil import relativedelta +from odoo import api, fields, models, _ +from odoo.addons.odex25_helpdesk.models.odex25_helpdesk_ticket import TICKET_PRIORITY +from odoo.addons.http_routing.models.ir_http import slug +from odoo.exceptions import UserError, ValidationError +from odoo.osv import expression + + +class odex25_helpdeskTeam(models.Model): + _name = "odex25_helpdesk.team" + _inherit = ['mail.alias.mixin', 'mail.thread', 'rating.parent.mixin'] + _description = "Helpdesk Team" + _order = 'sequence,name' + _rating_satisfaction_days = False # takes all existing ratings + + _sql_constraints = [('not_portal_show_rating_if_not_use_rating', + 'check (portal_show_rating = FALSE OR use_rating = TRUE)', + 'Cannot show ratings in portal if not using them'), ] + + def _default_stage_ids(self): + default_stage = self.env['odex25_helpdesk.stage'].search([('name', '=', _('New'))], limit=1) + if default_stage: + return [(4, default_stage.id)] + return [(0, 0, {'name': _('New'), 'sequence': 0, 'template_id': self.env.ref('odex25_helpdesk.new_ticket_request_email_template', raise_if_not_found=False) or None})] + + def _default_domain_member_ids(self): + return [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)] + + name = fields.Char('Helpdesk Team', required=True, translate=True) + description = fields.Text('About Team', translate=True) + active = fields.Boolean(default=True) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) + sequence = fields.Integer("Sequence", default=10) + color = fields.Integer('Color Index', default=1) + stage_ids = fields.Many2many( + 'odex25_helpdesk.stage', relation='team_stage_rel', string='Stages', + default=_default_stage_ids, + help="Stages the team will use. This team's tickets will only be able to be in these stages.") + assign_method = fields.Selection([ + ('manual', 'Manually'), + ('randomly', 'Random'), + ('balanced', 'Balanced')], string='Assignment Method', default='manual', + compute='_compute_assign_method', store=True, readonly=False, required=True, + help='Automatic assignment method for new tickets:\n' + '\tManually: manual\n' + '\tRandomly: randomly but everyone gets the same amount\n' + '\tBalanced: to the person with the least amount of open tickets') + member_ids = fields.Many2many('res.users', string='Team Members', domain=lambda self: self._default_domain_member_ids()) + visibility_member_ids = fields.Many2many('res.users', 'odex25_helpdesk_visibility_team', string='Team Visibility', domain=lambda self: self._default_domain_member_ids(), + help="Team Members to whom this team will be visible. Keep empty for everyone to see this team.") + ticket_ids = fields.One2many('odex25_helpdesk.ticket', 'team_id', string='Tickets') + + use_alias = fields.Boolean('Email alias', default=True) + has_external_mail_server = fields.Boolean(compute='_compute_has_external_mail_server') + allow_portal_ticket_closing = fields.Boolean('Ticket closing', help="Allow customers to close their tickets") + use_website_helpdesk_form = fields.Boolean('Website Form') + use_website_helpdesk_livechat = fields.Boolean('Live chat', + help="In Channel: You can create a new ticket by typing /Helpdesk [ticket title]. You can search ticket by typing /odex25_helpdesk_search [Keyword1],[Keyword2],.") + use_website_helpdesk_forum = fields.Boolean('Help Center') + use_website_helpdesk_slides = fields.Boolean('Enable eLearning') + use_odex25_helpdesk_timesheet = fields.Boolean('Timesheet on Ticket', help="This required to have project module installed.") + use_odex25_helpdesk_sale_timesheet = fields.Boolean( + 'Time Reinvoicing', compute='_compute_use_odex25_helpdesk_sale_timesheet', store=True, + readonly=False, help="Reinvoice the time spent on ticket through tasks.") + use_credit_notes = fields.Boolean('Refunds') + use_coupons = fields.Boolean('Coupons') + use_product_returns = fields.Boolean('Returns') + use_product_repairs = fields.Boolean('Repairs') + use_twitter = fields.Boolean('Twitter') + use_api = fields.Boolean('API') + use_rating = fields.Boolean('Ratings on tickets') + portal_show_rating = fields.Boolean( + 'Display Rating on Customer Portal', compute='_compute_portal_show_rating', store=True, + readonly=False) + portal_rating_url = fields.Char('URL to Submit an Issue', readonly=True, compute='_compute_portal_rating_url') + use_sla = fields.Boolean('SLA Policies') + upcoming_sla_fail_tickets = fields.Integer(string='Upcoming SLA Fail Tickets', compute='_compute_upcoming_sla_fail_tickets') + unassigned_tickets = fields.Integer(string='Unassigned Tickets', compute='_compute_unassigned_tickets') + resource_calendar_id = fields.Many2one('resource.calendar', 'Working Hours', + default=lambda self: self.env.company.resource_calendar_id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + + is_internal_team = fields.Boolean('Internal Team', default=False) + is_vip_team = fields.Boolean('VIP Team', default=False) + + @api.depends('name', 'portal_show_rating') + def _compute_portal_rating_url(self): + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + for team in self: + if team.name and team.portal_show_rating and team.id: + team.portal_rating_url = '%s/odex25_helpdesk/rating/%s' % (base_url, slug(team)) + else: + team.portal_rating_url = False + + def _compute_has_external_mail_server(self): + self.has_external_mail_server = self.env['ir.config_parameter'].sudo().get_param('base_setup.default_external_email_server') + + def _compute_upcoming_sla_fail_tickets(self): + ticket_data = self.env['odex25_helpdesk.ticket'].read_group([ + ('team_id', 'in', self.ids), + ('sla_deadline', '!=', False), + ('sla_deadline', '<=', fields.Datetime.to_string((datetime.date.today() + relativedelta.relativedelta(days=1)))), + ], ['team_id'], ['team_id']) + mapped_data = dict((data['team_id'][0], data['team_id_count']) for data in ticket_data) + for team in self: + team.upcoming_sla_fail_tickets = mapped_data.get(team.id, 0) + + def _compute_unassigned_tickets(self): + ticket_data = self.env['odex25_helpdesk.ticket'].read_group([('user_id', '=', False), ('team_id', 'in', self.ids), ('stage_id.is_close', '!=', True)], ['team_id'], ['team_id']) + mapped_data = dict((data['team_id'][0], data['team_id_count']) for data in ticket_data) + for team in self: + team.unassigned_tickets = mapped_data.get(team.id, 0) + + @api.depends('use_rating') + def _compute_portal_show_rating(self): + without_rating = self.filtered(lambda t: not t.use_rating) + without_rating.update({'portal_show_rating': False}) + + @api.depends('member_ids', 'visibility_member_ids') + def _compute_assign_method(self): + with_manual = self.filtered(lambda t: not t.member_ids and not t.visibility_member_ids) + with_manual.update({'assign_method': 'manual'}) + + @api.onchange('use_alias', 'name') + def _onchange_use_alias(self): + if not self.use_alias: + self.alias_name = False + + @api.depends('use_odex25_helpdesk_timesheet') + def _compute_use_odex25_helpdesk_sale_timesheet(self): + without_timesheet = self.filtered(lambda t: not t.use_odex25_helpdesk_timesheet) + without_timesheet.update({'use_odex25_helpdesk_sale_timesheet': False}) + + @api.constrains('assign_method', 'member_ids', 'visibility_member_ids') + def _check_member_assignation(self): + if not self.member_ids and not self.visibility_member_ids and self.assign_method != 'manual': + raise ValidationError(_("You must have team members assigned to change the assignment method.")) + + # ------------------------------------------------------------ + # ORM overrides + # ------------------------------------------------------------ + + @api.model + def create(self, vals): + team = super(odex25_helpdeskTeam, self.with_context(mail_create_nosubscribe=True)).create(vals) + team.sudo()._check_sla_group() + team.sudo()._check_modules_to_install() + # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install. + return team + + def write(self, vals): + result = super(odex25_helpdeskTeam, self).write(vals) + if 'active' in vals: + self.with_context(active_test=False).mapped('ticket_ids').write({'active': vals['active']}) + self.sudo()._check_sla_group() + self.sudo()._check_modules_to_install() + # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install. + return result + + def unlink(self): + stages = self.mapped('stage_ids').filtered(lambda stage: stage.team_ids <= self) # remove stages that only belong to team in self + stages.unlink() + return super(odex25_helpdeskTeam, self).unlink() + + def _check_sla_group(self): + for team in self: + if team.use_sla and not self.user_has_groups('odex25_helpdesk.group_use_sla'): + self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').write({'implied_ids': [(4, self.env.ref('odex25_helpdesk.group_use_sla').id)]}) + if team.use_sla: + self.env['odex25_helpdesk.sla'].with_context(active_test=False).search([('team_id', '=', team.id), ('active', '=', False)]).write({'active': True}) + else: + self.env['odex25_helpdesk.sla'].search([('team_id', '=', team.id)]).write({'active': False}) + if not self.search_count([('use_sla', '=', True)]): + self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').write({'implied_ids': [(3, self.env.ref('odex25_helpdesk.group_use_sla').id)]}) + self.env.ref('odex25_helpdesk.group_use_sla').write({'users': [(5, 0, 0)]}) + + def _check_modules_to_install(self): + # mapping of field names to module names + FIELD_MODULE = { + 'use_website_helpdesk_form': 'odex25_website_helpdesk_form', + 'use_website_helpdesk_livechat': 'odex25_website_helpdesk_livechat', + 'use_website_helpdesk_forum': 'odex25_website_helpdesk_forum', + 'use_website_helpdesk_slides': 'odex25_website_helpdesk_slides', + 'use_odex25_helpdesk_timesheet': 'odex25_helpdesk_timesheet', + 'use_odex25_helpdesk_sale_timesheet': 'odex25_helpdesk_sale_timesheet', + 'use_credit_notes': 'odex25_helpdesk_account', + 'use_product_returns': 'odex25_helpdesk_stock', + 'use_product_repairs': 'odex25_helpdesk_repair', + 'use_coupons': 'odex25_helpdesk_sale_coupon', + } + + # determine the modules to be installed + expected = [ + mname + for fname, mname in FIELD_MODULE.items() + if any(team[fname] for team in self) + ] + modules = self.env['ir.module.module'] + if expected: + STATES = ('installed', 'to install', 'to upgrade') + modules = modules.search([('name', 'in', expected)]) + modules = modules.filtered(lambda module: module.state not in STATES) + + # other stuff + for team in self: + if team.use_rating: + for stage in team.stage_ids: + if stage.is_close and not stage.fold: + stage.template_id = self.env.ref('odex25_helpdesk.rating_ticket_request_email_template', raise_if_not_found= False) + + if modules: + modules.button_immediate_install() + + # just in case we want to do something if we install a module. (like a refresh ...) + return bool(modules) + + # ------------------------------------------------------------ + # Mail Alias Mixin + # ------------------------------------------------------------ + + def _alias_get_creation_values(self): + values = super(odex25_helpdeskTeam, self)._alias_get_creation_values() + values['alias_model_id'] = self.env['ir.model']._get('odex25_helpdesk.ticket').id + if self.id: + values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}") + defaults['team_id'] = self.id + return values + + # ------------------------------------------------------------ + # Business Methods + # ------------------------------------------------------------ + + @api.model + def retrieve_dashboard(self): + domain = [('user_id', '=', self.env.uid)] + group_fields = ['priority', 'create_date', 'stage_id', 'close_hours'] + list_fields = ['priority', 'create_date', 'stage_id', 'close_hours'] + #TODO: remove SLA calculations if user_uses_sla is false. + user_uses_sla = self.user_has_groups('odex25_helpdesk.group_use_sla') and\ + bool(self.env['odex25_helpdesk.team'].search([('use_sla', '=', True), '|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)])) + + if user_uses_sla: + group_fields.insert(1, 'sla_deadline:year') + group_fields.insert(2, 'sla_deadline:hour') + group_fields.insert(3, 'sla_reached_late') + list_fields.insert(1, 'sla_deadline') + list_fields.insert(2, 'sla_reached_late') + + odex25_helpdeskTicket = self.env['odex25_helpdesk.ticket'] + tickets = odex25_helpdeskTicket.search_read(expression.AND([domain, [('stage_id.is_close', '=', False)]]), ['sla_deadline', 'open_hours', 'sla_reached_late', 'priority']) + + result = { + 'odex25_helpdesk_target_closed': self.env.user.odex25_helpdesk_target_closed, + 'odex25_helpdesk_target_rating': self.env.user.odex25_helpdesk_target_rating, + 'odex25_helpdesk_target_success': self.env.user.odex25_helpdesk_target_success, + 'today': {'count': 0, 'rating': 0, 'success': 0}, + '7days': {'count': 0, 'rating': 0, 'success': 0}, + 'my_all': {'count': 0, 'hours': 0, 'failed': 0}, + 'my_high': {'count': 0, 'hours': 0, 'failed': 0}, + 'my_urgent': {'count': 0, 'hours': 0, 'failed': 0}, + 'show_demo': not bool(odex25_helpdeskTicket.search([], limit=1)), + 'rating_enable': False, + 'success_rate_enable': user_uses_sla + } + + def _is_sla_failed(data): + deadline = data.get('sla_deadline') + sla_deadline = fields.Datetime.now() > deadline if deadline else False + return sla_deadline or data.get('sla_reached_late') + + def add_to(ticket, key="my_all"): + result[key]['count'] += 1 + result[key]['hours'] += ticket['open_hours'] + if _is_sla_failed(ticket): + result[key]['failed'] += 1 + + for ticket in tickets: + add_to(ticket, 'my_all') + if ticket['priority'] == '2': + add_to(ticket, 'my_high') + if ticket['priority'] == '3': + add_to(ticket, 'my_urgent') + + dt = fields.Date.today() + tickets = odex25_helpdeskTicket.read_group(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)], list_fields, group_fields, lazy=False) + for ticket in tickets: + result['today']['count'] += ticket['__count'] + if not _is_sla_failed(ticket): + result['today']['success'] += ticket['__count'] + + dt = fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6))) + tickets = odex25_helpdeskTicket.read_group(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)], list_fields, group_fields, lazy=False) + for ticket in tickets: + result['7days']['count'] += ticket['__count'] + if not _is_sla_failed(ticket): + result['7days']['success'] += ticket['__count'] + + result['today']['success'] = (result['today']['success'] * 100) / (result['today']['count'] or 1) + result['7days']['success'] = (result['7days']['success'] * 100) / (result['7days']['count'] or 1) + result['my_all']['hours'] = round(result['my_all']['hours'] / (result['my_all']['count'] or 1), 2) + result['my_high']['hours'] = round(result['my_high']['hours'] / (result['my_high']['count'] or 1), 2) + result['my_urgent']['hours'] = round(result['my_urgent']['hours'] / (result['my_urgent']['count'] or 1), 2) + + if self.env['odex25_helpdesk.team'].search([('use_rating', '=', True), '|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)]): + result['rating_enable'] = True + # rating of today + domain = [('user_id', '=', self.env.uid)] + dt = fields.Date.today() + tickets = self.env['odex25_helpdesk.ticket'].search(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)]) + activity = tickets.rating_get_grades() + total_rating = self._compute_activity_avg(activity) + total_activity_values = sum(activity.values()) + team_satisfaction = round((total_rating / total_activity_values if total_activity_values else 0), 2) * 5 + if team_satisfaction: + result['today']['rating'] = team_satisfaction + + # rating of last 7 days (6 days + today) + dt = fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6))) + tickets = self.env['odex25_helpdesk.ticket'].search(domain + [('stage_id.is_close', '=', True), ('close_date', '>=', dt)]) + activity = tickets.rating_get_grades() + total_rating = self._compute_activity_avg(activity) + total_activity_values = sum(activity.values()) + team_satisfaction_7days = round((total_rating / total_activity_values if total_activity_values else 0), 2) * 5 + if team_satisfaction_7days: + result['7days']['rating'] = team_satisfaction_7days + return result + + def _action_view_rating(self, period=False, only_my_closed=False): + """ return the action to see all the rating about the tickets of the Team + :param period: either 'today' or 'seven_days' to include (or not) the tickets closed in this period + :param only_my_closed: True will include only the ticket of the current user in a closed stage + """ + domain = [('team_id', 'in', self.ids)] + + if period == 'seven_days': + domain += [('close_date', '>=', fields.Datetime.to_string((datetime.date.today() - relativedelta.relativedelta(days=6))))] + elif period == 'today': + domain += [('close_date', '>=', fields.Datetime.to_string(datetime.date.today()))] + + if only_my_closed: + domain += [('user_id', '=', self._uid), ('stage_id.is_close', '=', True)] + + ticket_ids = self.env['odex25_helpdesk.ticket'].search(domain).ids + action = self.env["ir.actions.actions"]._for_xml_id("rating.rating_rating_view") + action['domain'] = [('res_id', 'in', ticket_ids), ('rating', '!=', -1), ('res_model', '=', 'odex25_helpdesk.ticket'), ('consumed', '=', True)] + action['help'] = '

No data yet !

Create tickets to get statistics.

' + return action + + def action_view_ticket(self): + action = self.env["ir.actions.actions"]._for_xml_id("odex25_helpdesk.odex25_helpdesk_ticket_action_team") + action['display_name'] = self.name + return action + + @api.model + def action_view_rating_today(self): + # call this method of on click "Customer Rating" button on dashbord for today rating of teams tickets + return self.search(['|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)])._action_view_rating(period='today', only_my_closed=True) + + @api.model + def action_view_rating_7days(self): + # call this method of on click "Customer Rating" button on dashbord for last 7days rating of teams tickets + return self.search(['|', ('member_ids', 'in', self._uid), ('member_ids', '=', False)])._action_view_rating(period='seven_days', only_my_closed=True) + + def action_view_all_rating(self): + """ return the action to see all the rating about the all sort of activity of the team (tickets) """ + return self._action_view_rating() + + @api.model + def _compute_activity_avg(self, activity): + # compute average base on all rating value + # like: 5 great, 3 okey, 1 bad + # great = 5, okey = 3, bad = 0 + # (5*5) + (2*3) + (1*0) = 60 / 8 (nuber of activity for rating) + great = activity['great'] * 5.00 + okey = activity['okay'] * 3.00 + bad = activity['bad'] * 0.00 + return great + okey + bad + + def _determine_user_to_assign(self): + """ Get a dict with the user (per team) that should be assign to the nearly created ticket according to the team policy + :returns a mapping of team identifier with the "to assign" user (maybe an empty record). + :rtype : dict (key=team_id, value=record of res.users) + """ + result = dict.fromkeys(self.ids, self.env['res.users']) + for team in self: + member_ids = sorted(team.member_ids.ids) if team.member_ids else sorted(team.visibility_member_ids.ids) + if member_ids: + if team.assign_method == 'randomly': # randomly means new tickets get uniformly distributed + last_assigned_user = self.env['odex25_helpdesk.ticket'].search([('team_id', '=', team.id)], order='create_date desc, id desc', limit=1).user_id + index = 0 + if last_assigned_user and last_assigned_user.id in member_ids: + previous_index = member_ids.index(last_assigned_user.id) + index = (previous_index + 1) % len(member_ids) + result[team.id] = self.env['res.users'].browse(member_ids[index]) + elif team.assign_method == 'balanced': # find the member with the least open ticket + ticket_count_data = self.env['odex25_helpdesk.ticket'].read_group([('stage_id.is_close', '=', False), ('user_id', 'in', member_ids), ('team_id', '=', team.id)], ['user_id'], ['user_id']) + open_ticket_per_user_map = dict.fromkeys(member_ids, 0) # dict: user_id -> open ticket count + open_ticket_per_user_map.update((item['user_id'][0], item['user_id_count']) for item in ticket_count_data) + result[team.id] = self.env['res.users'].browse(min(open_ticket_per_user_map, key=open_ticket_per_user_map.get)) + return result + + def _determine_stage(self): + """ Get a dict with the stage (per team) that should be set as first to a created ticket + :returns a mapping of team identifier with the stage (maybe an empty record). + :rtype : dict (key=team_id, value=record of odex25_helpdesk.stage) + """ + result = dict.fromkeys(self.ids, self.env['odex25_helpdesk.stage']) + for team in self: + result[team.id] = self.env['odex25_helpdesk.stage'].search([('team_ids', 'in', team.id)], order='sequence', limit=1) + return result + + def _get_closing_stage(self): + """ + Return the first closing kanban stage or the last stage of the pipe if none + """ + closed_stage = self.stage_ids.filtered(lambda stage: stage.is_close) + if not closed_stage: + closed_stage = self.stage_ids[-1] + return closed_stage + + + +class odex25_helpdeskStage(models.Model): + _name = 'odex25_helpdesk.stage' + _description = 'Helpdesk Stage' + _order = 'sequence, id' + + def _default_team_ids(self): + team_id = self.env.context.get('default_team_id') + if team_id: + return [(4, team_id, 0)] + + name = fields.Char('Stage Name', required=True, translate=True) + description = fields.Text(translate=True) + sequence = fields.Integer('Sequence', default=10) + is_close = fields.Boolean( + 'Closing Stage', + help='Tickets in this stage are considered as done. This is used notably when ' + 'computing SLAs and KPIs on tickets.') + fold = fields.Boolean( + 'Folded in Kanban', + help='This stage is folded in the kanban view when there are no records in that stage to display.') + team_ids = fields.Many2many( + 'odex25_helpdesk.team', relation='team_stage_rel', string='Team', + default=_default_team_ids, + help='Specific team that uses this stage. Other teams will not be able to see or use this stage.') + template_id = fields.Many2one( + 'mail.template', 'Email Template', + domain="[('model', '=', 'odex25_helpdesk.ticket')]", + help="Automated email sent to the ticket's customer when the ticket reaches this stage.") + legend_blocked = fields.Char( + 'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True, + help='Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage.') + legend_done = fields.Char( + 'Green Kanban Label', default=lambda s: _('Ready'), translate=True, required=True, + help='Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage.') + legend_normal = fields.Char( + 'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True, + help='Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage.') + + def unlink(self): + stages = self + default_team_id = self.env.context.get('default_team_id') + if default_team_id: + shared_stages = self.filtered(lambda x: len(x.team_ids) > 1 and default_team_id in x.team_ids.ids) + tickets = self.env['odex25_helpdesk.ticket'].with_context(active_test=False).search([('team_id', '=', default_team_id), ('stage_id', 'in', self.ids)]) + if shared_stages and not tickets: + shared_stages.write({'team_ids': [(3, default_team_id)]}) + stages = self.filtered(lambda x: x not in shared_stages) + return super(odex25_helpdeskStage, stages).unlink() + + +class odex25_helpdeskSLA(models.Model): + _name = "odex25_helpdesk.sla" + _order = "name" + _description = "Helpdesk SLA Policies" + + name = fields.Char('SLA Policy Name', required=True, index=True) + description = fields.Text('SLA Policy Description') + active = fields.Boolean('Active', default=True) + team_id = fields.Many2one('odex25_helpdesk.team', 'Team', required=True) + target_type = fields.Selection([('stage', 'Reaching Stage'), ('assigning', 'Assigning Ticket')], default='stage', required=True) + ticket_type_id = fields.Many2one( + 'odex25_helpdesk.ticket.type', "Ticket Type", + help="Only apply the SLA to a specific ticket type. If left empty it will apply to all types.") + tag_ids = fields.Many2many( + 'odex25_helpdesk.tag', string='Tags', + help="Only apply the SLA to tickets with specific tags. If left empty it will apply to all tags.") + stage_id = fields.Many2one( + 'odex25_helpdesk.stage', 'Target Stage', + help='Minimum stage a ticket needs to reach in order to satisfy this SLA.') + exclude_stage_ids = fields.Many2many( + 'odex25_helpdesk.stage', string='Exclude Stages', + compute='_compute_exclude_stage_ids', store=True, readonly=False, copy=True, + domain="[('id', '!=', stage_id.id)]", + help='The amount of time the ticket spends in this stage will not be taken into account when evaluating the status of the SLA Policy.') + priority = fields.Selection( + TICKET_PRIORITY, string='Minimum Priority', + default='0', required=True, + help='Tickets under this priority will not be taken into account.') + company_id = fields.Many2one('res.company', 'Company', related='team_id.company_id', readonly=True, store=True) + time_days = fields.Integer( + 'Days', default=0, required=True, + help="Days to reach given stage based on ticket creation date") + time_hours = fields.Integer( + 'Hours', default=0, inverse='_inverse_time_hours', required=True, + help="Hours to reach given stage based on ticket creation date") + time_minutes = fields.Integer( + 'Minutes', default=0, inverse='_inverse_time_minutes', required=True, + help="Minutes to reach given stage based on ticket creation date") + category_id = fields.Many2one('service.category') + service_id = fields.Many2one('helpdesk.service') + + @api.depends('target_type') + def _compute_exclude_stage_ids(self): + self.update({'exclude_stage_ids': False}) + + def _inverse_time_hours(self): + for sla in self: + sla.time_hours = max(0, sla.time_hours) + if sla.time_hours >= 24: + sla.time_days += sla.time_hours / 24 + sla.time_hours = sla.time_hours % 24 + + def _inverse_time_minutes(self): + for sla in self: + sla.time_minutes = max(0, sla.time_minutes) + if sla.time_minutes >= 60: + sla.time_hours += sla.time_minutes / 60 + sla.time_minutes = sla.time_minutes % 60 diff --git a/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk_ticket.py b/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk_ticket.py new file mode 100644 index 000000000..3ee17259a --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/odex25_helpdesk_ticket.py @@ -0,0 +1,970 @@ +# -*- coding: utf-8 -*- + +from dateutil.relativedelta import relativedelta +from datetime import timedelta +from random import randint + +from odoo import api, fields, models, tools, _ +from odoo.osv import expression +from odoo.exceptions import AccessError + +TICKET_PRIORITY = [ + ('0', 'All'), + ('1', 'Low priority'), + ('2', 'High priority'), + ('3', 'Urgent'), +] + + +class odex25_helpdeskTag(models.Model): + _name = 'odex25_helpdesk.tag' + _description = 'Helpdesk Tags' + _order = 'name' + + def _get_default_color(self): + return randint(1, 11) + + name = fields.Char('Tag Name', required=True) + color = fields.Integer('Color', default=_get_default_color) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Tag name already exists !"), + ] + + +class odex25_helpdeskTicketType(models.Model): + _name = 'odex25_helpdesk.ticket.type' + _description = 'Helpdesk Ticket Type' + _order = 'sequence' + + name = fields.Char('Type', required=True, translate=True) + sequence = fields.Integer(default=10) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Type name already exists !"), + ] + + +class odex25_helpdeskSLAStatus(models.Model): + _name = 'odex25_helpdesk.sla.status' + _description = "Ticket SLA Status" + _table = 'odex25_helpdesk_sla_status' + _order = 'deadline ASC, sla_stage_id' + _rec_name = 'sla_id' + + ticket_id = fields.Many2one('odex25_helpdesk.ticket', string='Ticket', required=True, ondelete='cascade', + index=True) + sla_id = fields.Many2one('odex25_helpdesk.sla', required=True, ondelete='cascade') + sla_stage_id = fields.Many2one('odex25_helpdesk.stage', related='sla_id.stage_id', + store=True) # need to be stored for the search in `_sla_reach` + target_type = fields.Selection(related='sla_id.target_type', store=True) + deadline = fields.Datetime("Deadline", compute='_compute_deadline', compute_sudo=True, store=True) + reached_datetime = fields.Datetime("Reached Date", + help="Datetime at which the SLA stage was reached for the first time") + status = fields.Selection([('failed', 'Failed'), ('reached', 'Reached'), ('ongoing', 'Ongoing')], string="Status", + compute='_compute_status', compute_sudo=True, search='_search_status') + color = fields.Integer("Color Index", compute='_compute_color') + exceeded_days = fields.Float("Excedeed Working Days", compute='_compute_exceeded_days', compute_sudo=True, + store=True, + help="Working days exceeded for reached SLAs compared with deadline. Positive number means the SLA was eached after the deadline.") + + @api.depends('ticket_id.create_date', 'sla_id', 'ticket_id.stage_id') + def _compute_deadline(self): + for status in self: + if (status.deadline and status.reached_datetime) or ( + status.deadline and status.target_type == 'stage' and not status.sla_id.exclude_stage_ids) or ( + status.status == 'failed'): + continue + if status.target_type == 'assigning' and status.sla_stage_id == status.ticket_id.stage_id: + deadline = fields.Datetime.now() + elif status.target_type == 'assigning' and status.sla_stage_id: + status.deadline = False + continue + else: + deadline = status.ticket_id.create_date + working_calendar = status.ticket_id.team_id.resource_calendar_id + if not working_calendar: + # Normally, having a working_calendar is mandatory + status.deadline = deadline + continue + + if status.target_type == 'stage' and status.sla_id.exclude_stage_ids: + if status.ticket_id.stage_id in status.sla_id.exclude_stage_ids: + # We are in the freezed time stage: No deadline + status.deadline = False + continue + + time_days = status.sla_id.time_days + if time_days and ( + status.sla_id.target_type == 'stage' or status.sla_id.target_type == 'assigning' and not status.sla_id.stage_id): + deadline = working_calendar.plan_days(time_days + 1, deadline, compute_leaves=True) + # We should also depend on ticket creation time, otherwise for 1 day SLA, all tickets + # created on monday will have their deadline filled with tuesday 8:00 + create_dt = status.ticket_id.create_date + deadline = deadline.replace(hour=create_dt.hour, minute=create_dt.minute, second=create_dt.second, + microsecond=create_dt.microsecond) + elif time_days and status.target_type == 'assigning' and status.sla_stage_id == status.ticket_id.stage_id: + deadline = working_calendar.plan_days(time_days + 1, deadline, compute_leaves=True) + reached_stage_dt = fields.Datetime.now() + deadline = deadline.replace(hour=reached_stage_dt.hour, minute=reached_stage_dt.minute, + second=reached_stage_dt.second, microsecond=reached_stage_dt.microsecond) + + sla_hours = status.sla_id.time_hours + (status.sla_id.time_minutes / 60) + + if status.target_type == 'stage' and status.sla_id.exclude_stage_ids: + sla_hours += status._get_freezed_hours(working_calendar) + + # Except if ticket creation time is later than the end time of the working day + deadline_for_working_cal = working_calendar.plan_hours(0, deadline) + if deadline_for_working_cal and deadline.day < deadline_for_working_cal.day: + deadline = deadline.replace(hour=0, minute=0, second=0, microsecond=0) + # We should execute the function plan_hours in any case because, in a 1 day SLA environment, + # if I create a ticket knowing that I'm not working the day after at the same time, ticket + # deadline will be set at time I don't work (ticket creation time might not be in working calendar). + status.deadline = working_calendar.plan_hours(sla_hours, deadline, compute_leaves=True) + + @api.depends('deadline', 'reached_datetime') + def _compute_status(self): + """ Note: this computed field depending on 'now()' is stored, but refreshed by a cron """ + for status in self: + if status.reached_datetime and status.deadline: # if reached_datetime, SLA is finished: either failed or succeeded + status.status = 'reached' if status.reached_datetime < status.deadline else 'failed' + else: # if not finished, deadline should be compared to now() + status.status = 'ongoing' if not status.deadline or status.deadline > fields.Datetime.now() else 'failed' + + @api.model + def _search_status(self, operator, value): + """ Supported operators: '=', 'in' and their negative form. """ + # constants + datetime_now = fields.Datetime.now() + positive_domain = { + 'failed': ['|', '&', ('reached_datetime', '=', True), ('deadline', '<=', 'reached_datetime'), '&', + ('reached_datetime', '=', False), ('deadline', '<=', fields.Datetime.to_string(datetime_now))], + 'reached': ['&', ('reached_datetime', '=', True), ('reached_datetime', '<', 'deadline')], + 'ongoing': ['&', ('reached_datetime', '=', False), + ('deadline', '<=', fields.Datetime.to_string(datetime_now))] + } + # in/not in case: we treat value as a list of selection item + if not isinstance(value, list): + value = [value] + # transform domains + if operator in expression.NEGATIVE_TERM_OPERATORS: + # "('status', 'not in', [A, B])" tranformed into "('status', '=', C) OR ('status', '=', D)" + domains_to_keep = [dom for key, dom in positive_domain if key not in value] + return expression.OR(domains_to_keep) + else: + return expression.OR(positive_domain[value_item] for value_item in value) + + @api.depends('status') + def _compute_color(self): + for status in self: + if status.status == 'failed': + status.color = 1 + elif status.status == 'reached': + status.color = 10 + else: + status.color = 0 + + @api.depends('deadline', 'reached_datetime') + def _compute_exceeded_days(self): + for status in self: + if status.reached_datetime and status.deadline and status.ticket_id.team_id.resource_calendar_id: + if status.reached_datetime <= status.deadline: + start_dt = status.reached_datetime + end_dt = status.deadline + factor = -1 + else: + start_dt = status.deadline + end_dt = status.reached_datetime + factor = 1 + duration_data = status.ticket_id.team_id.resource_calendar_id.get_work_duration_data(start_dt, end_dt, + compute_leaves=True) + status.exceeded_days = duration_data['days'] * factor + else: + status.exceeded_days = False + + def _get_freezed_hours(self, working_calendar): + self.ensure_one() + hours_freezed = 0 + + field_stage = self.env['ir.model.fields']._get(self.ticket_id._name, "stage_id") + freeze_stages = self.sla_id.exclude_stage_ids.ids + tracking_lines = self.ticket_id.message_ids.tracking_value_ids.filtered( + lambda tv: tv.field == field_stage).sorted(key="create_date") + + if not tracking_lines: + return 0 + + old_time = self.ticket_id.create_date + for tracking_line in tracking_lines: + if tracking_line.old_value_integer in freeze_stages: + # We must use get_work_hours_count to compute real waiting hours (as the deadline computation is also based on calendar) + hours_freezed += working_calendar.get_work_hours_count(old_time, tracking_line.create_date) + old_time = tracking_line.create_date + if tracking_lines[-1].new_value_integer in freeze_stages: + # the last tracking line is not yet created + hours_freezed += working_calendar.get_work_hours_count(old_time, fields.Datetime.now()) + return hours_freezed + + +class odex25_helpdeskTicket(models.Model): + _name = 'odex25_helpdesk.ticket' + _description = 'Helpdesk Ticket' + _order = 'priority desc, id desc' + _inherit = ['portal.mixin', 'mail.thread.cc', 'utm.mixin', 'rating.mixin', 'mail.activity.mixin'] + + @api.model + def default_get(self, fields): + result = super(odex25_helpdeskTicket, self).default_get(fields) + if result.get('team_id') and fields: + team = self.env['odex25_helpdesk.team'].browse(result['team_id']) + if 'user_id' in fields and 'user_id' not in result: # if no user given, deduce it from the team + result['user_id'] = team._determine_user_to_assign()[team.id].id + if 'stage_id' in fields and 'stage_id' not in result: # if no stage given, deduce it from the team + result['stage_id'] = team._determine_stage()[team.id].id + return result + + @api.model + def _read_group_stage_ids(self, stages, domain, order): + # write the domain + # - ('id', 'in', stages.ids): add columns that should be present + # - OR ('team_ids', '=', team_id) if team_id: add team columns + search_domain = [('id', 'in', stages.ids)] + if self.env.context.get('default_team_id'): + search_domain = ['|', ('team_ids', 'in', self.env.context['default_team_id'])] + search_domain + + return stages.search(search_domain, order=order) + + name = fields.Char(string='Subject', required=True, index=True) + team_id = fields.Many2one('odex25_helpdesk.team', string='Helpdesk Team', index=True) + description = fields.Text(required=True, tracking=True) + active = fields.Boolean(default=True) + ticket_type_id = fields.Many2one('odex25_helpdesk.ticket.type', string="Ticket Type") + tag_ids = fields.Many2many('odex25_helpdesk.tag', string='Tags') + company_id = fields.Many2one(related='team_id.company_id', string='Company', store=True, readonly=True) + color = fields.Integer(string='Color Index') + kanban_state = fields.Selection([ + ('normal', 'Grey'), + ('done', 'Green'), + ('blocked', 'Red')], string='Kanban State', + default='normal', required=True) + kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Column Status', tracking=True) + legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, + related_sudo=False) + legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, + related_sudo=False) + legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, + related_sudo=False) + domain_user_ids = fields.Many2many('res.users', compute='_compute_domain_user_ids') + user_id = fields.Many2one( + 'res.users', string='Assigned to', compute='_compute_user_and_stage_ids', store=True, + readonly=False, tracking=True, + domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]) + partner_id = fields.Many2one('res.partner', string='Customer') + partner_ticket_count = fields.Integer('Number of closed tickets from the same partner', + compute='_compute_partner_ticket_count') + attachment_number = fields.Integer(compute='_compute_attachment_number', string="Number of Attachments") + is_self_assigned = fields.Boolean("Am I assigned", compute='_compute_is_self_assigned') + # Used to submit tickets from a contact form + partner_name = fields.Char(string='Customer Name', compute='_compute_partner_info', store=True, readonly=False) + partner_email = fields.Char(string='Customer Email', compute='_compute_partner_info', store=True, readonly=False) + closed_by_partner = fields.Boolean('Closed by Partner', readonly=True, + help="If checked, this means the ticket was closed through the customer portal by the customer.") + # Used in message_get_default_recipients, so if no partner is created, email is sent anyway + email = fields.Char(related='partner_email', string='Email on Customer', readonly=False) + # priority = fields.Selection(related="service_id.priority", string='Priority', default='0') + priority = fields.Selection(TICKET_PRIORITY, string='Priority', default='0') + stage_id = fields.Many2one( + 'odex25_helpdesk.stage', string='Stage', compute='_compute_user_and_stage_ids', store=True, + readonly=False, ondelete='restrict', tracking=True, group_expand='_read_group_stage_ids', + copy=False, index=True, domain="[('team_ids', '=', team_id)]") + date_last_stage_update = fields.Datetime("Last Stage Update", copy=False, readonly=True) + # next 4 fields are computed in write (or create) + assign_date = fields.Datetime("First assignment date") + assign_hours = fields.Integer("Time to first assignment (hours)", compute='_compute_assign_hours', store=True, + help="This duration is based on the working calendar of the team") + close_date = fields.Datetime("Close date", copy=False) + close_hours = fields.Integer("Time to close (hours)", compute='_compute_close_hours', store=True, + help="This duration is based on the working calendar of the team") + open_hours = fields.Integer("Open Time (hours)", compute='_compute_open_hours', search='_search_open_hours', + help="This duration is not based on the working calendar of the team") + # SLA relative + sla_ids = fields.Many2many('odex25_helpdesk.sla', 'odex25_helpdesk_sla_status', 'ticket_id', 'sla_id', + string="SLAs", copy=False) + sla_status_ids = fields.One2many('odex25_helpdesk.sla.status', 'ticket_id', string="SLA Status") + sla_reached_late = fields.Boolean("Has SLA reached late", compute='_compute_sla_reached_late', compute_sudo=True, + store=True) + sla_deadline = fields.Datetime("SLA Deadline", compute='_compute_sla_deadline', compute_sudo=True, store=True, + help="The closest deadline of all SLA applied on this ticket") + sla_fail = fields.Boolean("Failed SLA Policy", compute='_compute_sla_fail', search='_search_sla_fail') + sla_success = fields.Boolean("Success SLA Policy", compute='_compute_sla_success', search='_search_sla_success') + + use_credit_notes = fields.Boolean(related='team_id.use_credit_notes', string='Use Credit Notes') + use_coupons = fields.Boolean(related='team_id.use_coupons', string='Use Coupons') + use_product_returns = fields.Boolean(related='team_id.use_product_returns', string='Use Returns') + use_product_repairs = fields.Boolean(related='team_id.use_product_repairs', string='Use Repairs') + + # customer portal: include comment and incoming emails in communication history + website_message_ids = fields.One2many( + domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) + + category_id = fields.Many2one('service.category') + service_id = fields.Many2one('helpdesk.service') + assistance_user_id = fields.Many2one('res.users', string="Assistance", domain=lambda self: [("groups_id", "=", + self.env.ref("odex25_helpdesk.group_odex25_helpdesk_user").id)]) + # work_email = fields.Char(related='employee_id.work_email', string="Work Email") + department_id = fields.Many2one('hr.department', string="Administrative Structure") + phone_no = fields.Char(string="Phone Number") + on_behalf = fields.Many2one('hr.employee', string="On behalf of") + emp_req = fields.Boolean(default=False) + schedule_date = fields.Datetime("Scheduled Date") + + @api.onchange('service_id') + def _onchange_invoice_date(self): + if self.service_id: + self.priority = self.service_id.priority + + @api.onchange('partner_id') + def onchange_partner_id(self): + """ + get the partner of the user + """ + user = self.env['res.users'].search([('partner_id', '=', self.partner_id.id)], limit=1) + employee_id = self.env['hr.employee'].search([('user_id', '=', user.id)], limit=1) + self.department_id = employee_id.department_id + + def activity_update(self): + for ticket in self.filtered(lambda request: request.schedule_date): + ticket.activity_schedule('odex25_helpdesk.mail_act_odex25_helpdesk_assistance', + fields.Datetime.from_string(ticket.schedule_date).date(), + note=ticket.name, user_id=ticket.assistance_user_id.id or self.env.uid) + + @api.depends('stage_id', 'kanban_state') + def _compute_kanban_state_label(self): + for task in self: + if task.kanban_state == 'normal': + task.kanban_state_label = task.legend_normal + elif task.kanban_state == 'blocked': + task.kanban_state_label = task.legend_blocked + else: + task.kanban_state_label = task.legend_done + + @api.depends('team_id') + def _compute_domain_user_ids(self): + for task in self: + if task.team_id and task.team_id.visibility_member_ids: + odex25_helpdesk_manager = self.env['res.users'].search( + [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id)]) + task.domain_user_ids = [(6, 0, (odex25_helpdesk_manager + task.team_id.visibility_member_ids).ids)] + else: + odex25_helpdesk_users = self.env['res.users'].search( + [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]).ids + task.domain_user_ids = [(6, 0, odex25_helpdesk_users)] + + def _compute_access_url(self): + super(odex25_helpdeskTicket, self)._compute_access_url() + for ticket in self: + ticket.access_url = '/my/ticket/%s' % ticket.id + + def _compute_attachment_number(self): + read_group_res = self.env['ir.attachment'].read_group( + [('res_model', '=', 'odex25_helpdesk.ticket'), ('res_id', 'in', self.ids)], + ['res_id'], ['res_id']) + attach_data = {res['res_id']: res['res_id_count'] for res in read_group_res} + for record in self: + record.attachment_number = attach_data.get(record.id, 0) + + @api.depends('sla_status_ids.deadline', 'sla_status_ids.reached_datetime') + def _compute_sla_reached_late(self): + """ Required to do it in SQL since we need to compare 2 columns value """ + mapping = {} + if self.ids: + self.env.cr.execute(""" + SELECT ticket_id, COUNT(id) AS reached_late_count + FROM odex25_helpdesk_sla_status + WHERE ticket_id IN %s AND deadline < reached_datetime + GROUP BY ticket_id + """, (tuple(self.ids),)) + mapping = dict(self.env.cr.fetchall()) + + for ticket in self: + ticket.sla_reached_late = mapping.get(ticket.id, 0) > 0 + + @api.depends('sla_status_ids.deadline', 'sla_status_ids.reached_datetime') + def _compute_sla_deadline(self): + """ Keep the deadline for the last stage (closed one), so a closed ticket can have a status failed. + Note: a ticket in a closed stage will probably have no deadline + """ + for ticket in self: + deadline = False + status_not_reached = ticket.sla_status_ids.filtered( + lambda status: not status.reached_datetime and status.deadline) + ticket.sla_deadline = min(status_not_reached.mapped('deadline')) if status_not_reached else deadline + + @api.depends('sla_deadline', 'sla_reached_late') + def _compute_sla_fail(self): + now = fields.Datetime.now() + for ticket in self: + if ticket.sla_deadline: + ticket.sla_fail = (ticket.sla_deadline < now) or ticket.sla_reached_late + else: + ticket.sla_fail = ticket.sla_reached_late + + @api.model + def _search_sla_fail(self, operator, value): + datetime_now = fields.Datetime.now() + if (value and operator in expression.NEGATIVE_TERM_OPERATORS) or ( + not value and operator not in expression.NEGATIVE_TERM_OPERATORS): # is not failed + return ['&', ('sla_reached_late', '=', False), '|', ('sla_deadline', '=', False), + ('sla_deadline', '>=', datetime_now)] + return ['|', ('sla_reached_late', '=', True), ('sla_deadline', '<', datetime_now)] # is failed + + @api.depends('sla_deadline', 'sla_reached_late') + def _compute_sla_success(self): + now = fields.Datetime.now() + for ticket in self: + ticket.sla_success = (ticket.sla_deadline and ticket.sla_deadline > now) + + @api.model + def _search_sla_success(self, operator, value): + datetime_now = fields.Datetime.now() + if (value and operator in expression.NEGATIVE_TERM_OPERATORS) or ( + not value and operator not in expression.NEGATIVE_TERM_OPERATORS): # is failed + return [[('sla_status_ids.reached_datetime', '>', datetime_now), ('sla_reached_late', '!=', False)]] + return [('sla_status_ids.reached_datetime', '<', datetime_now), ('sla_reached_late', '=', False)] # is success + + @api.depends('user_id') + def _compute_is_self_assigned(self): + for ticket in self: + ticket.is_self_assigned = self.env.user == ticket.user_id + + @api.depends('team_id') + def _compute_user_and_stage_ids(self): + for ticket in self.filtered(lambda ticket: ticket.team_id): + if not ticket.user_id: + ticket.user_id = ticket.team_id._determine_user_to_assign()[ticket.team_id.id] + if not ticket.stage_id or ticket.stage_id not in ticket.team_id.stage_ids: + ticket.stage_id = ticket.team_id._determine_stage()[ticket.team_id.id] + + @api.depends('partner_id') + def _compute_partner_info(self): + for ticket in self: + if ticket.partner_id: + ticket.partner_name = ticket.partner_id.name + ticket.partner_email = ticket.partner_id.email + + @api.depends('partner_id') + def _compute_partner_ticket_count(self): + data = self.env['odex25_helpdesk.ticket'].read_group([ + ('partner_id', 'in', self.mapped('partner_id').ids), + ('stage_id.is_close', '=', False) + ], ['partner_id'], ['partner_id'], lazy=False) + ticket_per_partner_map = dict((item['partner_id'][0], item['__count']) for item in data) + for ticket in self: + ticket.partner_ticket_count = ticket_per_partner_map.get(ticket.partner_id.id, 0) + + @api.depends('assign_date') + def _compute_assign_hours(self): + for ticket in self: + create_date = fields.Datetime.from_string(ticket.create_date) + if create_date and ticket.assign_date and ticket.team_id.resource_calendar_id: + duration_data = ticket.team_id.resource_calendar_id.get_work_duration_data(create_date, + fields.Datetime.from_string( + ticket.assign_date), + compute_leaves=True) + ticket.assign_hours = duration_data['hours'] + else: + ticket.assign_hours = False + + @api.depends('create_date', 'close_date') + def _compute_close_hours(self): + for ticket in self: + create_date = fields.Datetime.from_string(ticket.create_date) + if create_date and ticket.close_date: + duration_data = ticket.team_id.resource_calendar_id.get_work_duration_data(create_date, + fields.Datetime.from_string( + ticket.close_date), + compute_leaves=True) + ticket.close_hours = duration_data['hours'] + else: + ticket.close_hours = False + + @api.depends('close_hours') + def _compute_open_hours(self): + for ticket in self: + if ticket.create_date: # fix from https://github.com/odoo/enterprise/commit/928fbd1a16e9837190e9c172fa50828fae2a44f7 + if ticket.close_date: + time_difference = ticket.close_date - fields.Datetime.from_string(ticket.create_date) + else: + time_difference = fields.Datetime.now() - fields.Datetime.from_string(ticket.create_date) + ticket.open_hours = (time_difference.seconds) / 3600 + time_difference.days * 24 + else: + ticket.open_hours = 0 + + @api.model + def _search_open_hours(self, operator, value): + dt = fields.Datetime.now() - relativedelta.relativedelta(hours=value) + + d1, d2 = False, False + if operator in ['<', '<=', '>', '>=']: + d1 = ['&', ('close_date', '=', False), ('create_date', expression.TERM_OPERATORS_NEGATION[operator], dt)] + d2 = ['&', ('close_date', '!=', False), ('close_hours', operator, value)] + elif operator in ['=', '!=']: + subdomain = ['&', ('create_date', '>=', dt.replace(minute=0, second=0, microsecond=0)), + ('create_date', '<=', dt.replace(minute=59, second=59, microsecond=99))] + if operator in expression.NEGATIVE_TERM_OPERATORS: + subdomain = expression.distribute_not(subdomain) + d1 = expression.AND([[('close_date', '=', False)], subdomain]) + d2 = ['&', ('close_date', '!=', False), ('close_hours', operator, value)] + return expression.OR([d1, d2]) + + # ------------------------------------------------------------ + # ORM overrides + # ------------------------------------------------------------ + + def name_get(self): + result = [] + for ticket in self: + result.append((ticket.id, "%s (#%d)" % (ticket.name, ticket._origin.id))) + return result + + @api.model + def create_action(self, action_ref, title, search_view_ref): + action = self.env["ir.actions.actions"]._for_xml_id(action_ref) + if title: + action['display_name'] = title + if search_view_ref: + action['search_view_id'] = self.env.ref(search_view_ref).read()[0] + action['views'] = [(False, view) for view in action['view_mode'].split(",")] + + return {'action': action} + + @api.model_create_multi + def create(self, list_value): + now = fields.Datetime.now() + # determine user_id and stage_id if not given. Done in batch. + teams = self.env['odex25_helpdesk.team'].browse([vals['team_id'] for vals in list_value if vals.get('team_id')]) + team_default_map = dict.fromkeys(teams.ids, dict()) + for team in teams: + team_default_map[team.id] = { + 'stage_id': team._determine_stage()[team.id].id, + 'user_id': team._determine_user_to_assign()[team.id].id + } + + # Manually create a partner now since 'generate_recipients' doesn't keep the name. This is + # to avoid intrusive changes in the 'mail' module + for vals in list_value: + partner_id = vals.get('partner_id', False) + partner_name = vals.get('partner_name', False) + partner_email = vals.get('partner_email', False) + if partner_name and partner_email and not partner_id: + try: + vals['partner_id'] = self.env['res.partner'].find_or_create( + tools.formataddr((partner_name, partner_email)) + ).id + except UnicodeEncodeError: + # 'formataddr' doesn't support non-ascii characters in email. Therefore, we fall + # back on a simple partner creation. + vals['partner_id'] = self.env['res.partner'].create({ + 'name': partner_name, + 'email': partner_email, + }).id + + # determine partner email for ticket with partner but no email given + partners = self.env['res.partner'].browse([vals['partner_id'] for vals in list_value if + 'partner_id' in vals and vals.get( + 'partner_id') and 'partner_email' not in vals]) + partner_email_map = {partner.id: partner.email for partner in partners} + partner_name_map = {partner.id: partner.name for partner in partners} + + for vals in list_value: + if vals.get('team_id'): + team_default = team_default_map[vals['team_id']] + if 'stage_id' not in vals: + vals['stage_id'] = team_default['stage_id'] + # Note: this will break the randomly distributed user assignment. Indeed, it will be too difficult to + # equally assigned user when creating ticket in batch, as it requires to search after the last assigned + # after every ticket creation, which is not very performant. We decided to not cover this user case. + if 'user_id' not in vals: + vals['user_id'] = team_default['user_id'] + if vals.get( + 'user_id'): # if a user is finally assigned, force ticket assign_date and reset assign_hours + vals['assign_date'] = fields.Datetime.now() + vals['assign_hours'] = 0 + + # set partner email if in map of not given + if vals.get('partner_id') in partner_email_map: + vals['partner_email'] = partner_email_map.get(vals['partner_id']) + # set partner name if in map of not given + if vals.get('partner_id') in partner_name_map: + vals['partner_name'] = partner_name_map.get(vals['partner_id']) + + if vals.get('stage_id'): + vals['date_last_stage_update'] = now + + # context: no_log, because subtype already handle this + tickets = super(odex25_helpdeskTicket, self).create(list_value) + tickets.activity_update() + tickets.emp_req = True + + # make customer follower + for ticket in tickets: + if ticket.partner_id: + ticket.message_subscribe(partner_ids=ticket.partner_id.ids) + + ticket._portal_ensure_token() + + # apply SLA + tickets.sudo()._sla_apply() + + return tickets + + def write(self, vals): + # we set the assignation date (assign_date) to now for tickets that are being assigned for the first time + # same thing for the closing date + assigned_tickets = closed_tickets = self.browse() + if vals.get('user_id'): + assigned_tickets = self.filtered(lambda ticket: not ticket.assign_date) + + if vals.get('stage_id'): + if self.env['odex25_helpdesk.stage'].browse(vals.get('stage_id')).is_close: + closed_tickets = self.filtered(lambda ticket: not ticket.close_date) + else: # auto reset the 'closed_by_partner' flag + vals['closed_by_partner'] = False + + now = fields.Datetime.now() + + # update last stage date when changing stage + if 'stage_id' in vals: + vals['date_last_stage_update'] = now + + res = super(odex25_helpdeskTicket, self - assigned_tickets - closed_tickets).write(vals) + res &= super(odex25_helpdeskTicket, assigned_tickets - closed_tickets).write(dict(vals, **{ + 'assign_date': now, + })) + res &= super(odex25_helpdeskTicket, closed_tickets - assigned_tickets).write(dict(vals, **{ + 'close_date': now, + })) + res &= super(odex25_helpdeskTicket, assigned_tickets & closed_tickets).write(dict(vals, **{ + 'assign_date': now, + 'close_date': now, + })) + + if vals.get('partner_id'): + self.message_subscribe([vals['partner_id']]) + if vals.get('assistance_user_id'): + # need to change description of activity also so unlink old and create new activity + self.activity_unlink(['odex25_helpdesk.mail_act_odex25_helpdesk_assistance']) + self.activity_update() + + # SLA business + sla_triggers = self._sla_reset_trigger() + if any(field_name in sla_triggers for field_name in vals.keys()): + self.sudo()._sla_apply(keep_reached=True) + if 'stage_id' in vals: + self.sudo()._sla_reach(vals['stage_id']) + + if 'stage_id' in vals or 'user_id' in vals: + self.filtered(lambda ticket: ticket.user_id).sudo()._sla_assigning_reach() + + return res + + # ------------------------------------------------------------ + # Actions and Business methods + # ------------------------------------------------------------ + + @api.model + def _sla_reset_trigger(self): + """ Get the list of field for which we have to reset the SLAs (regenerate) """ + return ['team_id', 'priority', 'service_id', 'tag_ids'] + + def _sla_apply(self, keep_reached=False): + """ Apply SLA to current tickets: erase the current SLAs, then find and link the new SLAs to each ticket. + Note: transferring ticket to a team "not using SLA" (but with SLAs defined), SLA status of the ticket will be + erased but nothing will be recreated. + :returns recordset of new odex25_helpdesk.sla.status applied on current tickets + """ + # get SLA to apply + sla_per_tickets = self._sla_find() + + # generate values of new sla status + sla_status_value_list = [] + for tickets, slas in sla_per_tickets.items(): + sla_status_value_list += tickets._sla_generate_status_values(slas, keep_reached=keep_reached) + + sla_status_to_remove = self.mapped('sla_status_ids') + if keep_reached: # keep only the reached one to avoid losing reached_date info + sla_status_to_remove = sla_status_to_remove.filtered(lambda status: not status.reached_datetime) + + # if we are going to recreate many sla.status, then add norecompute to avoid 2 recomputation (unlink + recreate). Here, + # `norecompute` will not trigger recomputation. It will be done on the create multi (if value list is not empty). + if sla_status_value_list: + sla_status_to_remove.with_context(norecompute=True) + + # unlink status and create the new ones in 2 operations (recomputation optimized) + sla_status_to_remove.unlink() + return self.env['odex25_helpdesk.sla.status'].create(sla_status_value_list) + + def _sla_find(self): + """ Find the SLA to apply on the current tickets + :returns a map with the tickets linked to the SLA to apply on them + :rtype : dict {: } + """ + tickets_map = {} + sla_domain_map = {} + + def _generate_key(ticket): + """ Return a tuple identifying the combinaison of field determining the SLA to apply on the ticket """ + fields_list = self._sla_reset_trigger() + key = list() + for field_name in fields_list: + if ticket._fields[field_name].type == 'many2one': + key.append(ticket[field_name].id) + else: + key.append(ticket[field_name]) + return tuple(key) + + for ticket in self: + if ticket.team_id.use_sla: # limit to the team using SLA + key = _generate_key(ticket) + # group the ticket per key + tickets_map.setdefault(key, self.env['odex25_helpdesk.ticket']) + tickets_map[key] |= ticket + # group the SLA to apply, by key + if key not in sla_domain_map: + sla_domain_map[key] = [ + ('team_id', '=', ticket.team_id.id), ('priority', '<=', ticket.priority), + '|', + '&', ('stage_id.sequence', '>=', ticket.stage_id.sequence), ('target_type', '=', 'stage'), + ('target_type', '=', 'assigning'), + '|', ('service_id', '=', ticket.service_id.id), ('service_id', '=', False)] + + result = {} + for key, tickets in tickets_map.items(): # only one search per ticket group + domain = sla_domain_map[key] + slas = self.env['odex25_helpdesk.sla'].search(domain) + result[tickets] = slas.filtered(lambda s: s.tag_ids <= tickets.tag_ids) # SLA to apply on ticket subset + return result + + def _sla_generate_status_values(self, slas, keep_reached=False): + """ Return the list of values for given SLA to be applied on current ticket """ + status_to_keep = dict.fromkeys(self.ids, list()) + + # generate the map of status to keep by ticket only if requested + if keep_reached: + for ticket in self: + for status in ticket.sla_status_ids: + if status.reached_datetime: + status_to_keep[ticket.id].append(status.sla_id.id) + + # create the list of value, and maybe exclude the existing ones + result = [] + for ticket in self: + for sla in slas: + if not (keep_reached and sla.id in status_to_keep[ticket.id]): + if sla.target_type == 'stage' and ticket.stage_id == sla.stage_id: + # in case of SLA of type stage and on first stage + reached_datetime = fields.Datetime.now() + elif sla.target_type == 'assigning' and ( + not sla.stage_id or ticket.stage_id == sla.stage_id) and ticket.user_id: + # in case of SLA of type assigning and ticket is already assigned + reached_datetime = fields.Datetime.now() + else: + reached_datetime = False + result.append({ + 'ticket_id': ticket.id, + 'sla_id': sla.id, + 'reached_datetime': reached_datetime + }) + + return result + + def _sla_assigning_reach(self): + """ Flag the SLA status of current ticket for the given stage_id as reached, and even the unreached SLA applied + on stage having a sequence lower than the given one. + """ + self.env['odex25_helpdesk.sla.status'].search([ + ('ticket_id', 'in', self.ids), + ('reached_datetime', '=', False), + ('deadline', '!=', False), + ('target_type', '=', 'assigning') + ]).write({'reached_datetime': fields.Datetime.now()}) + + def _sla_reach(self, stage_id): + """ Flag the SLA status of current ticket for the given stage_id as reached, and even the unreached SLA applied + on stage having a sequence lower than the given one. + """ + stage = self.env['odex25_helpdesk.stage'].browse(stage_id) + stages = self.env['odex25_helpdesk.stage'].search([('sequence', '<=', stage.sequence), ( + 'team_ids', 'in', self.mapped('team_id').ids)]) # take previous stages + self.env['odex25_helpdesk.sla.status'].search([ + ('ticket_id', 'in', self.ids), + ('sla_stage_id', 'in', stages.ids), + ('reached_datetime', '=', False), + ('target_type', '=', 'stage') + ]).write({'reached_datetime': fields.Datetime.now()}) + + # For all SLA of type assigning, we compute deadline if they are not succeded (is succeded = has a reach_datetime) + # and if they are linked to a specific stage. + self.env['odex25_helpdesk.sla.status'].search([ + ('ticket_id', 'in', self.ids), + ('sla_stage_id', '!=', False), + ('reached_datetime', '=', False), + ('target_type', '=', 'assigning') + ])._compute_deadline() + + def assign_ticket_to_self(self): + self.ensure_one() + self.user_id = self.env.user + + def open_customer_tickets(self): + return { + 'type': 'ir.actions.act_window', + 'name': _('Customer Tickets'), + 'res_model': 'odex25_helpdesk.ticket', + 'view_mode': 'kanban,tree,form,pivot,graph', + 'context': {'search_default_is_open': True, 'search_default_partner_id': self.partner_id.id} + } + + def action_get_attachment_tree_view(self): + attachment_action = self.env.ref('base.action_attachment') + action = attachment_action.read()[0] + action['domain'] = str(['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)]) + return action + + # ------------------------------------------------------------ + # Messaging API + # ------------------------------------------------------------ + + # DVE FIXME: if partner gets created when sending the message it should be set as partner_id of the ticket. + def _message_get_suggested_recipients(self): + recipients = super(odex25_helpdeskTicket, self)._message_get_suggested_recipients() + try: + for ticket in self: + if ticket.partner_id and ticket.partner_id.email: + ticket._message_add_suggested_recipient(recipients, partner=ticket.partner_id, reason=_('Customer')) + elif ticket.partner_email: + ticket._message_add_suggested_recipient(recipients, email=ticket.partner_email, + reason=_('Customer Email')) + except AccessError: # no read access rights -> just ignore suggested recipients because this implies modifying followers + pass + return recipients + + def _ticket_email_split(self, msg): + email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) + # check left-part is not already an alias + return [ + x for x in email_list + if x.split('@')[0] not in self.mapped('team_id.alias_name') + ] + + @api.model + def message_new(self, msg, custom_values=None): + values = dict(custom_values or {}, partner_email=msg.get('from'), partner_id=msg.get('author_id')) + ticket = super(odex25_helpdeskTicket, self.with_context(mail_notify_author=True)).message_new(msg, + custom_values=values) + partner_ids = [x.id for x in + self.env['mail.thread']._mail_find_partner_from_emails(self._ticket_email_split(msg), + records=ticket) if x] + customer_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails( + tools.email_split(values['partner_email']), records=ticket) if p] + partner_ids += customer_ids + if customer_ids and not values.get('partner_id'): + ticket.partner_id = customer_ids[0] + if partner_ids: + ticket.message_subscribe(partner_ids) + return ticket + + def message_update(self, msg, update_vals=None): + partner_ids = [x.id for x in + self.env['mail.thread']._mail_find_partner_from_emails(self._ticket_email_split(msg), + records=self) if x] + if partner_ids: + self.message_subscribe(partner_ids) + return super(odex25_helpdeskTicket, self).message_update(msg, update_vals=update_vals) + + def _message_post_after_hook(self, message, msg_vals): + if self.partner_email and self.partner_id and not self.partner_id.email: + self.partner_id.email = self.partner_email + + if self.partner_email and not self.partner_id: + # we consider that posting a message with a specified recipient (not a follower, a specific one) + # on a document without customer means that it was created through the chatter using + # suggested recipients. This heuristic allows to avoid ugly hacks in JS. + new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email) + if new_partner: + self.search([ + ('partner_id', '=', False), + ('partner_email', '=', new_partner.email), + ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id}) + return super(odex25_helpdeskTicket, self)._message_post_after_hook(message, msg_vals) + + def _track_template(self, changes): + res = super(odex25_helpdeskTicket, self)._track_template(changes) + ticket = self[0] + if 'stage_id' in changes and ticket.stage_id.template_id: + res['stage_id'] = (ticket.stage_id.template_id, { + 'auto_delete_message': True, + 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), + 'email_layout_xmlid': 'mail.mail_notification_light' + } + ) + return res + + def _creation_subtype(self): + return self.env.ref('odex25_helpdesk.mt_ticket_new') + + def _track_subtype(self, init_values): + self.ensure_one() + if 'stage_id' in init_values: + return self.env.ref('odex25_helpdesk.mt_ticket_stage') + return super(odex25_helpdeskTicket, self)._track_subtype(init_values) + + def _notify_get_groups(self, msg_vals=None): + """ Handle Helpdesk users and managers recipients that can assign + tickets directly from notification emails. Also give access button + to portal and portal customers. If they are notified they should + probably have access to the document. """ + groups = super(odex25_helpdeskTicket, self)._notify_get_groups() + msg_vals = msg_vals or {} + + self.ensure_one() + for group_name, group_method, group_data in groups: + if group_name != 'customer': + group_data['has_button_access'] = True + + if self.user_id: + return groups + + take_action = self._notify_get_action_link('assign', **msg_vals) + odex25_helpdesk_actions = [{'url': take_action, 'title': _('Assign to me')}] + odex25_helpdesk_user_group_id = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id + new_groups = [( + 'group_odex25_helpdesk_user', + lambda pdata: pdata['type'] == 'user' and odex25_helpdesk_user_group_id in pdata['groups'], + {'actions': odex25_helpdesk_actions} + )] + return new_groups + groups + + def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): + """ Override to set alias of tickets to their team if any. """ + aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company, + doc_names=None) + res = {ticket.id: aliases.get(ticket.team_id.id) for ticket in self} + leftover = self.filtered(lambda rec: not rec.team_id) + if leftover: + res.update(super(odex25_helpdeskTicket, leftover)._notify_get_reply_to(default=default, records=None, + company=company, + doc_names=doc_names)) + return res + + # ------------------------------------------------------------ + # Rating Mixin + # ------------------------------------------------------------ + + def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None): + return super(odex25_helpdeskTicket, self).rating_apply(rate, token=token, feedback=feedback, + subtype_xmlid="odex25_helpdesk.mt_ticket_rated") + + def _rating_get_parent_field_name(self): + return 'team_id' diff --git a/odex25_helpdesk/odex25_helpdesk/models/res_partner.py b/odex25_helpdesk/odex25_helpdesk/models/res_partner.py new file mode 100644 index 000000000..da829178f --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/res_partner.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + ticket_count = fields.Integer("Tickets", compute='_compute_ticket_count') + + def _compute_ticket_count(self): + # retrieve all children partners and prefetch 'parent_id' on them + all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)]) + all_partners.read(['parent_id']) + + # group tickets by partner, and account for each partner in self + groups = self.env['odex25_helpdesk.ticket'].read_group( + [('partner_id', 'in', all_partners.ids)], + fields=['partner_id'], groupby=['partner_id'], + ) + self.ticket_count = 0 + for group in groups: + partner = self.browse(group['partner_id'][0]) + while partner: + if partner in self: + partner.ticket_count += group['partner_id_count'] + partner = partner.parent_id + + def action_open_odex25_helpdesk_ticket(self): + action = self.env["ir.actions.actions"]._for_xml_id("odex25_helpdesk.odex25_helpdesk_ticket_action_main_tree") + action['context'] = {} + action['domain'] = [('partner_id', 'child_of', self.ids)] + return action diff --git a/odex25_helpdesk/odex25_helpdesk/models/res_setting.py b/odex25_helpdesk/odex25_helpdesk/models/res_setting.py new file mode 100644 index 000000000..0e27c1138 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/res_setting.py @@ -0,0 +1,40 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError, UserError + +class odex25_helpdeskSLA(models.Model): + _inherit = "odex25_helpdesk.sla" + + category_id = fields.Many2one('service.category') + service_id = fields.Many2one('helpdesk.service') + + + +class ServiceCategory(models.Model): + _name = 'service.category' + _description = 'Service Category' + + name = fields.Char('Service Category', required=True) + + @api.constrains('name') + def unique_service_category_constrains(self): + if self.name: + parties_party = self.env['service.category'].search([('name', '=', self.name), ('id', '!=', self.id)]) + if parties_party: + raise ValidationError(_('The Service Category Must Be Unique')) + + +class HelpdeskService(models.Model): + _name = 'helpdesk.service' + _description = 'Helpdesk Service' + + name = fields.Char('Service', required=True) + category_id = fields.Many2one('service.category') + priority = fields.Selection([('0', 'All'), ('1', 'Low priority'), ('2', 'High priority'), ('3', 'Urgent')], + string='Priority', default='0') + + @api.constrains('name') + def unique_helpdesk_service_constrains(self): + if self.name: + parties_party = self.env['helpdesk.service'].search([('name', '=', self.name), ('id', '!=', self.id)]) + if parties_party: + raise ValidationError(_('The Helpdesk Service Must Be Unique')) diff --git a/odex25_helpdesk/odex25_helpdesk/models/res_users.py b/odex25_helpdesk/odex25_helpdesk/models/res_users.py new file mode 100644 index 000000000..41e366df9 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/models/res_users.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +from odoo import fields, models, api + + +class ResUsers(models.Model): + _inherit = 'res.users' + + odex25_helpdesk_target_closed = fields.Float(string='Target Tickets to Close', default=1) + odex25_helpdesk_target_rating = fields.Float(string='Target Customer Rating', default=100) + odex25_helpdesk_target_success = fields.Float(string='Target Success Rate', default=100) + + _sql_constraints = [ + ('target_closed_not_zero', 'CHECK(odex25_helpdesk_target_closed > 0)', 'You cannot have negative targets'), + ('target_rating_not_zero', 'CHECK(odex25_helpdesk_target_rating > 0)', 'You cannot have negative targets'), + ('target_success_not_zero', 'CHECK(odex25_helpdesk_target_success > 0)', 'You cannot have negative targets'), + ] + + def __init__(self, pool, cr): + """ Override of __init__ to add access rights. + Access rights are disabled by default, but allowed + on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. + """ + init_res = super(ResUsers, self).__init__(pool, cr) + odex25_helpdesk_fields = [ + 'odex25_helpdesk_target_closed', + 'odex25_helpdesk_target_rating', + 'odex25_helpdesk_target_success', + ] + # duplicate list to avoid modifying the original reference + type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) + type(self).SELF_WRITEABLE_FIELDS.extend(odex25_helpdesk_fields) + # duplicate list to avoid modifying the original reference + type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS) + type(self).SELF_READABLE_FIELDS.extend(odex25_helpdesk_fields) + return init_res + + +class GroupsView(models.Model): + _inherit = 'res.groups' + + def get_application_groups(self, domain): + group_helpdesk_user = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user', raise_if_not_found=False) + group_helpdesk_admin = self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager', raise_if_not_found=False) + if group_helpdesk_admin and group_helpdesk_user: + return super().get_application_groups( + domain + [('id', 'not in', (group_helpdesk_user.id, group_helpdesk_admin.id))]) + return super(GroupsView, self).get_application_groups(domain) diff --git a/odex25_helpdesk/odex25_helpdesk/report/__init__.py b/odex25_helpdesk/odex25_helpdesk/report/__init__.py new file mode 100644 index 000000000..32d83819d --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/report/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import odex25_helpdesk_sla_report_analysis diff --git a/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis.py b/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis.py new file mode 100644 index 000000000..e0b30f9c4 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, tools +from odoo.addons.odex25_helpdesk.models.odex25_helpdesk_ticket import TICKET_PRIORITY + + +class odex25_helpdeskSLAReport(models.Model): + _name = 'odex25_helpdesk.sla.report.analysis' + _description = "SLA Status Analysis" + _auto = False + _order = 'create_date DESC' + + ticket_id = fields.Many2one('odex25_helpdesk.ticket', string='Ticket', readonly=True) + create_date = fields.Date("Ticket Create Date", readonly=True) + priority = fields.Selection(TICKET_PRIORITY, string='Minimum Priority', readonly=True) + user_id = fields.Many2one('res.users', string="Assigned To", readonly=True) + partner_id = fields.Many2one('res.partner', string="Customer", readonly=True) + ticket_type_id = fields.Many2one('odex25_helpdesk.ticket.type', string="Ticket Type", readonly=True) + ticket_stage_id = fields.Many2one('odex25_helpdesk.stage', string="Ticket Stage", readonly=True) + ticket_deadline = fields.Datetime("Ticket Deadline", readonly=True) + ticket_failed = fields.Boolean("Ticket Failed", group_operator="bool_or", readonly=True) + ticket_closed = fields.Boolean("Ticket Closed", readonly=True) + ticket_close_hours = fields.Integer("Time to close (hours)", group_operator="avg", readonly=True) + ticket_open_hours = fields.Integer("Open Time (hours)", group_operator="avg", readonly=True) + ticket_assignation_hours = fields.Integer("Time to first assignment (hours)", group_operator="avg", readonly=True) + + sla_id = fields.Many2one('odex25_helpdesk.sla', string="SLA", readonly=True) + sla_stage_id = fields.Many2one('odex25_helpdesk.stage', string="SLA Stage", readonly=True) + sla_deadline = fields.Datetime("SLA Deadline", group_operator='min', readonly=True) + sla_reached_datetime = fields.Datetime("SLA Reached Date", group_operator='min', readonly=True) + sla_status = fields.Selection([('failed', 'Failed'), ('reached', 'Reached'), ('ongoing', 'Ongoing')], string="Status", readonly=True) + sla_status_failed = fields.Boolean("SLA Status Failed", group_operator='bool_or', readonly=True) + sla_exceeded_days = fields.Integer("Day to reach SLA", group_operator='avg', readonly=True, help="Day to reach the stage of the SLA, without taking the working calendar into account") + + team_id = fields.Many2one('odex25_helpdesk.team', string='Team', readonly=True) + company_id = fields.Many2one('res.company', string='Company', readonly=True) + + def init(self): + tools.drop_view_if_exists(self.env.cr, 'odex25_helpdesk_sla_report_analysis') + self.env.cr.execute(""" + CREATE VIEW odex25_helpdesk_sla_report_analysis AS ( + SELECT + ST.id as id, + T.create_date AS create_date, + T.id AS ticket_id, + T.team_id, + T.stage_id AS ticket_stage_id, + T.ticket_type_id, + T.user_id, + T.partner_id, + T.company_id, + T.priority AS priority, + T.sla_reached_late OR T.sla_deadline < NOW() AT TIME ZONE 'UTC' AS ticket_failed, + T.sla_deadline AS ticket_deadline, + T.close_hours AS ticket_close_hours, + EXTRACT(HOUR FROM (COALESCE(T.assign_date, NOW()) - T.create_date)) AS ticket_open_hours, + T.assign_hours AS ticket_assignation_hours, + STA.is_close AS ticket_closed, + ST.sla_id, + SLA.stage_id AS sla_stage_id, + ST.deadline AS sla_deadline, + ST.reached_datetime AS sla_reached_datetime, + ST.exceeded_days AS sla_exceeded_days, + CASE + WHEN ST.reached_datetime IS NOT NULL AND ST.reached_datetime < ST.deadline THEN 'reached' + WHEN ST.reached_datetime IS NOT NULL AND ST.reached_datetime >= ST.deadline THEN 'failed' + WHEN ST.reached_datetime IS NULL AND ST.deadline > NOW() THEN 'ongoing' + ELSE 'failed' + END AS sla_status, + ST.reached_datetime >= ST.deadline OR (ST.reached_datetime IS NULL AND ST.deadline < NOW() AT TIME ZONE 'UTC') AS sla_status_failed + FROM odex25_helpdesk_ticket T + LEFT JOIN odex25_helpdesk_stage STA ON (T.stage_id = STA.id) + LEFT JOIN odex25_helpdesk_sla_status ST ON (T.id = ST.ticket_id) + LEFT JOIN odex25_helpdesk_sla SLA ON (ST.sla_id = SLA.id) + ) + """) diff --git a/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis_views.xml b/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis_views.xml new file mode 100644 index 000000000..fc226ad56 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/report/odex25_helpdesk_sla_report_analysis_views.xml @@ -0,0 +1,100 @@ + + + + + odex25_helpdesk.sla.report.analysis.pivot + odex25_helpdesk.sla.report.analysis + + + + + + + + + + + + odex25_helpdesk.sla.report.analysis.graph + odex25_helpdesk.sla.report.analysis + + + + + + + + + + odex25_helpdesk.sla.report.analysis.search + odex25_helpdesk.sla.report.analysis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SLA Status Analysis + odex25_helpdesk.sla.report.analysis + pivot,graph + + {'search_default_last_7days': 1} + +

+ No data yet ! +

+ Create tickets to get statistics. +

+
+
+ + + + pivot + + + + + + + graph + + + + + + +
diff --git a/odex25_helpdesk/odex25_helpdesk/security/ir.model.access.csv b/odex25_helpdesk/odex25_helpdesk/security/ir.model.access.csv new file mode 100644 index 000000000..cb34efd9c --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/security/ir.model.access.csv @@ -0,0 +1,21 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_odex25_helpdesk_tag,odex25_helpdesk.tag,model_odex25_helpdesk_tag,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1 +access_odex25_helpdesk_sla,odex25_helpdesk.sla,model_odex25_helpdesk_sla,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0 +access_odex25_helpdesk_sla_status,odex25_helpdesk.sla.status,model_odex25_helpdesk_sla_status,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0 +access_odex25_helpdesk_sla_manager,odex25_helpdesk.sla.manager,model_odex25_helpdesk_sla,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 +access_odex25_helpdesk_stage,odex25_helpdesk.stage,model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0 +access_odex25_helpdesk_stage_manager,odex25_helpdesk.stage.manager,model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 +access_odex25_helpdesk_stage_portal,odex25_helpdesk.stage.portal,odex25_helpdesk.model_odex25_helpdesk_stage,base.group_portal,1,0,0,0 +access_odex25_helpdesk_ticket_portal,odex25_helpdesk.ticket.portal,odex25_helpdesk.model_odex25_helpdesk_ticket,base.group_portal,1,0,0,0 +access_odex25_helpdesk_ticket,odex25_helpdesk.ticket,model_odex25_helpdesk_ticket,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1 +access_odex25_helpdesk_team_public,odex25_helpdesk.team,model_odex25_helpdesk_team,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0 +access_odex25_helpdesk_team_no_group,odex25_helpdesk.team,model_odex25_helpdesk_team,,1,0,0,0 +access_odex25_helpdesk_team_portal,odex25_helpdesk.team.portal,odex25_helpdesk.model_odex25_helpdesk_team,base.group_portal,1,0,0,0 +access_odex25_helpdesk_team_manager,odex25_helpdesk.team.manager,model_odex25_helpdesk_team,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 +access_odex25_helpdesk_ticket_type_user,odex25_helpdesk.ticket.type.user,model_odex25_helpdesk_ticket_type,odex25_helpdesk.group_odex25_helpdesk_user,1,0,0,0 +access_odex25_helpdesk_ticket_type_manager,odex25_helpdesk.ticket.type.manager,model_odex25_helpdesk_ticket_type,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 +access_odex25_helpdesk_sla_report_analysis,access_odex25_helpdesk_sla_report_analysis,model_odex25_helpdesk_sla_report_analysis,odex25_helpdesk.group_odex25_helpdesk_manager,1,0,0,0 +access_mail_activity_type_odex25_helpdesk_manager,mail.activity.type.odex25_helpdesk.manager,mail.model_mail_activity_type,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 + +access_zfp_helpdesk_service_category,zfp_helpdesk.zfp_helpdesk,odex25_helpdesk.model_service_category,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1 +access_zfp_helpdesk_service,zfp_helpdesk.zfp_helpdesk,odex25_helpdesk.model_helpdesk_service,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1 \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk/security/odex25_helpdesk_security.xml b/odex25_helpdesk/odex25_helpdesk/security/odex25_helpdesk_security.xml new file mode 100644 index 000000000..704dbfbf8 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/security/odex25_helpdesk_security.xml @@ -0,0 +1,120 @@ + + + + User + + + + + Administrator + + + + + + + + Assignment User + + + + + + On Behalf User + + + + + + Show SLA Policies + + + + + + + + + + + Helpdesk Administrator + + [(1,'=',1)] + + + + + + + Helpdesk User + + ['|', ('visibility_member_ids','in', user.id), ('visibility_member_ids','=', + False)] + + + + + Helpdesk Ticket User + + ['|', ('team_id.visibility_member_ids','in', user.id), + ('team_id.visibility_member_ids','=', False)] + + + + + + Helpdesk Ticket Managers + + [(1,'=',1)] + + + + + Project: tickets multi-company + + ['|', + ('company_id', '=', False), + ('company_id', 'in', company_ids) + ] + + + + Project: teams multi-company + + ['|', + ('company_id', '=', False), + ('company_id', 'in', company_ids), + ] + + + + Project: multi-company + + ['|', + ('company_id', '=', False), + ('company_id', 'in', company_ids), + ] + + + + Tickets: portal users: portal or following + + [ + '|', + ('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), + ('message_partner_ids', 'in', [user.partner_id.id]) + ] + + + + + + Helpdesk SLA Report: multi-company + + ['|', ('company_id', '=', False), ('company_id', 'child_of', + [user.company_id.id])] + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/static/description/icon.png b/odex25_helpdesk/odex25_helpdesk/static/description/icon.png new file mode 100644 index 000000000..971298382 Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk/static/description/icon.png differ diff --git a/odex25_helpdesk/odex25_helpdesk/static/description/icon.svg b/odex25_helpdesk/odex25_helpdesk/static/description/icon.svg new file mode 100644 index 000000000..361bc6ad3 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/description/icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/css/portal_odex25_helpdesk.css b/odex25_helpdesk/odex25_helpdesk/static/src/css/portal_odex25_helpdesk.css new file mode 100644 index 000000000..c07ea6c24 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/src/css/portal_odex25_helpdesk.css @@ -0,0 +1,11 @@ +.oe_website_rating_team .oe_rate{ + clear: both; +} +.oe_website_rating_team .oe_rate img { + margin-right: 6px; +} + +.oe_website_rating_team .thumbnail{ + height: 355px; +} + diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/img/menu-navigation.gif b/odex25_helpdesk/odex25_helpdesk/static/src/img/menu-navigation.gif new file mode 100644 index 000000000..9c786bfd6 Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk/static/src/img/menu-navigation.gif differ diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js b/odex25_helpdesk/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js new file mode 100644 index 000000000..4eea2e01e --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/src/js/odex25_helpdesk_dashboard.js @@ -0,0 +1,264 @@ +odoo.define('odex25_helpdesk.dashboard', function (require) { +"use strict"; + +/** + * This file defines the Helpdesk Dashboard view (alongside its renderer, model + * and controller), extending the Kanban view. + * The Helpdesk Dashboard view is registered to the view registry. + * A large part of this code should be extracted in an AbstractDashboard + * widget in web, to avoid code duplication (see SalesTeamDashboard). + */ + +var core = require('web.core'); +var KanbanController = require('web.KanbanController'); +var KanbanModel = require('web.KanbanModel'); +var KanbanRenderer = require('web.KanbanRenderer'); +var KanbanView = require('web.KanbanView'); +var session = require('web.session'); +var view_registry = require('web.view_registry'); + +var QWeb = core.qweb; + +var _t = core._t; +var _lt = core._lt; + +var odex25_helpdeskDashboardRenderer = KanbanRenderer.extend({ + events: _.extend({}, KanbanRenderer.prototype.events, { + 'click .o_dashboard_action': '_onDashboardActionClicked', + 'click .o_target_to_set': '_onDashboardTargetClicked', + }), + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Notifies the controller that the target has changed. + * + * @private + * @param {string} target_name the name of the changed target + * @param {string} value the new value + */ + _notifyTargetChange: function (target_name, value) { + this.trigger_up('dashboard_edit_target', { + target_name: target_name, + target_value: value, + }); + }, + + /** + * @override + * @private + * @returns {Promise} + */ + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + var values = self.state.dashboardValues; + var odex25_helpdesk_dashboard = QWeb.render('odex25_helpdesk.odex25_helpdeskDashboard', { + widget: self, + show_demo: values.show_demo, + rating_enable: values.rating_enable, + success_rate_enable: values.success_rate_enable, + values: values, + }); + self.$el.prepend(odex25_helpdesk_dashboard); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} + */ + _onDashboardActionClicked: function (e) { + var self = this; + e.preventDefault(); + var $action = $(e.currentTarget); + var action_ref = $action.attr('name'); + var title = $action.attr('title'); + var search_view_ref = $action.attr('search_view_ref'); + if ($action.attr('show_demo') != 'true'){ + if ($action.attr('name').includes("odex25_helpdesk.")) { + this._rpc({ + model: 'odex25_helpdesk.ticket', + method: 'create_action', + args: [action_ref, title, search_view_ref], + }).then(function (result) { + if (result.action) { + self.do_action(result.action, { + additional_context: $action.attr('context') + }); + } + }); + } + else { + this.trigger_up('dashboard_open_action', { + action_name: $action.attr('name'), + }); + } + } + }, + + /** + * @private + * @param {MouseEvent} + */ + _onDashboardTargetClicked: function (e) { + var self = this; + var $target = $(e.currentTarget); + var target_name = $target.attr('name'); + var target_value = $target.attr('value'); + + var $input = $('', {type: "text", name: target_name}); + if (target_value) { + $input.attr('value', target_value); + } + $input.on('keyup input', function (e) { + if (e.which === $.ui.keyCode.ENTER) { + self._notifyTargetChange(target_name, $input.val()); + } + }); + $input.on('blur', function () { + self._notifyTargetChange(target_name, $input.val()); + }); + $input.replaceAll($target) + .focus() + .select(); + }, +}); + +var odex25_helpdeskDashboardModel = KanbanModel.extend({ + /** + * @override + */ + init: function () { + this.dashboardValues = {}; + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + __get: function (localID) { + var result = this._super.apply(this, arguments); + if (_.isObject(result)) { + result.dashboardValues = this.dashboardValues[localID]; + } + return result; + }, + /** + * @œverride + * @returns {Promise} + */ + __load: function () { + return this._loadDashboard(this._super.apply(this, arguments)); + }, + /** + * @œverride + * @returns {Promise} + */ + __reload: function () { + return this._loadDashboard(this._super.apply(this, arguments)); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Promise} super_def a promise that resolves with a dataPoint id + * @returns {Promise -> string} resolves to the dataPoint id + */ + _loadDashboard: function (super_def) { + var self = this; + var dashboard_def = this._rpc({ + model: 'odex25_helpdesk.team', + method: 'retrieve_dashboard', + }); + return Promise.all([super_def, dashboard_def]).then(function(results) { + var id = results[0]; + var dashboardValues = results[1]; + self.dashboardValues[id] = dashboardValues; + return id; + }); + }, +}); + +var odex25_helpdeskDashboardController = KanbanController.extend({ + custom_events: _.extend({}, KanbanController.prototype.custom_events, { + dashboard_open_action: '_onDashboardOpenAction', + dashboard_edit_target: '_onDashboardEditTarget', + }), + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} e + */ + _onDashboardEditTarget: function (e) { + var target_name = e.data.target_name; + var target_value = e.data.target_value; + if (isNaN(target_value)) { + this.do_warn(false, _t("Please enter an integer value")); + } else { + var values = {}; + values[target_name] = parseInt(target_value); + this._rpc({ + model: 'res.users', + method: 'write', + args: [[session.uid], values], + }) + .then(this.reload.bind(this)); + } + }, + /** + * @private + * @param {OdooEvent} e + */ + _onDashboardOpenAction: function (e) { + var self = this; + var action_name = e.data.action_name; + if (_.contains(['action_view_rating_today', 'action_view_rating_7days'], action_name)) { + return this._rpc({model: this.modelName, method: action_name}) + .then(function (data) { + if (data) { + return self.do_action(data); + } + }); + } + return this.do_action(action_name); + }, +}); + +var odex25_helpdeskDashboardView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Model: odex25_helpdeskDashboardModel, + Renderer: odex25_helpdeskDashboardRenderer, + Controller: odex25_helpdeskDashboardController, + }), + display_name: _lt('Dashboard'), + icon: 'fa-dashboard', + searchview_hidden: true, +}); + +view_registry.add('odex25_helpdesk_dashboard', odex25_helpdeskDashboardView); + +return { + Model: odex25_helpdeskDashboardModel, + Renderer: odex25_helpdeskDashboardRenderer, + Controller: odex25_helpdeskDashboardController, +}; + +}); diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js b/odex25_helpdesk/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js new file mode 100644 index 000000000..cd01e62c4 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/src/js/tours/odex25_helpdesk.js @@ -0,0 +1,75 @@ +odoo.define('odex25_helpdesk.tour', function(require) { +"use strict"; + +var core = require('web.core'); +var tour = require('web_tour.tour'); + +var _t = core._t; + +tour.register('odex25_helpdesk_tour', { + url: "/web", +}, [{ + trigger: '.o_app[data-menu-xmlid="odex25_helpdesk.menu_odex25_helpdesk_root"]', + content: _t('Want to boost your customer satisfaction?
Click Helpdesk to start.'), + position: 'bottom', +}, { + trigger: '.oe_kanban_action_button', + extra_trigger: '.o_kanban_primary_left', + content: _t('Click here to view this team\'s tickets.'), + position: 'bottom', + width: 200, +}, { + trigger: '.o-kanban-button-new', + extra_trigger: '.o_kanban_odex25_helpdesk_ticket', + content: _t('Let\'s create your first ticket.'), + position: 'bottom', + width: 200, +}, { + trigger: 'input.field_name', + extra_trigger: '.o_form_editable', + content: _t('Enter a subject or title for this ticket.
(e.g. Problem with installation, Wrong order, Can\'t understand bill, etc.)'), + position: 'right', +}, { + trigger: '.o_field_widget.field_partner_id', + extra_trigger: '.o_form_editable', + content: _t('Enter the customer. Feel free to create it on the fly.'), + position: 'top', +}, { + trigger: '.o_field_widget.field_user_id', + extra_trigger: '.o_form_editable', + content: _t('Assign the ticket to someone.'), + position: 'right', +}, { + trigger: '.o_form_button_save', + content: _t('Save this ticket and the modifications you\'ve made to it.'), + position: 'bottom', +}, { + trigger: '.o_back_button', + extra_trigger: '.o_form_view.o_form_readonly', + content: _t('Use the breadcrumbs to go back to the Kanban view.'), + position: 'bottom', +}, { + trigger: '.o_kanban_record', + content: _t('Click these cards to open their form view, or drag & drop them through the different stages of this team.'), + position: 'right', + run: "drag_and_drop .o_kanban_group:eq(2) ", +}, { + trigger: '.o_priority', + extra_trigger: '.o_kanban_record', + content: _t('Stars mark the ticket priority. You can change it directly from here!'), + position: 'bottom', + run: "drag_and_drop .o_kanban_group:eq(2) ", +}, { + trigger: ".o_column_quick_create .o_quick_create_folded", + content: _t('Add columns to configure stages for your tickets.
e.g. Awaiting Customer Feedback, Customer Followup, ...'), + position: 'right', +} +// TODO: Restore this step +// , { +// trigger: '.dropdown-toggle[data-menu-xmlid="odex25_helpdesk.odex25_helpdesk_menu_config"]', +// content: _t('Click here and select "Helpdesk Teams" for further configuration.'), +// position: 'bottom', +// } +]); + +}); diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/scss/odex25_helpdesk.scss b/odex25_helpdesk/odex25_helpdesk/static/src/scss/odex25_helpdesk.scss new file mode 100644 index 000000000..412f0177a --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/src/scss/odex25_helpdesk.scss @@ -0,0 +1,153 @@ +.o_kanban_view.o_kanban_dashboard.o_odex25_helpdesk_kanban { + $sale-table-spacing: 10px; + $odex25_helpdesk-record-width: 420px; + + .o_kanban_group { + width: $odex25_helpdesk-record-width + 2*$o-kanban-group-padding; + } + .o_kanban_record { + width: $odex25_helpdesk-record-width; + min-height: 250px; + } + + .o_odex25_helpdesk_dashboard { + @include o-position-sticky($top: 0px) + border-bottom: 1px solid #CED4DA; + background-color: $o-view-background-color; + z-index: 10; + + .o_welcome_message { + width: 100%; + @include o-position-absolute($left: 0, $top: 0); + display: flex; + justify-content: center; + + .o_welcome_image { + padding: 20px; + } + .o_welcome_content > a { + color: white; + display: inline-block; + } + } + + .o_dashboard_action { + cursor: pointer; + } + + .ribbon { + &::before, &::after { + display: none; + } + + span { + background-color: $o-brand-odoo; + padding: 5px; + font-size: medium; + z-index: unset; + height:auto; + } + } + + .ribbon-top-right { + margin-top: -$o-kanban-dashboard-vpadding; + + span { + left: 0px; + right: 30px; + } + } + + > .o_demo { + opacity: 0.07; + } + + > div { + display: inline-block; + vertical-align: top; + @include media-breakpoint-up(md) { + width: 50%; + } + + > table { + margin-bottom: 0; + table-layout: fixed; + border-spacing: $sale-table-spacing 0px; + border-collapse: separate; + > tbody > tr > td { + vertical-align: middle; + text-align: center; + border-top: 1px solid $o-view-background-color; + width: 25%; + + height: 33px; + + span { + display: inline; + } + + a:hover { + text-decoration: none; + } + + &.o_demo{ + cursor: default; + a { + cursor: default; + } + } + + &.o_main { + background-color: $o-brand-primary; + &:hover { + background-color: darken($o-brand-primary, 10%); + } + a { + color: white; + } + &.o_demo{ + &:hover { + background-color: $o-brand-primary; + } + } + } + &.o_warning { + background-color: orange; + &:hover { + background-color: darken(orange, 10%); + } + a { + color: white; + } + &.o_demo{ + &:hover { + background-color: orange; + } + } + } + &.o_secondary { + background-color: $o-brand-lightsecondary; + &:hover { + background-color: darken($o-brand-lightsecondary, 10%); + } + a { + color: black; + } + &.o_demo{ + &:hover { + background-color: $o-brand-lightsecondary; + } + } + } + &.o_highlight, .o_highlight { + font-size: 20px; + } + &.o_text { + text-align: left; + } + } + + } + } + } +} diff --git a/odex25_helpdesk/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml b/odex25_helpdesk/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml new file mode 100644 index 000000000..7278c53b1 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/src/xml/odex25_helpdesk_team_templates.xml @@ -0,0 +1,202 @@ + + + + +
+
+ + + +
+ Sample +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
My Tickets
+
+ +
+ Tickets +
+
+ +
+ High Priority () +
+
+ +
+ Urgent () +
+
Avg Open Hours + + + + + + + + + +
SLA Failed + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+
My Performance
+ Today +
+ + + +
+ Closed Tickets +
+
+ + + % +
+ Happy Rating +
+
+ + + + % +
+ Success Rate +
+
Avg 7 days + + + + + + % + + + + % + +
Daily Target + + + + + + Click to set + + + + + + % + + + Click to set + + + + + + % + + + Click to set + + +
+
+
+
+
diff --git a/odex25_helpdesk/odex25_helpdesk/static/tests/odex25_helpdesk_dashboard_tests.js b/odex25_helpdesk/odex25_helpdesk/static/tests/odex25_helpdesk_dashboard_tests.js new file mode 100644 index 000000000..5eb8ba7f4 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/static/tests/odex25_helpdesk_dashboard_tests.js @@ -0,0 +1,150 @@ +odoo.define('odex25_helpdesk.dashboard_tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); +var view_registry = require('web.view_registry'); + +var createView = testUtils.createView; + +QUnit.module('Views', {}, function () { + +QUnit.module('Helpdesk Dashboard', { + beforeEach: function() { + this.data = { + partner: { + fields: { + foo: {string: "Foo", type: "char"}, + }, + records: [ + {id: 1, foo: "yop"}, + {id: 2, foo: "blip"}, + {id: 3, foo: "gnap"}, + {id: 4, foo: "blip"}, + ] + }, + }; + this.dashboard_data = { + '7days': {count: 0, rating: 0, success: 0}, + odex25_helpdesk_target_closed: 12, + odex25_helpdesk_target_rating: 0, + odex25_helpdesk_target_success: 0, + my_all: {count: 0, hours: 0, failed: 0}, + my_high: {count: 0, hours: 0, failed: 0}, + my_urgent: {count: 0, hours: 0, failed: 0}, + rating_enable: false, + show_demo: false, + success_rate_enable: false, + today: {count: 0, rating: 0, success: 0}, + }; + } +}); + +QUnit.test('dashboard basic rendering', async function(assert) { + assert.expect(4); + + var dashboard_data = this.dashboard_data; + var kanban = await createView({ + View: view_registry.get('odex25_helpdesk_dashboard'), + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + mockRPC: function(route, args) { + if (args.method === 'retrieve_dashboard') { + assert.ok(true, "should call /retrieve_dashboard"); + return Promise.resolve(dashboard_data); + } + return this._super(route, args); + }, + }); + + assert.containsOnce(kanban, 'div.o_odex25_helpdesk_dashboard', + "should render the dashboard"); + assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), '12', + "should have written correct target"); + assert.hasAttrValue(kanban.$('.o_target_to_set'), 'value', '12', + "target's value is 12"); + kanban.destroy(); +}); + +QUnit.test('edit the target', async function(assert) { + assert.expect(6); + + var dashboard_data = this.dashboard_data; + dashboard_data.odex25_helpdesk_target_closed = 0; + var kanban = await createView({ + View: view_registry.get('odex25_helpdesk_dashboard'), + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + mockRPC: function(route, args) { + if (args.method === 'retrieve_dashboard') { + // should be called twice: for the first rendering, and after the target update + assert.ok(true, "should call /retrieve_dashboard"); + return Promise.resolve(dashboard_data); + } + if (args.model === 'res.users' && args.method === 'write') { + assert.ok(true, "should modify odex25_helpdesk_target_closed"); + dashboard_data.odex25_helpdesk_target_closed = args.args[1]['odex25_helpdesk_target_closed']; + return Promise.resolve(); + } + return this._super(route, args); + }, + }); + + assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), "Click to set", + "should have correct target"); + assert.ok(!kanban.$('.o_target_to_set').attr('value'), "should have no target"); + + // edit the target + await testUtils.dom.click(kanban.$('.o_target_to_set')); + await testUtils.fields.editAndTrigger(kanban.$('.o_odex25_helpdesk_dashboard input'), + 1200, [$.Event('keyup', {which: $.ui.keyCode.ENTER})]); // set the target + + assert.strictEqual(kanban.$('.o_target_to_set').text().trim(), "1200", + "should have correct target"); + kanban.destroy(); +}); + +QUnit.test('dashboard rendering with empty many2one', async function(assert) { + assert.expect(2); + + // add an empty many2one + this.data.partner.fields.partner_id = {string: "Partner", type: 'many2one', relation: 'partner'}; + this.data.partner.records[0].partner_id = false; + + var dashboard_data = this.dashboard_data; + var kanban = await createView({ + View: view_registry.get('odex25_helpdesk_dashboard'), + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
' + + '
', + mockRPC: function(route, args) { + if (args.method === 'retrieve_dashboard') { + assert.ok(true, "should call /retrieve_dashboard"); + return Promise.resolve(dashboard_data); + } + return this._super(route, args); + }, + }); + + assert.containsOnce(kanban, 'div.o_odex25_helpdesk_dashboard', + "should render the dashboard"); + kanban.destroy(); +}); + +}); + +}); diff --git a/odex25_helpdesk/odex25_helpdesk/tests/__init__.py b/odex25_helpdesk/odex25_helpdesk/tests/__init__.py new file mode 100644 index 000000000..f91800691 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import test_odex25_helpdesk_flow +from . import test_odex25_helpdesk_sla +from . import test_ui +from . import test_doc_links \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk/tests/common.py b/odex25_helpdesk/odex25_helpdesk/tests/common.py new file mode 100644 index 000000000..9a04c62a7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/tests/common.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from contextlib import contextmanager + +from odoo import fields +from odoo.tests.common import SavepointCase +from odoo import fields + + +class odex25_helpdeskCommon(SavepointCase): + + @classmethod + def setUpClass(cls): + super(odex25_helpdeskCommon, cls).setUpClass() + cls.env.user.tz = 'Europe/Brussels' + cls.env['resource.calendar'].search([]).write({'tz': 'Europe/Brussels'}) + + # we create a Helpdesk user and a manager + Users = cls.env['res.users'].with_context(tracking_disable=True) + cls.main_company_id = cls.env.ref('base.main_company').id + cls.odex25_helpdesk_manager = Users.create({ + 'company_id': cls.main_company_id, + 'name': 'Helpdesk Manager', + 'login': 'hm', + 'email': 'hm@example.com', + 'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id])], + 'tz': 'Europe/Brussels', + }) + cls.odex25_helpdesk_user = Users.create({ + 'company_id': cls.main_company_id, + 'name': 'Helpdesk User', + 'login': 'hu', + 'email': 'hu@example.com', + 'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id])], + 'tz': 'Europe/Brussels', + }) + # the manager defines a team for our tests (the .sudo() at the end is to avoid potential uid problems) + cls.test_team = cls.env['odex25_helpdesk.team'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Test Team'}).sudo() + # He then defines its stages + stage_as_manager = cls.env['odex25_helpdesk.stage'].with_user(cls.odex25_helpdesk_manager) + cls.stage_new = stage_as_manager.create({ + 'name': 'New', + 'sequence': 10, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': False, + }) + cls.stage_progress = stage_as_manager.create({ + 'name': 'In Progress', + 'sequence': 20, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': False, + }) + cls.stage_done = stage_as_manager.create({ + 'name': 'Done', + 'sequence': 30, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': True, + }) + cls.stage_cancel = stage_as_manager.create({ + 'name': 'Cancelled', + 'sequence': 40, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': True, + }) + + # He also creates a ticket types for Question and Issue + cls.type_question = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({ + 'name': 'Question_test', + }).sudo() + cls.type_issue = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({ + 'name': 'Issue_test', + }).sudo() + + @classmethod + def setUpSLATeam(cls): + """ Generate Team, some stage and SLAs for the team """ + # create team and stages + cls.team_with_sla = cls.env['odex25_helpdesk.team'].create({ + 'name': 'Team with SLAs', + 'use_sla': True + }) + + Stage = cls.env['odex25_helpdesk.stage'] + cls.team_sla_stage_new = Stage.create({ + 'name': 'New', + 'sequence': 10, + 'team_ids': [(4, cls.team_with_sla.id, 0)], + 'is_close': False, + }) + cls.team_sla_stage_progress = Stage.create({ + 'name': 'In Progress', + 'sequence': 20, + 'team_ids': [(4, cls.team_with_sla.id, 0)], + 'is_close': False, + }) + cls.team_sla_stage_done = Stage.create({ + 'name': 'Done', + 'sequence': 30, + 'team_ids': [(4, cls.team_with_sla.id, 0)], + 'is_close': True, + }) + cls.team_sla_stage_cancel = Stage.create({ + 'name': 'Cancelled', + 'sequence': 40, + 'team_ids': [(4, cls.team_with_sla.id, 0)], + 'is_close': True, + }) + + # create SLAs + SLA = cls.env['odex25_helpdesk.sla'] + cls.sla_1_progress = SLA.create({ + 'name': "2 days to be in progress", + 'stage_id': cls.team_sla_stage_progress.id, + 'time_days': 2, + 'team_id': cls.team_with_sla.id, + }) + cls.sla_2_done = SLA.create({ + 'name': "7 days to be in progress", + 'stage_id': cls.team_sla_stage_done.id, + 'time_days': 7, + 'team_id': cls.team_with_sla.id, + 'priority': '0', + }) + cls.sla_3_done_prior = SLA.create({ + 'name': "5 days to be in done for 3 stars ticket", + 'stage_id': cls.team_sla_stage_done.id, + 'time_days': 5, + 'team_id': cls.team_with_sla.id, + 'priority': '3', + }) + + def _utils_set_create_date(self, records, date_str): + """ This method is a hack in order to be able to define/redefine the create_date + of the any recordset. This is done in SQL because ORM does not allow to write + onto the create_date field. + :param records: recordset of any odoo models + """ + query = """ + UPDATE %s + SET create_date = %%s + WHERE id IN %%s + """ % (records._table,) + self.env.cr.execute(query, (date_str, tuple(records.ids))) + + records.invalidate_cache() + + if records._name == 'odex25_helpdesk.ticket': + field = self.env['odex25_helpdesk.sla.status']._fields['deadline'] + self.env.add_to_compute(field, records.sla_status_ids) + records.recompute() + + @contextmanager + def _ticket_patch_now(self, datetime_str): + datetime_now_old = getattr(fields.Datetime, 'now') + datetime_today_old = getattr(fields.Datetime, 'today') + + def new_now(): + return fields.Datetime.from_string(datetime_str) + + def new_today(): + return fields.Datetime.from_string(datetime_str).replace(hour=0, minute=0, second=0) + + try: + setattr(fields.Datetime, 'now', new_now) + setattr(fields.Datetime, 'today', new_today) + + yield + finally: + # back + setattr(fields.Datetime, 'now', datetime_now_old) + setattr(fields.Datetime, 'today', datetime_today_old) diff --git a/odex25_helpdesk/odex25_helpdesk/tests/test_doc_links.py b/odex25_helpdesk/odex25_helpdesk/tests/test_doc_links.py new file mode 100644 index 000000000..81961bf26 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/tests/test_doc_links.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import HttpCase, tagged +import re + + +@tagged('-standard', 'external', 'post_install', '-at_install') # nightly is not a real tag +class TestDocLinks(HttpCase): + """ + Parse the 'odex25_helpdesk.team' view to extract all documentation links and + check that every links are still valid. + """ + + def setUp(self): + """ + Set-up the test environment + """ + super(TestDocLinks, self).setUp() + self.re = re.compile(" +To: odex25_helpdesk_team_0@aqualung.com +Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" + +--000000000000a47519057e029630 +Content-Type: text/plain; charset="UTF-8" + + +--000000000000a47519057e029630 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
A good message
+ +--000000000000a47519057e029630-- +""" + + new_message1 = """MIME-Version: 1.0 +Date: Thu, 27 Dec 2018 16:27:45 +0100 +Message-ID: blablabla1 +Subject: Helpdesk team 1 in company 1 +From: B client +To: odex25_helpdesk_team_1@aqualung.com +Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" + +--000000000000a47519057e029630 +Content-Type: text/plain; charset="UTF-8" + + +--000000000000a47519057e029630 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
A good message bis
+ +--000000000000a47519057e029630-- +""" + partners_exist = Partner.search([('email', 'in', ['client_a@someprovider.com', 'client_b@someprovider.com'])]) + self.assertFalse(partners_exist) + + odex25_helpdesk_ticket0_id = self.env['mail.thread'].message_process('odex25_helpdesk.ticket', new_message0) + odex25_helpdesk_ticket1_id = self.env['mail.thread'].message_process('odex25_helpdesk.ticket', new_message1) + + odex25_helpdesk_ticket0 = self.env['odex25_helpdesk.ticket'].browse(odex25_helpdesk_ticket0_id) + odex25_helpdesk_ticket1 = self.env['odex25_helpdesk.ticket'].browse(odex25_helpdesk_ticket1_id) + + self.assertEqual(odex25_helpdesk_ticket0.team_id, odex25_helpdesk_team0) + self.assertEqual(odex25_helpdesk_ticket1.team_id, odex25_helpdesk_team1) + + self.assertEqual(odex25_helpdesk_ticket0.company_id, company0) + self.assertEqual(odex25_helpdesk_ticket1.company_id, company1) + + partner0 = Partner.search([('email', '=', 'client_a@someprovider.com')]) + partner1 = Partner.search([('email', '=', 'client_b@someprovider.com')]) + self.assertTrue(partner0) + self.assertTrue(partner1) + + self.assertEqual(partner0.company_id, company0) + self.assertEqual(partner1.company_id, company1) + + self.assertEqual(odex25_helpdesk_ticket0.partner_id, partner0) + self.assertEqual(odex25_helpdesk_ticket1.partner_id, partner1) + + self.assertTrue(partner0 in odex25_helpdesk_ticket0.message_follower_ids.mapped('partner_id')) + self.assertTrue(partner1 in odex25_helpdesk_ticket1.message_follower_ids.mapped('partner_id')) + + def test_team_assignation_balanced(self): + #We create an sla policy with minimum priority set as '2' + self.test_team.use_sla = True + sla = self.env['odex25_helpdesk.sla'].create({ + 'name': 'test sla policy', + 'team_id': self.test_team.id, + 'stage_id': self.stage_progress.id, + 'priority': '2', + 'time_days': 0, + 'time_hours': 1 + }) + + #We create a ticket with priority less than what's on the sla policy + ticket_1 = self.env['odex25_helpdesk.ticket'].create({ + 'name': 'test ', + 'team_id': self.test_team.id, + 'priority': '1' + }) + + #We create a ticket with priority equal to what's on the sla policy + ticket_2 = self.env['odex25_helpdesk.ticket'].create({ + 'name': 'test sla ticket', + 'team_id': self.test_team.id, + 'priority': '2' + }) + + #We create a ticket with priority greater than what's on the sla policy + ticket_3 = self.env['odex25_helpdesk.ticket'].create({ + 'name': 'test sla ticket', + 'team_id': self.test_team.id, + 'priority': '3' + }) + #We confirm that the sla policy has been applied successfully on the ticket. + #sla policy must not be applied + self.assertTrue(sla not in ticket_1.sla_status_ids.mapped('sla_id')) + #sla policy must be applied + self.assertTrue(sla in ticket_2.sla_status_ids.mapped('sla_id')) + self.assertTrue(sla in ticket_3.sla_status_ids.mapped('sla_id')) diff --git a/odex25_helpdesk/odex25_helpdesk/tests/test_odex25_helpdesk_sla.py b/odex25_helpdesk/odex25_helpdesk/tests/test_odex25_helpdesk_sla.py new file mode 100644 index 000000000..6bb5f1b06 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/tests/test_odex25_helpdesk_sla.py @@ -0,0 +1,353 @@ +# -*- coding: utf-8 -*- +from contextlib import contextmanager +from unittest.mock import patch +from dateutil.relativedelta import relativedelta +from datetime import datetime + +from odoo import fields +from odoo.tests.common import SavepointCase + +NOW = datetime(2018, 10, 10, 9, 18) +NOW2 = datetime(2019, 1, 8, 9, 0) + + +class odex25_helpdeskSLA(SavepointCase): + + @classmethod + def setUpClass(cls): + super(odex25_helpdeskSLA, cls).setUpClass() + + # we create a Helpdesk user and a manager + Users = cls.env['res.users'].with_context(tracking_disable=True) + cls.main_company_id = cls.env.ref('base.main_company').id + cls.odex25_helpdesk_manager = Users.create({ + 'company_id': cls.main_company_id, + 'name': 'Helpdesk Manager', + 'login': 'hm', + 'email': 'hm@example.com', + 'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id])] + }) + cls.odex25_helpdesk_user = Users.create({ + 'company_id': cls.main_company_id, + 'name': 'Helpdesk User', + 'login': 'hu', + 'email': 'hu@example.com', + 'groups_id': [(6, 0, [cls.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id])] + }) + # the manager defines a team for our tests (the .sudo() at the end is to avoid potential uid problems) + cls.test_team = cls.env['odex25_helpdesk.team'].with_user(cls.odex25_helpdesk_manager).create({ + 'name': 'Test Team', + 'use_sla': True + }).sudo() + # He then defines its stages + stage_as_manager = cls.env['odex25_helpdesk.stage'].with_user(cls.odex25_helpdesk_manager) + cls.stage_new = stage_as_manager.create({ + 'name': 'New', + 'sequence': 10, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': False, + }) + cls.stage_progress = stage_as_manager.create({ + 'name': 'In Progress', + 'sequence': 20, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': False, + }) + cls.stage_wait = stage_as_manager.create({ + 'name': 'Waiting', + 'sequence': 25, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': False, + }) + cls.stage_done = stage_as_manager.create({ + 'name': 'Done', + 'sequence': 30, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': True, + }) + cls.stage_cancel = stage_as_manager.create({ + 'name': 'Cancelled', + 'sequence': 40, + 'team_ids': [(4, cls.test_team.id, 0)], + 'is_close': True, + }) + + cls.tag_vip = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'VIP'}) + cls.tag_urgent = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Urgent'}) + cls.tag_freeze = cls.env['odex25_helpdesk.tag'].with_user(cls.odex25_helpdesk_manager).create({'name': 'Freeze'}) + + cls.sla = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA', + 'team_id': cls.test_team.id, + 'time_days': 1, + 'time_hours': 24, + 'stage_id': cls.stage_progress.id, + }) + cls.sla_2 = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA done stage with freeze time', + 'team_id': cls.test_team.id, + 'time_days': 1, + 'time_hours': 2, + 'time_minutes': 2, + 'tag_ids': [(4, cls.tag_freeze.id)], + 'exclude_stage_ids': cls.stage_wait.ids, + 'stage_id': cls.stage_done.id, + }) + cls.sla_assigning_1 = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA assigning no stage', + 'team_id': cls.test_team.id, + 'time_hours': 1, + 'target_type': 'assigning' + }) + + cls.sla_assigning_2 = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA assigning new stage', + 'team_id': cls.test_team.id, + 'time_hours': 1, + 'stage_id': cls.stage_new.id, + 'target_type': 'assigning' + }) + + cls.sla_assigning_3 = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA assigning progress stage', + 'team_id': cls.test_team.id, + 'time_hours': 1, + 'stage_id': cls.stage_progress.id, + 'target_type': 'assigning' + }) + + cls.sla_assigning_4 = cls.env['odex25_helpdesk.sla'].create({ + 'name': 'SLA assigning done stage', + 'team_id': cls.test_team.id, + 'time_hours': 1, + 'stage_id': cls.stage_done.id, + 'target_type': 'assigning' + }) + + # He also creates a ticket types for Question and Issue + cls.type_question = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({ + 'name': 'Question_test', + }).sudo() + cls.type_issue = cls.env['odex25_helpdesk.ticket.type'].with_user(cls.odex25_helpdesk_manager).create({ + 'name': 'Issue_test', + }).sudo() + + def _utils_set_create_date(self, records, date_str, ticket_to_update=False): + """ This method is a hack in order to be able to define/redefine the create_date + of the any recordset. This is done in SQL because ORM does not allow to write + onto the create_date field. + :param records: recordset of any odoo models + """ + query = """ + UPDATE %s + SET create_date = %%s + WHERE id IN %%s + """ % (records._table,) + self.env.cr.execute(query, (date_str, tuple(records.ids))) + + records.invalidate_cache() + + if ticket_to_update: + ticket_to_update.sla_status_ids._compute_deadline() + + @contextmanager + def _ticket_patch_now(self, datetime_str): + datetime_now_old = getattr(fields.Datetime, 'now') + datetime_today_old = getattr(fields.Datetime, 'today') + + def new_now(): + return fields.Datetime.from_string(datetime_str) + + def new_today(): + return fields.Datetime.from_string(datetime_str).replace(hour=0, minute=0, second=0) + + try: + setattr(fields.Datetime, 'now', new_now) + setattr(fields.Datetime, 'today', new_today) + + yield + finally: + # back + setattr(fields.Datetime, 'now', datetime_now_old) + setattr(fields.Datetime, 'today', datetime_today_old) + + def create_ticket(self, *arg, **kwargs): + default_values = { + 'name': "Help me", + 'team_id': self.test_team.id, + 'tag_ids': [(4, self.tag_urgent.id)], + 'stage_id': self.stage_new.id, + } + if 'tag_ids' in kwargs: + # from recordset to ORM command + kwargs['tag_ids'] = [(6, False, [tag.id for tag in kwargs['tag_ids']])] + values = dict(default_values, **kwargs) + return self.env['odex25_helpdesk.ticket'].create(values) + + def test_sla_no_tag(self): + """ SLA without tag should apply to all tickets """ + self.sla.tag_ids = [(5,)] + ticket = self.create_ticket(tag_ids=self.tag_urgent) + self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied") + + def test_sla_single_tag(self): + self.sla.tag_ids = [(4, self.tag_urgent.id)] + ticket = self.create_ticket(tag_ids=self.tag_urgent) + self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied") + + def test_sla_multiple_tags(self): + self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)] + ticket = self.create_ticket(tag_ids=self.tag_urgent) + self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should not have been applied yet") + ticket.tag_ids = [(4, self.tag_vip.id)] + self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied") + + def test_sla_tag_and_ticket_type(self): + self.sla.tag_ids = [(6, False, self.tag_urgent.ids)] + self.sla.ticket_type_id = self.type_question + ticket = self.create_ticket(tag_ids=self.tag_urgent) + self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should not have been applied yet") + ticket.ticket_type_id = self.type_question + self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied") + + def test_sla_remove_tag(self): + self.sla.tag_ids = [(6, False, (self.tag_urgent | self.tag_vip).ids)] + ticket = self.create_ticket(tag_ids=self.tag_urgent | self.tag_vip) + self.assertEqual(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning').sla_id, self.sla, "SLA should have been applied") + ticket.tag_ids = [(5,)] # Remove all tags + self.assertFalse(ticket.sla_status_ids.filtered(lambda sla: sla.target_type != 'assigning'), "SLA should no longer apply") + + @patch.object(fields.Datetime, 'now', lambda: NOW2) + def test_sla_waiting(self): + ticket = self.create_ticket(tag_ids=self.tag_freeze) + self._utils_set_create_date(ticket, '2019-01-08 9:00:00', ticket) + status = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_2.id) + self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes') + + ticket.write({'stage_id': self.stage_progress.id}) + initial_values = {ticket.id: {'stage_id': self.stage_new}} + ticket.message_track(['stage_id'], initial_values) + self._utils_set_create_date(ticket.message_ids.tracking_value_ids, '2019-01-08 11:09:50', ticket) + self.assertEqual(status.deadline, datetime(2019, 1, 9, 12, 2, 0), 'No waiting time, deadline = creation date + 1 day + 2 hours + 2 minutes') + + # We are in waiting stage, they are no more deadline. + ticket.write({'stage_id': self.stage_wait.id}) + initial_values = {ticket.id: {'stage_id': self.stage_progress}} + ticket.message_track(['stage_id'], initial_values) + self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-08 12:15:00', ticket) + self.assertFalse(status.deadline, 'In waiting stage: no more deadline') + + # We have a response of our customer, the ticket switch to in progress stage (outside working hours) + ticket.write({'stage_id': self.stage_progress.id}) + initial_values = {ticket.id: {'stage_id': self.stage_wait}} + ticket.message_track(['stage_id'], initial_values) + self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-12 10:35:58', ticket) + # waiting time = 3 full working days 9 - 10 - 11 January (12 doesn't count as it's Saturday) + # + (8 January) 12:15:00 -> 16:00:00 (end of working day) 3,75 hours + # Old deadline = '2019-01-09 12:02:00' + # New: '2019-01-09 12:02:00' + 3 days (waiting) + 2 days (weekend) + 3.75 hours (waiting) = '2019-01-14 15:47:00' + self.assertEqual(status.deadline, datetime(2019, 1, 14, 15, 47), 'We have waiting time: deadline = old_deadline + 3 full working days (waiting) + 3.75 hours (waiting) + 2 days (weekend)') + + ticket.write({'stage_id': self.stage_wait.id}) + initial_values = {ticket.id: {'stage_id': self.stage_progress}} + ticket.message_track(['stage_id'], initial_values) + self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-14 15:30:00', ticket) + self.assertFalse(status.deadline, 'In waiting stage: no more deadline') + + # We need to patch now with a new value as it will be used to compute freezed time. + with patch.object(fields.Datetime, 'now', lambda: datetime(2019, 1, 16, 15, 0)): + ticket.write({'stage_id': self.stage_done.id}) + initial_values = {ticket.id: {'stage_id': self.stage_wait}} + ticket.message_track(['stage_id'], initial_values) + self._utils_set_create_date(ticket.message_ids.tracking_value_ids[0], '2019-01-16 15:00:00', ticket) + self.assertEqual(status.deadline, datetime(2019, 1, 16, 15, 17), 'We have waiting time: deadline = old_deadline + 7.5 hours (waiting)') + + def test_sla_assigning(self): + ticket = self.create_ticket() + + status_1 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_1.id) + status_2 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_2.id) + status_3 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_3.id) + status_4 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_4.id) + + self.assertFalse(status_1.reached_datetime, "SLA status 1: reached not his target") + self.assertFalse(status_2.reached_datetime, "SLA status 2: reached not his target") + self.assertFalse(status_3.reached_datetime, "SLA status 3: reached not his target") + self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target") + self.assertTrue(status_1.deadline, "SLA status 1: has deadline") + self.assertTrue(status_2.deadline, "SLA status 2: has deadline") + self.assertFalse(status_3.deadline, "SLA status 3: hasn't deadline") + self.assertFalse(status_4.deadline, "SLA status 4: hasn't deadline") + + ticket.write({'user_id': self.odex25_helpdesk_user.id}) + + self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target") + self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target") + self.assertFalse(status_3.reached_datetime, "SLA status 3: reached not his target") + self.assertTrue(status_1.deadline, "SLA status 1: has deadline") + self.assertTrue(status_2.deadline, "SLA status 2: has deadline") + self.assertFalse(status_3.deadline, "SLA status 3: hasn't deadline") + + ticket.write({'stage_id': self.stage_progress.id}) + + self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target") + self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target") + self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target") + self.assertTrue(status_1.deadline, "SLA status 1: has deadline") + self.assertTrue(status_2.deadline, "SLA status 2: has deadline") + self.assertTrue(status_3.deadline, "SLA status 3: has deadline") + + ticket.write({'user_id': False}) + + self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target") + self.assertTrue(status_2.reached_datetime, "SLA status 2: reached his target") + self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target") + self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target") + self.assertTrue(status_1.deadline, "SLA status 1: has deadline") + self.assertTrue(status_2.deadline, "SLA status 2: has deadline") + self.assertTrue(status_3.deadline, "SLA status 3: has deadline") + self.assertFalse(status_4.deadline, "SLA status 4: hasn't deadline") + + ticket.write({'stage_id': self.stage_done.id}) + + self.assertTrue(status_3.reached_datetime, "SLA status 3: reached his target") + self.assertFalse(status_4.reached_datetime, "SLA status 4: reached not his target") + self.assertTrue(status_3.deadline, "SLA status 3: has deadline") + self.assertTrue(status_4.deadline, "SLA status 4: has deadline") + + ticket.write({'user_id': self.odex25_helpdesk_user.id}) + + self.assertTrue(status_4.reached_datetime, "SLA status 4: reached his target") + + def test_sla_assigning_skip_step(self): + ticket = self.create_ticket() + + status_1 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_1.id) + status_2 = ticket.sla_status_ids.filtered(lambda sla: sla.sla_id.id == self.sla_assigning_2.id) + + self.assertTrue(status_2.deadline, "SLA status 2: has deadline") + + ticket.write({'stage_id': self.stage_progress.id}) + + self.assertFalse(status_2.deadline, "SLA status 2: has no more deadline") + self.assertFalse(status_2.reached_datetime, "SLA status 2: reached not his target") + + ticket.write({'user_id': self.odex25_helpdesk_user.id}) + + self.assertTrue(status_1.reached_datetime, "SLA status 1: reached his target") + + @patch.object(fields.Date, 'today', lambda: NOW.date()) + @patch.object(fields.Datetime, 'today', lambda: NOW.replace(hour=0, minute=0, second=0)) + @patch.object(fields.Datetime, 'now', lambda: NOW) + def test_failed_tickets(self): + self.sla.time_days = 0 + self.sla.time_hours = 3 + # Failed ticket + failed_ticket = self.create_ticket(user_id=self.env.user.id, create_date=NOW - relativedelta(hours=3, minutes=2)) + + # Not failed ticket + ticket = self.create_ticket(user_id=self.env.user.id, create_date=NOW - relativedelta(hours=2, minutes=2)) + + data = self.env['odex25_helpdesk.team'].retrieve_dashboard() + self.assertEqual(data['my_all']['count'], 2, "There should be 2 tickets") + self.assertEqual(data['my_all']['failed'], 1, "There should be 1 failed ticket") diff --git a/odex25_helpdesk/odex25_helpdesk/tests/test_ui.py b/odex25_helpdesk/odex25_helpdesk/tests/test_ui.py new file mode 100644 index 000000000..418d53f86 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/tests/test_ui.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +# -*- coding: utf-8 -*- + +import odoo.tests + + +@odoo.tests.tagged('post_install', '-at_install') +class TestUi(odoo.tests.HttpCase): + def test_ui(self): + self.start_tour("/web", 'odex25_helpdesk_tour', login="admin") diff --git a/odex25_helpdesk/odex25_helpdesk/views/assets.xml b/odex25_helpdesk/odex25_helpdesk/views/assets.xml new file mode 100644 index 000000000..fecff3d02 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/assets.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/views/digest_views.xml b/odex25_helpdesk/odex25_helpdesk/views/digest_views.xml new file mode 100644 index 000000000..3c3688fbf --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/digest_views.xml @@ -0,0 +1,15 @@ + + + + digest.digest.view.form.inherit.sale.order + digest.digest + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/views/mail_activity_views.xml b/odex25_helpdesk/odex25_helpdesk/views/mail_activity_views.xml new file mode 100644 index 000000000..9b93d44b4 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/mail_activity_views.xml @@ -0,0 +1,23 @@ + + + + + + Ticket Assistance + Your Assistance is Required + fa-ticket + + + + + Activity Types + mail.activity.type + tree,form + ['|', ('res_model_id', '=', False), ('res_model_id.model', '=', 'odex25_helpdesk.ticket')] + {'default_res_model': 'odex25_helpdesk.ticket'} + + + \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_portal_templates.xml b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_portal_templates.xml new file mode 100644 index 000000000..9df5b3c52 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_portal_templates.xml @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_team_views.xml b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_team_views.xml new file mode 100644 index 000000000..ce547f42e --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_team_views.xml @@ -0,0 +1,635 @@ + + + + + + odex25_helpdesk.team.tree + odex25_helpdesk.team + + + + + + + + + + + + SLA Policies + odex25_helpdesk.sla + tree,form + {'default_team_id': active_id, 'search_default_team_id': active_id} + +

+ No SLA policies found. Let's create one! +

+

+ Make sure tickets are handled in a timely manner by using SLA Policies. +
+

+
+
+ + + Templates + mail.template + tree,form + [('team_id', '=', active_id)] + {'default_team_id': active_id} + +

+ Create a new template +

+
+
+ + + Helpdesk Teams + odex25_helpdesk.team + tree,form + +

+ No teams found +

+

+ Teams regroup tickets for people sharing the same expertise or from the same area. +

+
+
+ + + odex25_helpdesk.team.form + odex25_helpdesk.team + +
+ +
+ +
+
+ + + +

Productivity & Visibility

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Channels

+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

Sell & Track Hours

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

Performance

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

Self-Service

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + odex25_helpdesk.team.search + odex25_helpdesk.team + + + + + + + + + + odex25_helpdesk.team.dashboard + odex25_helpdesk.team + 200 + + + + + + + + + + + + + + + +
+ +
+
+
+ +
+ + &nbsp; + + +
+
+ + + +
+
+ + +
+
+
+
+
+
+ + + Helpdesk Overview + odex25_helpdesk.team + kanban,form + {} + + +

+ No teams found +

+

+ Teams regroup tickets for people sharing the same expertise or from the same area. +

+
+
+ + + + diff --git a/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_views.xml b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_views.xml new file mode 100644 index 000000000..83bd24b53 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/odex25_helpdesk_views.xml @@ -0,0 +1,1036 @@ + + + + + + + + + + odex25_helpdesk.tags.tree + odex25_helpdesk.tag + + + + + + + + + + Ticket Tags + odex25_helpdesk.tag + tree,form + +

+ No tags found. Let's create one! +

+

+ Tags are perfect to organize your tickets. +

+
+
+ + + + + + + + odex25_helpdesk.ticket.type.tree + odex25_helpdesk.ticket.type + + + + + + + + + + Ticket Type + odex25_helpdesk.ticket.type + tree,form + +

+ No types found. Let's create one! +

+

+ Types are perfect to categorize your tickets. +

+
+
+ + + + + + + odex25_helpdesk.stages.tree + odex25_helpdesk.stage + + + + + + + + + + + odex25_helpdesk.stage.form + odex25_helpdesk.stage + +
+ + + + + + + + + + + + + + +

+ At each stage employees can block or make task/issue ready for next stage. + You can define here labels that will be displayed for the state instead + of the default labels. +

+
+
+
+
+
+ + + Stages + odex25_helpdesk.stage + tree,form + +

+ No stages found. Let's create one! +

+

+ Adapt your pipeline to your workflow and easily track the progress of your tickets. +

+
+
+ + + + + + odex25_helpdesk.sla.tree + odex25_helpdesk.sla + + + + + + + + + + + + odex25_helpdesk.sla.search + odex25_helpdesk.sla + + + + + + + + + + + odex25_helpdesk.sla.form + odex25_helpdesk.sla + +
+ + +
+
+ + + + + + + + + + + + + +
+
+ +
+
+
+ + days +
+ + hours +
+ + minutes +
+
+ +
+
+ +
+
+
+
+ + + SLA Policies + odex25_helpdesk.sla + tree,form + +

+ No SLA policies found. Let's create one! +

+

+ Make sure tickets are handled in a timely manner by using SLA Policies. +
+

+
+
+ + + + + + odex25_helpdesk.ticket.activity + odex25_helpdesk.ticket + + + + +
+ +
+ + +
+
+
+
+
+
+ + + odex25_helpdesk.ticket.graph + odex25_helpdesk.ticket + + + + + + + + + + odex25_helpdesk.ticket.pivot + odex25_helpdesk.ticket + + + + + + + + + + + odex25_helpdesk.ticket.graph.analysis + odex25_helpdesk.ticket + + + + + + + + + + + odex25_helpdesk.ticket.pivot.analysis + odex25_helpdesk.ticket + + + + + + + + + + + odex25_helpdesk.ticket.search + odex25_helpdesk.ticket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + odex25_helpdesk.ticket.search + odex25_helpdesk.ticket + + + + + + + + + + + + + + + + + + + + + + + + + + odex25_helpdesk.ticket.tree + odex25_helpdesk.ticket + + + + + + + + + + + + + + + + + + odex25_helpdesk.ticket.form.quick_create + odex25_helpdesk.ticket + 1000 + +
+ + + + + + +
+ +
+
+
+
+
+ + + odex25_helpdesk.ticket.kanban + odex25_helpdesk.ticket + 10 + + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+
+ + + (#) + +
+
+ +
+
+ + + + + +
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+
+
+
+ + + + odex25_helpdesk.ticket.form + odex25_helpdesk.ticket + +
+
+ +
+ + + + + +
+ +
+ + +
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + Tickets + odex25_helpdesk.ticket + list,kanban,form,activity + + {'search_default_my_ticket': True, 'search_default_is_open': True} + +

+ No tickets found. Let's create one! +

+

To get things done, use activities and status on tickets. +
+ Chat in real time or by email to collaborate efficiently. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + list,kanban,form,activity + + {'search_default_is_open': True} + +

+ No tickets found. Let's create one! +

+

To get things done, use activities and status on tickets. +
+ Chat in real time or by email to collaborate efficiently. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + kanban,list,form,pivot,graph + + {'search_default_upcoming_sla_fail': True} + +

+ Congratulations! +

+

You completed all your tickets on time. +

+
+
+ + + + Tickets + odex25_helpdesk.ticket + list,kanban,form,activity + + {'create': False, 'search_default_is_open': True, 'search_default_my_ticket': True} + + +

+ No tickets found +

+

To get things done, use activities and status on tickets. +
+ Chat in real time or by email to collaborate efficiently. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + list,kanban,form,activity + [] + {'create': False, 'search_default_is_open': True, 'search_default_my_ticket': True, + 'search_default_sla_failed': True} + + + +

+ Congratulations! +

+

You completed all your tickets on time. +

+
+
+ + + + Closed Tickets Analysis + odex25_helpdesk.ticket + pivot,graph + + + {'search_default_my_ticket': True} + + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + Closed Tickets Analysis + odex25_helpdesk.ticket + pivot,graph + + + + {'search_default_my_ticket': True, 'search_default_close': True} + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + + Tickets + odex25_helpdesk.ticket + kanban,list,form,activity + [('team_id', '=', active_id)] + {'default_team_id': active_id} + + +

+ No tickets found. Let's create one! +

+

To get things done, use activities and status on tickets. +
+ Chat in real time or by email to collaborate efficiently. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + kanban,tree,form + {'search_default_team_id': active_ids, 'search_default_archive': True} + + +

+ Click create a new ticket. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + kanban,tree,form + {'search_default_team_id': active_id, 'search_default_sla_failed': True} + + +

+ Click create a new ticket. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + kanban,list,form,activity + {'search_default_team_id': active_id, 'search_default_unassigned': True} + + +

+ No tickets found. Let's create one! +

+

To get things done, use activities and status on tickets. +
+ Chat in real time or by email to collaborate efficiently. +

+
+
+ + + odex25_helpdesk.ticket.search + odex25_helpdesk.ticket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Helpdesk Ticket Analysis + odex25_helpdesk.ticket + pivot,graph + + + {'search_default_group_by_create_date': 1} + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + + + Ticket Analysis + odex25_helpdesk.ticket + pivot,graph + + + {'search_default_team_id': active_ids, 'search_default_is_open': True} + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + + odex25_helpdesk.ticket.pivot + odex25_helpdesk.ticket + + + + + + + + + + + + odex25_helpdesk.ticket.graph + odex25_helpdesk.ticket + + + + + + + + + + + Performance Analysis + odex25_helpdesk.ticket + pivot,graph + + + + {'search_default_team_id': active_ids, 'pivot_measures': ['close_hours', '__count__']} + + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + Success Rate Analysis + odex25_helpdesk.ticket + pivot,graph + + + + {'search_default_is_close': True, 'search_default_my_ticket': True, + 'search_default_not_sla_failed': True,} + + +

+ No data yet ! +

+

+ Create tickets to get statistics. +

+
+
+ + + Success Rate Analysis + odex25_helpdesk.ticket + pivot,graph + + + + {'search_default_is_close': True, 'search_default_my_ticket': True, + 'search_default_not_sla_failed': True,} + + +

+ No data yet! +

+

+ Create tickets to get statistics. +

+
+
+ + + Ticket Analysis + odex25_helpdesk.ticket + pivot,graph + + [('stage_id.is_close', '=', False)] + {'search_default_my_ticket': True, 'pivot_measures': ['close_hours', '__count__']} + + +

+ No data yet! +

+

+ Create tickets to get statistics. +

+
+
+ + + + + + +
diff --git a/odex25_helpdesk/odex25_helpdesk/views/res_partner_views.xml b/odex25_helpdesk/odex25_helpdesk/views/res_partner_views.xml new file mode 100644 index 000000000..05ac5449f --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/res_partner_views.xml @@ -0,0 +1,23 @@ + + + + + res.partner.form.inherit.odex25_helpdesk + res.partner + + + + +
+ +
+
+
+ +
diff --git a/odex25_helpdesk/odex25_helpdesk/views/zfp_config_setting.xml b/odex25_helpdesk/odex25_helpdesk/views/zfp_config_setting.xml new file mode 100644 index 000000000..4b300bf69 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk/views/zfp_config_setting.xml @@ -0,0 +1,115 @@ + + + + + service.category.form + service.category + +
+ + + + + +
+
+
+ + + service.category.tree + service.category + + + + + + + + + + Service Category + service.category + tree,form + +

+ No Service Category found. Let's create one! +

+
+
+ + + + + + + + + + + + + + + + + + + helpdesk.service.form + helpdesk.service + +
+ + + + + + + + + + + + +
+
+
+ + + helpdesk.service.tree + helpdesk.service + + + + + + + + + + Helpdesk Service + helpdesk.service + + + + Helpdesk Service + helpdesk.service + tree,form + +

+ No Service found. Let's create one! +

+
+
+ + + + +
diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/__init__.py b/odex25_helpdesk/odex25_helpdesk_assignation_method/__init__.py new file mode 100644 index 000000000..dc5e6b693 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_assignation_method/__manifest__.py new file mode 100644 index 000000000..fcb67ec65 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'EXP Helpdesk Assignation methods', + 'summary': 'Ticket Assignation methods for team members considering ticket types', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk_security'], + 'description': """ + Ticket Assignation methods for team members considering ticket types + """, + 'auto_install': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/helpdesk_views.xml', + ], + 'license': '', +} diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/i18n/ar_001.po b/odex25_helpdesk/odex25_helpdesk_assignation_method/i18n/ar_001.po new file mode 100644 index 000000000..7a5bf325c --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/i18n/ar_001.po @@ -0,0 +1,132 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_assignation_method +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-09-21 11:37+0000\n" +"PO-Revision-Date: 2022-09-21 11:37+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__create_uid +msgid "Created by" +msgstr "" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__create_date +msgid "Created on" +msgstr "" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model,name:odex25_helpdesk_assignation_method.model_odex25_helpdesk_team +msgid "Helpdesk Team" +msgstr "فريق مكتب المساعدة" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model,name:odex25_helpdesk_assignation_method.model_odex25_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_assignation_method +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method +msgid "How to assign newly created tickets to the right person" +msgstr "كيفية تعيين التذاكر التي تم إنشاؤها حديثًا إلى الشخص المناسب" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__id +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__id +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_assignation_method +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method +msgid "Keep empty for everyone to see this team" +msgstr "ابق فارغًا حتى يرى الجميع هذا الفريق" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_ticket____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__write_date +msgid "Last Updated on" +msgstr "" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__member_id +msgid "Member" +msgstr "العضو" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__members_ids +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method +msgid "Members" +msgstr "الأعضاء" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__team_id +msgid "Team" +msgstr "فريق" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__team_leader_id +msgid "Team Leader" +msgstr "قائد الفريق" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_odex25_helpdesk_team__member_ids +msgid "Team Members" +msgstr "أعضاء الفريق" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__ticket_type_ids +msgid "Ticket Type" +msgstr "نوع التذكرة" + +#. module: odex25_helpdesk_assignation_method +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_assignation_method.helpdesk_team_view_form_assignation_method +msgid "" +"With random assignation, every user gets the same number of tickets. With " +"balanced assignation, tickets are assigned to the user with the least amount" +" of open tickets." +msgstr "مع التخصيص العشوائي ، يحصل كل مستخدم على نفس عدد التذاكر. مع التوزيع المتوازن ، يتم تخصيص التذاكر للمستخدم بأقل عدد من التذاكر المفتوحة" + +#. module: odex25_helpdesk_assignation_method +#: code:addons/odex25_helpdesk_assignation_method/models/helpdesk_team.py:0 +#, python-format +msgid "You must have team members assigned to change the assignation method." +msgstr "يجب أن يكون لديك أعضاء فريق معينين لتغيير طريقة التعيين" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model,name:odex25_helpdesk_assignation_method.model_helpdesk_team_member +msgid "helpdesk.team.member" +msgstr "" + +#. module: odex25_helpdesk_assignation_method +#: model:ir.model.fields,field_description:odex25_helpdesk_assignation_method.field_helpdesk_team_member__service_id +msgid "Service" +msgstr "الخدمة" diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_assignation_method/models/__init__.py new file mode 100644 index 000000000..283c2fd2f --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import helpdesk_team \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/models/helpdesk_team.py b/odex25_helpdesk/odex25_helpdesk_assignation_method/models/helpdesk_team.py new file mode 100644 index 000000000..cb9f6ea11 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/models/helpdesk_team.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError + + +class HelpdeskTicket(models.Model): + _inherit = "odex25_helpdesk.ticket" + + team_leader_id = fields.Many2one(related='team_id.team_leader_id') + + @api.onchange('service_id') + def get_user_and_assign_it(self): + for rec in self: + result = rec._onchange_ticket_type_values(rec.team_id,rec.service_id) + rec.user_id = result['user_id'] + + def _onchange_ticket_type_values(self, team, ticket_type=None): + return { + 'user_id': team.get_new_user(ticket_type), + } + + + @api.model + def create(self, vals): + res = super(HelpdeskTicket, self).create(vals) + result = self._onchange_ticket_type_values(res.team_id,res.service_id) + res.user_id = result['user_id'] + return res + +class HelpdeskTeam(models.Model): + _inherit = "odex25_helpdesk.team" + + team_leader_id = fields.Many2one('res.users',domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_manager').id)]) + members_ids = fields.One2many('helpdesk.team.member','team_id') + member_ids = fields.Many2many('res.users', string='Team Members', domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]) + + @api.onchange('members_ids') + def _onchange_members_ids(self): + """ + update the member_ids based on members_ids + """ + for team in self: + for rec in team.member_ids: + rec = False + members = [member.member_id.id for member in self.members_ids] + addmember = [(6,0,members)] + team.update({ + 'member_ids': addmember + }) + + @api.constrains('assign_method', 'members_ids') + def _check_members_assignation(self): + if not self.members_ids and self.assign_method != 'manual': + raise ValidationError(_("You must have team members assigned to change the assignation method.")) + + def get_new_user(self, ticket_type=None): + for rec in self: + rec.ensure_one() + new_user = self.env['res.users'] + members_ids = [] + if ticket_type: + for member in rec.members_ids: + if ticket_type in member.service_id: + members_ids.append(member.member_id.id) + members_ids = sorted(members_ids) + elif len(members_ids) == 0: + members_ids = sorted([member.member_id.id for member in rec.members_ids]) + if members_ids: + if rec.assign_method == 'randomly': + # randomly means new ticketss get uniformly distributed + previous_assigned_user = self.env['odex25_helpdesk.ticket'].search([('team_id', '=', rec.id)], order='create_date desc', limit=1).user_id + # handle the case where the previous_assigned_user has left the team (or there is none). + if previous_assigned_user and previous_assigned_user.id in members_ids: + previous_index = members_ids.index(previous_assigned_user.id) + new_user = new_user.browse(members_ids[(previous_index + 1) % len(members_ids)]) + else: + new_user = new_user.browse(members_ids[0]) + elif rec.assign_method == 'balanced': + read_group_res = self.env['odex25_helpdesk.ticket'].read_group([('stage_id.is_close', '=', False), ('user_id', 'in', members_ids)], ['user_id'], ['user_id']) + # add all the members in case a member has no more open tickets (and thus doesn't appear in the previous read_group) + count_dict = dict((m_id, 0) for m_id in members_ids) + count_dict.update((data['user_id'][0], data['user_id_count']) for data in read_group_res) + new_user = new_user.browse(min(count_dict, key=count_dict.get)) + return new_user + + +class HelpdeskTeamMemebers(models.Model): + _name = "helpdesk.team.member" + + team_id = fields.Many2one('odex25_helpdesk.team') + member_id = fields.Many2one('res.users',domain=lambda self: [('groups_id', 'in', self.env.ref('odex25_helpdesk.group_odex25_helpdesk_user').id)]) + # ticket_type_ids = fields.Many2many('odex25_helpdesk.ticket.type') + service_id = fields.Many2many('helpdesk.service') + +# +# +# class InheritUser(models.Model): +# _inherit = 'res.users' +# +# @api.model +# def name_search(self, name='', args=None, operator='ilike', limit=100): +# if self._context.get('members', []): +# member_id = self.env['odex25_helpdesk.team'].new('members_ids',self._context.get('members', [])) +# args.append(('id', 'not in', +# [isinstance(d['member_id'], tuple) and d['member_id'][0] or d['member_id'] +# for d in member_id])) +# return super(InheritUser, self).name_search(name, args=args, operator=operator, limit=limit) \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/security/ir.model.access.csv b/odex25_helpdesk/odex25_helpdesk_assignation_method/security/ir.model.access.csv new file mode 100644 index 000000000..82181a5a2 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_helpdesk_team_member,helpdesk.team.member,odex25_helpdesk_assignation_method.model_helpdesk_team_member,,1,1,1,1 \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/icon.png b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/icon.png new file mode 100644 index 000000000..a37b5967b Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/icon.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/index.html b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/index.html new file mode 100644 index 000000000..4bd6cf0ca --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/index.html @@ -0,0 +1,83 @@ +
+
+

+ ONE OF ODEX MODULES

+
+ ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system +
+ .to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one + solution +
+
+
+ +
+
+

+ Contact Us +

+
+
+
+
+ +
+
+ + + +
+

+ + exposa + +

+
+
+
+ + + +
+

+ + exposa + +

+
+ +
+
+
+
+ +
+
diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/internet.png b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/internet.png new file mode 100644 index 000000000..a89809bfa Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/internet.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/linkedin.png b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/linkedin.png new file mode 100644 index 000000000..a0fe09caa Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/linkedin.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/mail.png b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/mail.png new file mode 100644 index 000000000..bc5a4de2a Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/mail.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/twitter.png b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/twitter.png new file mode 100644 index 000000000..983f325a3 Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_assignation_method/static/description/twitter.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_assignation_method/views/helpdesk_views.xml b/odex25_helpdesk/odex25_helpdesk_assignation_method/views/helpdesk_views.xml new file mode 100644 index 000000000..0af842550 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_assignation_method/views/helpdesk_views.xml @@ -0,0 +1,50 @@ + + + + helpdesk.team.form.inherit.assignation.method + odex25_helpdesk.team + + + +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + +
+
+
+
+
diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/__init__.py b/odex25_helpdesk/odex25_helpdesk_reopen/__init__.py new file mode 100644 index 000000000..dc5e6b693 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_reopen/__manifest__.py new file mode 100644 index 000000000..744d8eefb --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'EXP Helpdesk Reopen', + 'summary': 'adding reopen feature to helpdesk', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk'], + 'description': """ + ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system + .to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one solution + """, + 'auto_install': True, + 'data': [ + 'views/helpdesk_views.xml', + 'views/cron_repair.xml', + ], + 'license': '', +} diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/i18n/ar_001.po b/odex25_helpdesk/odex25_helpdesk_reopen/i18n/ar_001.po new file mode 100644 index 000000000..43d298ce8 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/i18n/ar_001.po @@ -0,0 +1,102 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_reopen +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-20 13:31+0000\n" +"PO-Revision-Date: 2023-08-20 13:31+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_auto_close +msgid "Auto Close Kanban Stage" +msgstr "مرحلة الإغلاق التلقائي" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__close_time +msgid "Close Time" +msgstr "زمن الإقفال" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_reopen +#: model:ir.actions.server,name:odex25_helpdesk_reopen.ir_cron_automatic_done_state_ir_actions_server +#: model:ir.cron,cron_name:odex25_helpdesk_reopen.ir_cron_automatic_done_state +#: model:ir.cron,name:odex25_helpdesk_reopen.ir_cron_automatic_done_state +msgid "Help Desk Cancel ;Automatic Done State" +msgstr "إلغاء مكتب المساعدة ؛ حالة تم تلقائيًا" + +#. module: odex25_helpdesk_reopen +#: model:ir.model,name:odex25_helpdesk_reopen.model_odex25_helpdesk_stage +msgid "Helpdesk Stage" +msgstr "مرحلة مكتب المساعدة" + +#. module: odex25_helpdesk_reopen +#: model:ir.model,name:odex25_helpdesk_reopen.model_odex25_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__id +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_ticket____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_reopen +msgid "Re-open Kanban Stage" +msgstr "أعد فتح مرحلة كانبان" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__reopen_time +msgid "Reopen Time" +msgstr "وقت إعادة الفتح" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_auto_close +msgid "" +"in this stage the ticket will be closed again if the customer not send email" +msgstr "في هذه المرحلة ، سيتم إغلاق التذكرة مرة أخرى إذا لم يرسل العميل بريدًا إلكترونيًا" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_reopen +msgid "" +"in this stage the ticket will be opened again if the customerreplied to your" +" close stage email" +msgstr "" +"في هذه المرحلة ، سيتم فتح التذكرة مرة أخرى إذا قام العميل بالرد على البريد " +"الإلكتروني الخاص بك" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__close_time +msgid "the time that make a ticket close task" +msgstr "الوقت الذي يجعل مهمة مغلقة" + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,help:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__reopen_time +msgid "the time that make a ticket reopen task" +msgstr "الوقت الذي يجعل مهمة إعادة فتح تذكرة" + + +#. module: odex25_helpdesk_reopen +#: model:ir.model.fields,field_description:odex25_helpdesk_reopen.field_odex25_helpdesk_stage__is_done +msgid "Close Ticket" +msgstr "إغلاق التذكرة" \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_reopen/models/__init__.py new file mode 100644 index 000000000..678daff6c --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import helpdesk \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/models/helpdesk.py b/odex25_helpdesk/odex25_helpdesk_reopen/models/helpdesk.py new file mode 100644 index 000000000..2d170c9f8 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/models/helpdesk.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime +from odoo import api, fields, models, tools, _ +from dateutil.relativedelta import relativedelta +import logging + +_logger = logging.getLogger(__name__) + + +class HelpdeskStage(models.Model): + _inherit = 'odex25_helpdesk.stage' + + is_reopen = fields.Boolean('Re-open Kanban Stage', help="in this stage the ticket will be opened again if the " + "customer""replied to your close stage email") + reopen_time = fields.Float(string="Reopen Time", help="the time that make a ticket reopen task") + is_auto_close = fields.Boolean('Auto Close Kanban Stage',help="in this stage the ticket will be closed again if " + "the customer not send email") + close_time = fields.Float(string="Close Time", help="the time that make a ticket close task") + is_done = fields.Boolean(string="Close Ticket") + + +class HelpdeskTicket(models.Model): + _inherit = 'odex25_helpdesk.ticket' + + def _message_post_after_hook(self, message, msg_vals): + if self.partner_email and self.partner_id and not self.partner_id.email: + self.partner_id.email = self.partner_email + + if self.partner_email and not self.partner_id: + # we consider that posting a message with a specified recipient (not a follower, a specific one) + # on a document without customer means that it was created through the chatter using + # suggested recipients. This heuristic allows to avoid ugly hacks in JS. + new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email) + if new_partner: + self.search([ + ('partner_id', '=', False), + ('partner_email', '=', new_partner.email), + ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id}) + if message.author_id: + if message.author_id.email == self.partner_email and self.stage_id.is_close: + previous = self.env['mail.message'].search([ + ('res_id', '=', self.id), + ('subject', '!=', ''), + ('model', '=', 'odex25_helpdesk.ticket'), + ('author_id', '!=', self.partner_id.id), + ]) + if len(previous) != 0 and previous[0]: + previous = previous[0] + for stage in self.team_id.stage_ids: + if stage.is_reopen: + difference = fields.Datetime.from_string(message.date) - fields.Datetime.from_string( + previous.date) + difference = str(difference).split(':') + hour, minute = divmod(stage.reopen_time, 1) + minute *= 60 + result = '{}:{}'.format(int(hour), int(minute)) + result = result.split(':') + if int(difference[0]) < int(result[0]): + self.stage_id = stage.id + break + elif int(difference[1]) <= int(result[1]): + self.stage_id = stage.id + + return super(HelpdeskTicket, self)._message_post_after_hook(message, msg_vals) + + @api.model + def automatic_close_state(self): + ticket = self.env['odex25_helpdesk.ticket'].search([]) + is_done = self.env['odex25_helpdesk.stage'].search([('is_done', '=', True)], limit=1) + for rec in ticket: + if rec.stage_id.is_auto_close: + close_time = rec.stage_id.close_time + hour, minute = divmod(close_time, 1) + minute *= 60 + to_str = fields.Datetime.to_string( + fields.Datetime.from_string(fields.Datetime.now()) - relativedelta(hours=hour, minutes=int(minute))) + if not rec.stage_id.is_close: + msg_res = rec.message_ids.filtered(lambda x: str(x.date) < to_str) + if len(rec.message_ids) == len(msg_res): + rec.stage_id = is_done.id diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/views/cron_repair.xml b/odex25_helpdesk/odex25_helpdesk_reopen/views/cron_repair.xml new file mode 100644 index 000000000..42b1a6b32 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/views/cron_repair.xml @@ -0,0 +1,13 @@ + + + + Help Desk Cancel ;Automatic Done State + + code + model.automatic_close_state() + 3 + minutes + -1 + + + diff --git a/odex25_helpdesk/odex25_helpdesk_reopen/views/helpdesk_views.xml b/odex25_helpdesk/odex25_helpdesk_reopen/views/helpdesk_views.xml new file mode 100644 index 000000000..87137d409 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_reopen/views/helpdesk_views.xml @@ -0,0 +1,20 @@ + + + + helpdesk.stage.reopen.form + odex25_helpdesk.stage + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_sale/__init__.py b/odex25_helpdesk/odex25_helpdesk_sale/__init__.py new file mode 100644 index 000000000..cde864bae --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/odex25_helpdesk/odex25_helpdesk_sale/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_sale/__manifest__.py new file mode 100644 index 000000000..e81f02b3d --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Helpdesk After Sales', + 'summary': 'Project, Tasks, After Sales', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk', 'sale_management'], + 'auto_install': True, + 'description': """ +Manage the after sale of the products from helpdesk tickets. + """, + 'data': [ + 'views/odex25_helpdesk_views.xml', + ], + 'demo': ['data/odex25_helpdesk_sale_demo.xml'], +} diff --git a/odex25_helpdesk/odex25_helpdesk_sale/data/odex25_helpdesk_sale_demo.xml b/odex25_helpdesk/odex25_helpdesk_sale/data/odex25_helpdesk_sale_demo.xml new file mode 100644 index 000000000..4f496f622 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/data/odex25_helpdesk_sale_demo.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + 1 + 3645.00 + + + + + + + + + + + + 1 + 14.00 + + + + + + + + + + + + 1 + 12.50 + + + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_sale/i18n/ar.po b/odex25_helpdesk/odex25_helpdesk_sale/i18n/ar.po new file mode 100644 index 000000000..b93406335 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/i18n/ar.po @@ -0,0 +1,59 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_sale +# +# Translators: +# Mustafa Rawi , 2020 +# Osama Ahmaro , 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~13.5+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-01 07:39+0000\n" +"PO-Revision-Date: 2020-09-07 08:20+0000\n" +"Last-Translator: Osama Ahmaro , 2020\n" +"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__commercial_partner_id +msgid "Commercial Entity" +msgstr "الكيان التجاري" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_sale +#: model:ir.model,name:odex25_helpdesk_sale.model_odex25_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,field_description:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id +msgid "Ref. Sales Order" +msgstr "" + +#. module: odex25_helpdesk_sale +#: model:ir.model.fields,help:odex25_helpdesk_sale.field_odex25_helpdesk_ticket__sale_order_id +msgid "" +"Reference of the Sales Order to which this ticket refers. Setting this " +"information aims at easing your After Sales process and only serves " +"indicative purposes." +msgstr "" diff --git a/odex25_helpdesk/odex25_helpdesk_sale/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_sale/models/__init__.py new file mode 100644 index 000000000..27eb465e7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import odex25_helpdesk diff --git a/odex25_helpdesk/odex25_helpdesk_sale/models/odex25_helpdesk.py b/odex25_helpdesk/odex25_helpdesk_sale/models/odex25_helpdesk.py new file mode 100644 index 000000000..66e965b0b --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/models/odex25_helpdesk.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class odex25_helpdeskTicket(models.Model): + _inherit = 'odex25_helpdesk.ticket' + + commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id') + sale_order_id = fields.Many2one('sale.order', string='Ref. Sales Order', + domain="""[ + '|', (not commercial_partner_id, '=', 1), ('partner_id', 'child_of', commercial_partner_id or []), + ('company_id', '=', company_id)]""", + groups="sales_team.group_sale_salesman,account.group_account_invoice", + help="Reference of the Sales Order to which this ticket refers. Setting this information aims at easing your After Sales process and only serves indicative purposes.") + + def copy(self, default=None): + if not self.env.user.has_group('sales_team.group_sale_salesman') and not self.env.user.has_group('account.group_account_invoice'): + if default is None: + default = {'sale_order_id': False} + else: + default.update({'sale_order_id': False}) + return super(odex25_helpdeskTicket, self).copy(default=default) diff --git a/odex25_helpdesk/odex25_helpdesk_sale/static/description/icon.png b/odex25_helpdesk/odex25_helpdesk_sale/static/description/icon.png new file mode 100644 index 000000000..4141f52da Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sale/static/description/icon.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sale/views/odex25_helpdesk_views.xml b/odex25_helpdesk/odex25_helpdesk_sale/views/odex25_helpdesk_views.xml new file mode 100644 index 000000000..492237564 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sale/views/odex25_helpdesk_views.xml @@ -0,0 +1,44 @@ + + + + odex25_helpdesk.ticket.form.inherit.invoicing + odex25_helpdesk.ticket + + + + + + + + {'always_reload': True} + {'res_partner_search_mode': 'customer'} + + + + + + odex25_helpdesk.ticket.form.quick_create + odex25_helpdesk.ticket + + + + {'always_reload': True} + {'res_partner_search_mode': 'customer'} + + + + + + odex25_helpdesk.ticket.form.inherit.invoicing + odex25_helpdesk.ticket + + + + {"no_create": True} + 0 + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_security/__init__.py b/odex25_helpdesk/odex25_helpdesk_security/__init__.py new file mode 100644 index 000000000..b75bfd8cf --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import controllers \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_security/__manifest__.py new file mode 100644 index 000000000..29efa4c63 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'EXP Helpdesk Security', + 'summary': 'Ticket Security', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk', 'odex25_helpdesk_reopen'], + 'description': """ + Ticket Security + """, + 'auto_install': True, + 'data': [ + # 'data/helpdesk_data.xml', + 'security/helpdesk_security.xml', + 'security/ir.model.access.csv', + 'views/view.xml', + ], + 'qweb': [ + "static/src/xml/template.xml", + ], + 'license': '', +} diff --git a/odex25_helpdesk/odex25_helpdesk_security/controllers/__init__.py b/odex25_helpdesk/odex25_helpdesk_security/controllers/__init__.py new file mode 100644 index 000000000..72f4562d7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/controllers/__init__.py @@ -0,0 +1 @@ +from . import controller \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/controllers/controller.py b/odex25_helpdesk/odex25_helpdesk_security/controllers/controller.py new file mode 100644 index 000000000..1606225ee --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/controllers/controller.py @@ -0,0 +1,33 @@ +from odoo import http +from odoo.http import request +from odoo.tools.translate import _ +from odoo.addons.mail.controllers.main import MailController +from odoo.osv.expression import OR +from odoo.tools import consteq, pycompat + +class MailController(MailController): + + @http.route('/mail/view', type='http', auth='none') + def mail_action_view(self, model=None, res_id=None, message_id=None, access_token=None, **kwargs): + user = request.session.uid + if message_id: + try: + message = request.env['mail.message'].sudo().browse(int(message_id)).exists() + except: + message = request.env['mail.message'] + if message: + model, res_id = message.model, message.res_id + else: + # either a wrong message_id, either someone trying ids -> just go to messaging + return self._redirect_to_messaging() + elif res_id and isinstance(res_id, str): + res_id = int(res_id) + + if request.session.uid: + user = request.env['res.users'].search([('id','=',request.session.uid)]) + if not user.has_group('odex25_helpdesk_security.group_helpdesk_normal_manager') and not user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'): + return False + else: + return self._redirect_to_record(model, res_id, access_token) + return self._redirect_to_record(model, res_id, access_token) + \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/i18n/ar_001.po b/odex25_helpdesk/odex25_helpdesk_security/i18n/ar_001.po new file mode 100644 index 000000000..30ff4ad31 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/i18n/ar_001.po @@ -0,0 +1,163 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_security +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-29 07:21+0000\n" +"PO-Revision-Date: 2023-08-29 07:21+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: odex25_helpdesk_security +#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_manager_ticket_action_main_tree +#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_normal_user_action_main_tree +#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_ticket_normal_user_menu_main +#: model:ir.ui.menu,name:odex25_helpdesk_security.odex25_helpdesk_ticket_action_main_my_tree +msgid "All Tickets" +msgstr "كل التذاكر" + +#. module: odex25_helpdesk_security +#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "" +"Can't create ticket in team , only team leader can create ticket in this " +"channel" +msgstr "" +"لا يمكن إنشاء تذكرة في الفريق ، يمكن لقائد الفريق فقط إنشاء تذكرة في هذه " +"القناة" + +#. module: odex25_helpdesk_security +#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "" +"Can't create ticket in team without leader and members except for helpdesk " +"manager" +msgstr "" +"لا يمكن إنشاء تذكرة في الفريق بدون قائد وأعضاء باستثناء مدير مكتب المساعدة" + +#. module: odex25_helpdesk_security +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.action_upcoming_sla_fail_all_tickets_normal_user +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_action_team_user +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_action_unassigned_normal_user +msgid "Click create a new ticket." +msgstr "انقر فوق إنشاء تذكرة جديدة" + +#. module: odex25_helpdesk_security +#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_team_dashboard_normal_user_action_main +#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_menu_team_normal_user_dashboard +msgid "Dashboard" +msgstr "لوحة عرض" + +#. module: odex25_helpdesk_security +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_security +#: model:ir.ui.menu,name:odex25_helpdesk_security.menu_helpdesk_normal_user_root +msgid "Helpdesk" +msgstr "مكتب المساعدة" + +#. module: odex25_helpdesk_security +#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_manager +msgid "Helpdesk Manager" +msgstr "مدير الدعم الفني" + +#. module: odex25_helpdesk_security +#: model:ir.model,name:odex25_helpdesk_security.model_odex25_helpdesk_stage +msgid "Helpdesk Stage" +msgstr "مرحلة مكتب المساعدة" + +#. module: odex25_helpdesk_security +#: model:ir.model,name:odex25_helpdesk_security.model_odex25_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_security +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage__id +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_security +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_stage____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_security.field_odex25_helpdesk_ticket____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_security +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_manager_ticket_action_main_tree +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_ticket_normal_user_action_main_tree +msgid "No tickets to display." +msgstr "لا تذاكر لعرضها." + +#. module: odex25_helpdesk_security +#: model:ir.ui.menu,name:odex25_helpdesk_security.ticket_report_menu_main +msgid "Reporting" +msgstr "التقارير" + +#. module: odex25_helpdesk_security +#: model:ir.ui.menu,name:odex25_helpdesk_security.menu_sla_analysis +msgid "SLA Status Analysis" +msgstr "تحليل حالة اتفاقية مستوى الخدمة" + +#. module: odex25_helpdesk_security +#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_user_manager +msgid "Team Manager" +msgstr "قائد فريق" + +#. module: odex25_helpdesk_security +#: model:res.groups,name:odex25_helpdesk_security.group_helpdesk_normal_user +msgid "Team Member" +msgstr "عضو فريق" + +#. module: odex25_helpdesk_security +#: model:ir.actions.act_window,name:odex25_helpdesk_security.action_upcoming_sla_fail_all_tickets_normal_user +#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_action_team_user +#: model:ir.actions.act_window,name:odex25_helpdesk_security.helpdesk_ticket_action_unassigned_normal_user +#: model:ir.ui.menu,name:odex25_helpdesk_security.helpdesk_ticket_report_menu +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban +msgid "Tickets" +msgstr "تذاكر" + +#. module: odex25_helpdesk_security +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban +msgid "Tickets to Review" +msgstr "تذاكر للمراجعة" + +#. module: odex25_helpdesk_security +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_security.helpdesk_team_normal_user_view_kanban +msgid "Unassigned Tickets" +msgstr "التذاكر غير المخصصة" + +#. module: odex25_helpdesk_security +#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0 +#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "" +"You don't have permission to delete this record, please contact Helpdesk " +"Manager" +msgstr "ليس لديك إذن بحذف هذا السجل ، يرجى الاتصال بمدير مكتب المساعدة" + +#. module: odex25_helpdesk_security +#: code:addons/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py:0 +#, python-format +msgid "" +"You're not allowed to create ticket in this team, please select a team that " +"you are a member in." +msgstr "" +"غير مسموح لك بإنشاء تذكرة في هذا الفريق ، يرجى تحديد الفريق الذي أنت عضو " +"فيه." + +#. module: odex25_helpdesk_security +#: model_terms:ir.actions.act_window,help:odex25_helpdesk_security.helpdesk_team_dashboard_normal_user_action_main +msgid "Your teams will appear here." +msgstr "ستظهر فرقك هنا." \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_security/models/__init__.py new file mode 100644 index 000000000..52d1688ec --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/models/__init__.py @@ -0,0 +1 @@ +from . import odex25_helpdesk_ticket \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py b/odex25_helpdesk/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py new file mode 100644 index 000000000..8759db247 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/models/odex25_helpdesk_ticket.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools, _ +from odoo.exceptions import ValidationError + + +class HelpdeskTicket(models.Model): + _inherit = 'odex25_helpdesk.ticket' + + @api.model + def create(self, vals): + """ + prevent creating ticket for other teams for normal users + """ + if 'team_id' in vals: + team = self.env['odex25_helpdesk.team'].browse(vals['team_id']) + if not team.team_leader_id and not team.member_ids and not self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_manager'): + raise ValidationError( + _("Can't create ticket in team without leader and members except for helpdesk manager")) + + if team.team_leader_id and not team.member_ids and self.env.user.id != team.team_leader_id.id: + raise ValidationError( + _("Can't create ticket in team , only team leader can create ticket in this channel")) + + if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'): + if 'team_id' in vals and vals['team_id']: + team = self.env['odex25_helpdesk.team'].search([('id','=',vals['team_id'])]) + members = [member.id for member in team.member_ids] + members.append(team.team_leader_id.id) + if self.env.user.id not in members: + raise ValidationError(_("You're not allowed to create ticket in this team, please select a team that you are a member in.")) + res = super(HelpdeskTicket,self).create(vals) + return res + + def unlink(self): + """ + prevent deleting for normal users + """ + if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'): + raise ValidationError(_("You don't have permission to delete this record, please contact Helpdesk Manager")) + return super(HelpdeskTicket,self).unlink() + + +class HelpdeskStage(models.Model): + _inherit = 'odex25_helpdesk.stage' + + def unlink(self): + """ + prevent deleting for normal users + """ + if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'): + raise ValidationError(_("You don't have permission to delete this record, please contact Helpdesk Manager")) + return super(HelpdeskStage,self).unlink() + + # @api.multi + # def write(self,vals): + # """ + # prevent writing for normal users + # """ + # if self.env.user.has_group('odex25_helpdesk_security.group_helpdesk_normal_user'): + # raise ValidationError(_("You don't have permission to this record, please contact Helpdesk Manager")) + # return super(HelpdeskStage,self).unlink() \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/security/helpdesk_security.xml b/odex25_helpdesk/odex25_helpdesk_security/security/helpdesk_security.xml new file mode 100644 index 000000000..5c576ddbc --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/security/helpdesk_security.xml @@ -0,0 +1,31 @@ + + + + + + + Team Member + + + + + + Team Manager + + + + + + Helpdesk Manager + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_security/security/ir.model.access.csv b/odex25_helpdesk/odex25_helpdesk_security/security/ir.model.access.csv new file mode 100644 index 000000000..4ab9b533f --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_helpdesk_user_ir_config_parameter,helpdesk.ir.config.parameter,base.model_ir_config_parameter,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,0 +access_helpdesk_stage,helpdesk.stage,odex25_helpdesk.model_odex25_helpdesk_stage,odex25_helpdesk.group_odex25_helpdesk_user,1,1,1,1 +access_helpdesk_tags,helpdesk.tag,odex25_helpdesk.model_odex25_helpdesk_tag,odex25_helpdesk.group_odex25_helpdesk_manager,1,1,1,1 +access_helpdesk_sla_report_analysis,sla_report_analysis,odex25_helpdesk.model_odex25_helpdesk_sla_report_analysis,odex25_helpdesk_security.group_helpdesk_normal_user_manager,1,1,1,1 \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_security/static/src/xml/template.xml b/odex25_helpdesk/odex25_helpdesk_security/static/src/xml/template.xml new file mode 100644 index 000000000..f8edc3622 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/static/src/xml/template.xml @@ -0,0 +1,30 @@ + + diff --git a/odex25_helpdesk/odex25_helpdesk_security/views/view.xml b/odex25_helpdesk/odex25_helpdesk_security/views/view.xml new file mode 100644 index 000000000..ed542930c --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_security/views/view.xml @@ -0,0 +1,340 @@ + + + + + + + All Tickets + odex25_helpdesk.ticket + tree,kanban,form + + {'search_default_my_ticket': True} + +

+ No tickets to display. +

+
+
+ + + [('user_id','=',uid)] + + + [('user_id','=',uid)] + + + [('user_id','=',uid)] + + + + + + [('user_id','=',uid),('stage_id.is_close', '=', False)] + + + + [('stage_id.is_close', '=', False),('user_id','=',uid),('priority', '=', '2')] + + + + [('user_id','=',uid),('priority', '!=', False)] + + + + + + [('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() - + datetime.timedelta(hours=15)).strftime('%Y-%m-%d %H:%M:%S'))] + + + + [('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() - + datetime.timedelta(hours=15)).strftime('%Y-%m-%d %H:%M:%S'))] + + + + + [('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() - + datetime.timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))] + + + + [('user_id','=',uid),('close_date', '>=', (datetime.datetime.today() - + datetime.timedelta(days=7)).strftime('%Y-%m-%d %H:%M:%S'))] + + + + + + ['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)] + + + ['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)] + + + + ['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)] + + + ['|',('team_id.member_ids' , 'in' , uid),('team_id.member_ids' , '=' ,False)] + + + + + + Tickets + odex25_helpdesk.ticket + kanban,tree,form,pivot,graph + + {'search_default_upcoming_sla_fail': True} + [('user_id','=',uid)] + +

+ Click create a new ticket. +

+
+
+ + + Tickets + odex25_helpdesk.ticket + kanban,tree,form + {'search_default_team_id': active_id, 'search_default_unassigned': True} + [('user_id','=',uid)] + + +

+ Click create a new ticket. +

+
+
+ + + Dashboard + odex25_helpdesk.team + kanban,form + {} + ['|',('member_ids','in',uid),'|',('team_leader_id','=',uid),'&',('team_leader_id','=',False),('member_ids','=',False)] + + +

+ Your teams will appear here. +

+
+
+ + + + + + All Tickets + odex25_helpdesk.ticket + tree,kanban,form + + {'search_default_my_ticket': True} + ['|',('user_id','=',uid),('team_leader_id','=',uid)] + +

+ No tickets to display. +

+
+
+ + + + + + + + + + + + + + + + + + + + Tickets + odex25_helpdesk.ticket + kanban,tree,form + {'search_default_my_ticket': True} + ['|',('user_id','=',uid),('team_leader_id','=',uid),('team_id', '=', active_id)] + + +

+ Click create a new ticket. +

+
+
+ + + helpdesk.team.dashboard + odex25_helpdesk.team + + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + + + Tickets to Review + + + + + + Unassigned Tickets + + + + + + + + odex25_helpdesk_security.group_helpdesk_normal_user_manager,odex25_helpdesk_security.group_helpdesk_normal_manager + + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + + + + helpdesk.ticket.kanban + odex25_helpdesk.ticket + + + + odex25_helpdesk_security.group_helpdesk_normal_manager + + + + + + + + + + + + + + + + + + + + + + + + + + helpdesk.stage.reopen.form + odex25_helpdesk.stage + + + + + 1 + + + 1 + + + 1 + {'no_open' : True} + + + 1 + {'no_open' : True} + + + 1 + + + 1 + + + + + + + odex25_helpdesk.stage.reopen.form + odex25_helpdesk.stage + + + + + 1 + + + 1 + + + + + + + + +
diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__init__.py b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__init__.py new file mode 100644 index 000000000..dc5e6b693 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__manifest__.py new file mode 100644 index 000000000..bc2047c66 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'EXP Helpdesk SLA Escalation Reminder', + 'summary': 'Reminder in SLA Policy to reminde the team leader of the task depending on configuration', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk'], + 'description': """ + Reminder in SLA Policy to reminde the team leader of the task depending on configuration + """, + 'auto_install': True, + 'data': [ + 'security/ir.model.access.csv', + 'data/reminder_cron.xml', + 'data/reminder_templates.xml', + 'views/helpdesk_sla_views.xml', + ], + 'license': '', +} diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_cron.xml b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_cron.xml new file mode 100644 index 000000000..b497c4475 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_cron.xml @@ -0,0 +1,13 @@ + + + + Helpdesk: reminder and escalation SLAs + + code + model.reminder_and_escalation() + 30 + minutes + -1 + + + \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_templates.xml b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_templates.xml new file mode 100644 index 000000000..420ada2b6 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/data/reminder_templates.xml @@ -0,0 +1,63 @@ + + + + Ticket Reminder + + Ticket Reminder + + + + + +
+
+

Reminder!

+

Dear ${ctx['mail_to'|safe]},

+ your ticket + + % if object.access_token: + ${object.name|safe}#${object.id|safe} + + % endif + + is about to seek the deadline + in ${ctx['time_untill']|safe}. Please follow up. +
+

+ Kind regards,
+ ${object.team_id.name or 'Helpdesk'} Team. +

+
+
+
+ + Ticket Escalation + + Ticket Escalation + + + + + +
+
+

Escalation!

+

Dear ${ctx['mail_to'|safe]},

+ The ticket + + % if object.access_token: + ${object.name|safe}#${object.id|safe} + + % endif + + assigned to ${object.user_id.name} + exceeded the deadline by ${ctx['time_untill']|safe}. +
+

+ Kind regards,
+ ${object.team_id.name or 'Helpdesk'} Team. +

+
+
+
+
\ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/i18n/ar_001.po b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/i18n/ar_001.po new file mode 100644 index 000000000..cf8490f65 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/i18n/ar_001.po @@ -0,0 +1,282 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_sla_escalation_reminder +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-09-21 11:48+0000\n" +"PO-Revision-Date: 2022-09-21 11:48+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid " Hours and " +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid " Minutes" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:mail.template,body_html:odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation +msgid "" +"\n" +"
\n" +"
\n" +"

Escalation!

\n" +"

Dear ${ctx['mail_to'|safe]},

\n" +" The ticket \n" +" \n" +" % if object.access_token:\n" +" ${object.name|safe}#${object.id|safe}\n" +" \n" +" % endif\n" +" \n" +" assigned to ${object.user_id.name} \n" +" exceeded the deadline by ${ctx['time_untill']|safe}.\n" +"
\n" +"

\n" +" Kind regards,
\n" +" ${object.team_id.name or 'Helpdesk'} Team.\n" +"

\n" +"
\n" +" " +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:mail.template,body_html:odex25_helpdesk_sla_escalation_reminder.ticket_sla_reminder +msgid "" +"\n" +"
\n" +"
\n" +"

Reminder!

\n" +"

عزيزي ${ctx['mail_to'|safe]},

\n" +" تذكرتك \n" +" \n" +" % if object.access_token:\n" +" ${object.name|safe}#${object.id|safe}\n" +" \n" +" % endif\n" +" \n" +" على وشك البحث عن الموعد النهائي\n" +" in ${ctx['time_untill']|safe}.أرجو المتابعة\n" +"
\n" +"

\n" +" أطيب التحيات
\n" +" ${object.team_id.name or 'الدعم الفني'} فريق.\n" +"

\n" +"
\n" +" " +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields.selection,name:odex25_helpdesk_sla_escalation_reminder.selection__helpdesk_sla_policy__type__after +msgid "After" +msgstr "بعد" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields.selection,name:odex25_helpdesk_sla_escalation_reminder.selection__helpdesk_sla_policy__type__before +msgid "Before" +msgstr "قبل" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__create_uid +msgid "Created by" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__create_date +msgid "Created on" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid "Days, " +msgstr "الأيام" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__escalation_ids +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Escalation" +msgstr "التصعيد" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_odex25_helpdesk_sla +msgid "Helpdesk SLA Policies" +msgstr "سياسات اتفاق مستوى الخدمة لمكتب المساعدة" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_odex25_helpdesk_ticket +msgid "Helpdesk Ticket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.actions.server,name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder_ir_actions_server +#: model:ir.cron,cron_name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder +#: model:ir.cron,name:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_reminder +msgid "Helpdesk: reminder and escalation SLAs" +msgstr "مكتب المساعدة: تذكير وتصعيد اتفاقيات مستوى الخدمة" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid "Hours Can't be more than working hours, Kindly! try again" +msgstr "لا يمكن أن تكون الساعات أكثر من ساعات العمل ، يرجى! حاول مرة أخرى" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__id +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__id +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid "Kindly, make sure that time is not less than or equal zero" +msgstr "يرجى التأكد من أن الوقت ليس أقل من أو يساوي الصفر" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid "Kindly, make sure that time is not less than zero" +msgstr "يرجى التأكد من أن الوقت لا يقل عن الصفر" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__write_date +msgid "Last Updated on" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#: code:addons/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py:0 +#, python-format +msgid "Madam, Sir" +msgstr "سيدتي ، سيدي" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_sla__reminder_ids +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Reminder" +msgstr "تذكير" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Reminder After-Days" +msgstr "" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Reminder After-Hours" +msgstr "تذكير بعد الأيام" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Reminder Before-Days" +msgstr "تذكير قبل الأيام" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_sla_escalation_reminder.helpdesk_sla_view_form_reminder_policy +msgid "Reminder Before-Hours" +msgstr "تذكير قبل ساعات" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__reminder_days +msgid "Reminder Days" +msgstr "أيام التذكير" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__reminder_hours +msgid "Reminder Hours" +msgstr "ساعات التذكير" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_odex25_helpdesk_ticket__reminders_ids +msgid "Reminders" +msgstr "تذكير" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__sla_escalation_id +msgid "Sla Escalation" +msgstr "تصعيد اتفاقية مستوى الخدمة (SLA)" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__sla_reminder_id +msgid "Sla Reminder" +msgstr "تذكير اتفاقية مستوى الخدمة (SLA)" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__team_id +msgid "Team" +msgstr "الفريق" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:mail.template,subject:odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation +msgid "Ticket Escalation" +msgstr "تصعيد التذاكر" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:mail.template,subject:odex25_helpdesk_sla_escalation_reminder.ticket_sla_reminder +msgid "Ticket Reminder" +msgstr "تذكير التذكرة" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__type +msgid "Type" +msgstr "النوع" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model.fields,field_description:odex25_helpdesk_sla_escalation_reminder.field_helpdesk_sla_policy__user_id +msgid "User" +msgstr "المستخدم" + +#. module: odex25_helpdesk_sla_escalation_reminder +#: model:ir.model,name:odex25_helpdesk_sla_escalation_reminder.model_helpdesk_sla_policy +msgid "helpdesk.sla.policy" +msgstr "" diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/__init__.py new file mode 100644 index 000000000..a73b21d8e --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import helpdesk_reminder_policy \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py new file mode 100644 index 000000000..081250132 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/models/helpdesk_reminder_policy.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +import datetime +import logging +_logger = logging.getLogger(__name__) + + +class HelpdeskTicket(models.Model): + _inherit = "odex25_helpdesk.ticket" + + reminders_ids = fields.Many2many('helpdesk.sla.policy') + # escalations_ids = fields.Many2many('helpdesk.sla.policy') + + +class HelpdeskSLA(models.Model): + _inherit = "odex25_helpdesk.sla" + + reminder_ids = fields.One2many('helpdesk.sla.policy','sla_reminder_id') + escalation_ids = fields.One2many('helpdesk.sla.policy','sla_escalation_id') + + @api.constrains('time_hours') + def prevent_hours_morethan_workinghours(self): + working_calendar = self.env.user.company_id.resource_calendar_id + for rec in self: + if working_calendar.is_full_day: + if rec.time_hours > working_calendar.working_hours: + raise ValidationError(_("Hours Can't be more than working hours, Kindly! try again")) + else: + if rec.time_hours > working_calendar.shift_one_working_hours or rec.time_hours > working_calendar.shift_two_working_hours: + raise ValidationError(_("Hours Can't be more than working hours, Kindly! try again")) + + @api.model + def reminder_and_escalation(self): + """ + send reminder and escalation according to sla policy + """ + policies = self.env['odex25_helpdesk.sla'].search([]) + for sla in policies: + if sla.reminder_ids: + for reminder in sla.reminder_ids: + tickets = self.env['odex25_helpdesk.ticket'].search([('sla_id','=',sla.id),('reminders_ids','not in',reminder.id),('deadline','!=',None)]) + _logger.error('length of tickets %s',len(tickets)) + for ticket in tickets: + difference = fields.Datetime.from_string(ticket.deadline) - datetime.datetime.now() + difference = str(difference).split(',') + hour, minute = divmod(reminder.reminder_hours, 1) + minute *= 60 + result = '{}:{}'.format(int(hour), int(minute)) + result = result.split(':') + if len(difference) == 2: + days = int(difference[0].split(' ')[0]) + hours = int(difference[1].split(':')[0]) + minutes = int(difference[1].split(':')[1]) + else: + days = 0 + hours = int(difference[0].split(':')[0]) + minutes = int(difference[0].split(':')[1]) + if days <= reminder.reminder_days: + if hours < int(result[0]): + if ticket.sla_id.stage_id.id != ticket.stage_id.id: + template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder') + template.email_to = ticket.user_id.partner_id.email + times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes') + if ticket.user_id: + to = ticket.user_id.name + else: + to = _("Madam, Sir") + template.sudo().with_context({ + 'time_untill':times, + 'mail_to': to, + 'lang':self.env.user.lang, + }).send_mail(ticket.id, force_send=True, raise_exception=False) + ticket.write({'reminders_ids': [(4, reminder.id, 0)]}) + elif hours == int(result[0]): + if minutes <= int(result[1]): + if ticket.sla_id.stage_id.id != ticket.stage_id.id: + template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder') + template.email_to = ticket.user_id.partner_id.email + times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes') + if ticket.user_id: + to = ticket.user_id.name + else: + to = _("Madam, Sir") + template.sudo().with_context({ + 'time_untill':times, + 'mail_to': to, + 'lang':self.env.user.lang, + }).send_mail(ticket.id, force_send=True, raise_exception=False) + ticket.write({'reminders_ids': [(4, reminder.id, 0)]}) + elif days <= -1: + if ticket.sla_id.stage_id.id != ticket.stage_id.id: + template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_reminder') + template.email_to = ticket.user_id.partner_id.email + times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes') + if ticket.user_id: + to = ticket.user_id.name + else: + to = _("Madam, Sir") + template.sudo().with_context({ + 'time_untill':times, + 'mail_to': to, + 'lang':self.env.user.lang, + }).send_mail(ticket.id, force_send=True, raise_exception=False) + ticket.write({'reminders_ids': [(4, reminder.id, 0)]}) + else: + continue + if sla.escalation_ids: + for escalation in sla.escalation_ids: + tickets = self.env['odex25_helpdesk.ticket'].search([('sla_id','=',sla.id),('reminders_ids','not in',escalation.id),('deadline','!=',None)]) + _logger.error('length of tickets %s',len(tickets)) + for ticket in tickets: + difference = datetime.datetime.now() - fields.Datetime.from_string(ticket.deadline) + difference = str(difference).split(',') + hour, minute = divmod(escalation.reminder_hours, 1) + minute *= 60 + result = '{}:{}'.format(int(hour), int(minute)) + result = result.split(':') + if len(difference) == 2: + days = int(difference[0].split(' ')[0]) + hours = int(difference[1].split(':')[0]) + minutes = int(difference[1].split(':')[1]) + else: + days = 0 + hours = int(difference[0].split(':')[0]) + minutes = int(difference[0].split(':')[1]) + if days >= escalation.reminder_days: + if hours > int(result[0]): + if ticket.sla_id.stage_id.id != ticket.stage_id.id: + template = self.env.ref('odex25_helpdesk_sla_escalation_reminder.ticket_sla_escalation') + template.email_to = escalation.user_id.partner_id.email + times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes') + if escalation.user_id: + to = escalation.user_id.name + else: + to = _("Madam, Sir") + template.sudo().with_context({ + 'time_untill':times, + 'mail_to': to, + 'lang':self.env.user.lang, + }).send_mail(ticket.id, force_send=True, raise_exception=False) + ticket.write({'reminders_ids': [(4, escalation.id, 0)]}) + elif hours == int(result[0]): + if minutes >= int(result[1]): + if ticket.sla_id.stage_id.id != ticket.stage_id.id: + template = self.env.ref('helpdesk_sla_escalation_reminder.ticket_sla_escalation') + template.email_to = escalation.user_id.partner_id.email + times = str(days) + _('Days, ') + str(hours) + _(' Hours and ') + str(minutes) + _(' Minutes') + if escalation.user_id: + to = escalation.user_id.name + else: + to = _("Madam, Sir") + template.sudo().with_context({ + 'time_untill':times, + 'mail_to': to, + 'lang':self.env.user.lang, + }).send_mail(ticket.id, force_send=True, raise_exception=False) + ticket.write({'reminders_ids': [(4, escalation.id, 0)]}) + else: + continue + + +class HelpdeskSLAReminderPolicy(models.Model): + _name = 'helpdesk.sla.policy' + + sla_reminder_id = fields.Many2one('odex25_helpdesk.sla') + sla_escalation_id = fields.Many2one('odex25_helpdesk.sla') + team_id = fields.Many2one(related="sla_reminder_id.team_id",store=True) + + type = fields.Selection([ + ('after','After'), + ('before','Before'), + ]) + reminder_hours = fields.Float() + reminder_days = fields.Integer() + user_id = fields.Many2one('res.users') + + @api.constrains('reminder_hours','reminder_days') + def _prevent_zero(self): + """ + Prevent the time to be zero if the days are zero + """ + if self.reminder_days < 0.0 or self.reminder_hours < 0.0: + raise ValidationError(_("Kindly, make sure that time is not less than zero")) + if self.reminder_days == 0.0 and self.reminder_hours <= 0.0: + raise ValidationError(_("Kindly, make sure that time is not less than or equal zero")) + + @api.onchange('team_id') + def _domain_user_to_sla(self): + """ + return domain in users + """ + users = [] + if self.sla_reminder_id: + users = self.sla_reminder_id.team_id.members_ids.mapped('member_id').ids + users.append(self.sla_reminder_id.team_id.team_leader_id.id) + if self.sla_escalation_id: + users = self.sla_escalation_id.team_id.members_ids.mapped('member_id').ids + users.append(self.sla_escalation_id.team_id.team_leader_id.id) + return{ + 'domain':{'user_id':[('id','in',users)]} + } \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/security/ir.model.access.csv b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/security/ir.model.access.csv new file mode 100644 index 000000000..eb5c019fd --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_helpdesk_sla_policy,odex25_helpdesk_sla_escalation_reminder.sla.policy,odex25_helpdesk_sla_escalation_reminder.model_helpdesk_sla_policy,,1,1,1,1 \ No newline at end of file diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/icon.png b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/icon.png new file mode 100644 index 000000000..a37b5967b Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/icon.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/index.html b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/index.html new file mode 100644 index 000000000..4bd6cf0ca --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/index.html @@ -0,0 +1,83 @@ +
+
+

+ ONE OF ODEX MODULES

+
+ ODEX system is over than 200+ modules developed by love of Expert Company, based on ODOO system +
+ .to effectively suite's Saudi and Arabic market needs.It is the first Arabic open source ERP and all-in-one + solution +
+
+
+ +
+
+

+ Contact Us +

+
+
+
+
+ +
+
+ + + +
+

+ + exposa + +

+
+
+
+ + + +
+

+ + exposa + +

+
+ +
+
+
+
+ +
+
diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/internet.png b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/internet.png new file mode 100644 index 000000000..a89809bfa Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/internet.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/linkedin.png b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/linkedin.png new file mode 100644 index 000000000..a0fe09caa Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/linkedin.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/mail.png b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/mail.png new file mode 100644 index 000000000..bc5a4de2a Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/mail.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/twitter.png b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/twitter.png new file mode 100644 index 000000000..983f325a3 Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/static/description/twitter.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/views/helpdesk_sla_views.xml b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/views/helpdesk_sla_views.xml new file mode 100644 index 000000000..230318467 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_sla_escalation_reminder/views/helpdesk_sla_views.xml @@ -0,0 +1,35 @@ + + + + odex25_helpdesk.sla.form.inherit.reminder.policy + odex25_helpdesk.sla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_stock/__init__.py b/odex25_helpdesk/odex25_helpdesk_stock/__init__.py new file mode 100644 index 000000000..35e7c9600 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/odex25_helpdesk/odex25_helpdesk_stock/__manifest__.py b/odex25_helpdesk/odex25_helpdesk_stock/__manifest__.py new file mode 100644 index 000000000..95f24a8a1 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/__manifest__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Helpdesk Stock', + 'summary': 'Project, Tasks, Stock', + 'author': "Expert Co Ltd", + 'website': "http://www.ex.com", + 'category': 'Odex25-Helpdesk/Odex25-Helpdesk', + 'depends': ['odex25_helpdesk_sale', 'stock'], + 'auto_install': False, + 'description': """ +Manage Product returns from helpdesk tickets + """, + 'data': [ + 'wizard/stock_picking_return_views.xml', + 'views/odex25_helpdesk_views.xml', + ], + 'demo': ['data/odex25_helpdesk_stock_demo.xml'], +} diff --git a/odex25_helpdesk/odex25_helpdesk_stock/data/odex25_helpdesk_stock_demo.xml b/odex25_helpdesk/odex25_helpdesk_stock/data/odex25_helpdesk_stock_demo.xml new file mode 100644 index 000000000..bbc0d88ee --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/data/odex25_helpdesk_stock_demo.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A first stock move + + + + + 1 + + 0.0 + + + + + + + + + + + + + + + + + + + + A second stock move + + + + + 1 + + 0.0 + + + + + + + diff --git a/odex25_helpdesk/odex25_helpdesk_stock/i18n/ar.po b/odex25_helpdesk/odex25_helpdesk_stock/i18n/ar.po new file mode 100644 index 000000000..fd97fe8dd --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/i18n/ar.po @@ -0,0 +1,142 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * odex25_helpdesk_stock +# +# Translators: +# Mustafa Rawi , 2020 +# Martin Trigaux, 2020 +# Osoul , 2020 +# Ghaith Gammar , 2020 +# Osama Ahmaro , 2020 +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~13.5+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-09-01 07:39+0000\n" +"PO-Revision-Date: 2020-09-07 08:20+0000\n" +"Last-Translator: Osama Ahmaro , 2020\n" +"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__partner_id +msgid "Customer" +msgstr "العميل" + +#. module: odex25_helpdesk_stock +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_stock.view_stock_return_picking_form_inherit_odex25_helpdesk_stock +msgid "Delivery to Return" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__display_name +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,help:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__tracking +msgid "Ensure the traceability of a storable product in your warehouse." +msgstr "تأكد من إمكانية تتبع المنتج القابل للتخزين في مخزنك." + +#. module: odex25_helpdesk_stock +#: model:ir.model,name:odex25_helpdesk_stock.model_odex25_helpdesk_ticket +msgid "HelpdeskTicket" +msgstr "تذكرة مكتب المساعدة" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__id +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__id +msgid "ID" +msgstr "المُعرف" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket____last_update +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__lot_id +msgid "Lot/Serial Number" +msgstr "رقم اللوط/المسلسل" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,help:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__lot_id +msgid "Lot/Serial number concerned by the ticket" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__picking_id +msgid "Picking" +msgstr "استلام" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__product_id +msgid "Product" +msgstr "المنتج" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,help:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__product_id +msgid "Product concerned by the ticket" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,help:odex25_helpdesk_stock.field_stock_return_picking__sale_order_id +msgid "" +"Reference of the Sales Order to which this ticket refers. Setting this " +"information aims at easing your After Sales process and only serves " +"indicative purposes." +msgstr "" + +#. module: odex25_helpdesk_stock +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_stock.odex25_helpdesk_ticket_view_form_inherit_stock_user +msgid "Return" +msgstr "الترجيع" + +#. module: odex25_helpdesk_stock +#: code:addons/odex25_helpdesk_stock/models/odex25_helpdesk.py:0 +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__picking_ids +#, python-format +msgid "Return Orders" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__pickings_count +msgid "Return Orders Count" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model,name:odex25_helpdesk_stock.model_stock_return_picking +msgid "Return Picking" +msgstr "استلام الشحنة المعادة" + +#. module: odex25_helpdesk_stock +#: model_terms:ir.ui.view,arch_db:odex25_helpdesk_stock.odex25_helpdesk_ticket_view_form_inherit_odex25_helpdesk_stock +msgid "Returns" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__sale_order_id +msgid "Sales Order" +msgstr "أمر البيع" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__suitable_picking_ids +msgid "Suitable Picking" +msgstr "" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_stock_return_picking__ticket_id +msgid "Ticket" +msgstr "التذكرة" + +#. module: odex25_helpdesk_stock +#: model:ir.model.fields,field_description:odex25_helpdesk_stock.field_odex25_helpdesk_ticket__tracking +msgid "Tracking" +msgstr "التتبع" diff --git a/odex25_helpdesk/odex25_helpdesk_stock/models/__init__.py b/odex25_helpdesk/odex25_helpdesk_stock/models/__init__.py new file mode 100644 index 000000000..27eb465e7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import odex25_helpdesk diff --git a/odex25_helpdesk/odex25_helpdesk_stock/models/odex25_helpdesk.py b/odex25_helpdesk/odex25_helpdesk_stock/models/odex25_helpdesk.py new file mode 100644 index 000000000..e3e21f93b --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/models/odex25_helpdesk.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models, _ + + +class odex25_helpdeskTicket(models.Model): + _inherit = 'odex25_helpdesk.ticket' + + product_id = fields.Many2one('product.product', string='Product', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Product concerned by the ticket") + tracking = fields.Selection(related='product_id.tracking') + lot_id = fields.Many2one('stock.production.lot', string='Lot/Serial Number', help="Lot/Serial number concerned by the ticket", domain="[('product_id', '=', product_id)]") + + pickings_count = fields.Integer('Return Orders Count', compute="_compute_pickings_count") + picking_ids = fields.Many2many('stock.picking', string="Return Orders") + + @api.depends('picking_ids') + def _compute_pickings_count(self): + for ticket in self: + ticket.pickings_count = len(ticket.picking_ids) + + def action_view_pickings(self): + self.ensure_one() + action = { + 'type': 'ir.actions.act_window', + 'name': _('Return Orders'), + 'res_model': 'stock.picking', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', self.picking_ids.ids)], + 'context': dict(self._context, create=False, default_company_id=self.company_id.id) + } + if self.pickings_count == 1: + action.update({ + 'view_mode': 'form', + 'res_id': self.picking_ids.id + }) + return action diff --git a/odex25_helpdesk/odex25_helpdesk_stock/static/description/icon.png b/odex25_helpdesk/odex25_helpdesk_stock/static/description/icon.png new file mode 100644 index 000000000..4141f52da Binary files /dev/null and b/odex25_helpdesk/odex25_helpdesk_stock/static/description/icon.png differ diff --git a/odex25_helpdesk/odex25_helpdesk_stock/tests/__init__.py b/odex25_helpdesk/odex25_helpdesk_stock/tests/__init__.py new file mode 100644 index 000000000..6daa352d7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_odex25_helpdesk_stock diff --git a/odex25_helpdesk/odex25_helpdesk_stock/tests/test_odex25_helpdesk_stock.py b/odex25_helpdesk/odex25_helpdesk_stock/tests/test_odex25_helpdesk_stock.py new file mode 100644 index 000000000..b6dbe932d --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/tests/test_odex25_helpdesk_stock.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.odex25_helpdesk.tests import common +from odoo.tests.common import Form, tagged + + +@tagged('post_install', '-at_install') +class Testodex25_helpdeskStock(common.odex25_helpdeskCommon): + """ Test used to check that the functionalities of After sale in Helpdesk(stock). + """ + + def test_odex25_helpdesk_stock(self): + # give the test team ability to create coupons + self.test_team.use_product_returns = True + + partner = self.env['res.partner'].create({ + 'name': 'Customer Credee' + }) + product = self.env['product.product'].create({ + 'name': 'product 1', + 'type': 'product', + 'invoice_policy': 'order', + }) + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + }) + self.env['sale.order.line'].create({ + 'product_id': product.id, + 'price_unit': 10, + 'product_uom_qty': 1, + 'order_id': so.id, + }) + so.action_confirm() + so._create_invoices() + invoice = so.invoice_ids + invoice.action_post() + so.picking_ids[0].move_lines[0].quantity_done = 1 + so.picking_ids[0]._action_done() + ticket = self.env['odex25_helpdesk.ticket'].create({ + 'name': 'test', + 'partner_id': partner.id, + 'team_id': self.test_team.id, + 'sale_order_id': so.id, + }) + + stock_picking_form = Form(self.env['stock.return.picking'].with_context({ + 'active_model': 'odex25_helpdesk.ticket', + 'default_ticket_id': ticket.id + })) + stock_picking_form.picking_id = so.picking_ids[0] + return_picking = stock_picking_form.save() + + self.assertEqual(len(return_picking.product_return_moves), 1, + "A picking line should be present") + self.assertEqual(return_picking.product_return_moves[0].product_id, product, + "The product of the picking line does not match the product of the sale order") + + return_picking.create_returns() + + return_picking = self.env['stock.picking'].search([ + ('partner_id', '=', partner.id), + ('picking_type_code', '=', 'incoming'), + ]) + + self.assertEqual(len(return_picking), 1, "No return created") + self.assertEqual(return_picking.state, 'assigned', "Wrong status of the refund") + self.assertEqual(ticket.pickings_count, 1, + "The ticket should be linked to a return") + self.assertEqual(return_picking.id, ticket.picking_ids[0].id, + "The correct return should be referenced in the ticket") diff --git a/odex25_helpdesk/odex25_helpdesk_stock/views/odex25_helpdesk_views.xml b/odex25_helpdesk/odex25_helpdesk_stock/views/odex25_helpdesk_views.xml new file mode 100644 index 000000000..77fed9253 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_stock/views/odex25_helpdesk_views.xml @@ -0,0 +1,51 @@ + + + + odex25_helpdesk.ticket.form.inherit.stock + odex25_helpdesk.ticket + + +
+ +
+ + + + + + + + + +
+ + + odex25_helpdesk.ticket.form.inherit.return.stock.user + odex25_helpdesk.ticket + + + + + + + ml-2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+
+
+
+ + + + + + + + + + + +
+
+ + + + + + + +
+
+
+
+
+ + + odex25_helpdesk.ticket.search.inherit.timesheet + odex25_helpdesk.ticket + + + + + + + + + + + odex25_helpdesk.ticket.kanban.timer + odex25_helpdesk.ticket + 5 + + + + + + + + + + + + + + + +
diff --git a/odex25_helpdesk/odex25_helpdesk_timesheet/views/project_views.xml b/odex25_helpdesk/odex25_helpdesk_timesheet/views/project_views.xml new file mode 100644 index 000000000..15d3024a1 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_timesheet/views/project_views.xml @@ -0,0 +1,47 @@ + + + + + Project Tickets + odex25_helpdesk.ticket + kanban,tree,form,pivot,graph + + {'search_default_project_id': active_id, 'default_project_id': active_id} + + + + project.project.tickets.kanban.inherited + project.project + + 24 + + + + + + + + + + + + project.form.inherit.odex25_helpdesk.timesheet + project.project + + +
+ +
+
+ +
+ +
diff --git a/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/__init__.py b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/__init__.py new file mode 100644 index 000000000..2ba46a4a2 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import odex25_helpdesk_ticket_create_timesheet diff --git a/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet.py b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet.py new file mode 100644 index 000000000..aff87ba79 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class odex25_helpdeskTicketCreateTimesheet(models.TransientModel): + _name = 'odex25_helpdesk.ticket.create.timesheet' + _description = "Create Timesheet from ticket" + + _sql_constraints = [('time_positive', 'CHECK(time_spent > 0)', "The timesheet's time must be positive" )] + + time_spent = fields.Float('Time', digits=(16, 2)) + description = fields.Char('Description') + ticket_id = fields.Many2one( + 'odex25_helpdesk.ticket', "Ticket", required=True, + default=lambda self: self.env.context.get('active_id', None), + help="Ticket for which we are creating a sales order", + ) + + def action_generate_timesheet(self): + values = { + 'task_id': self.ticket_id.task_id.id, + 'project_id': self.ticket_id.project_id.id, + 'date': fields.Datetime.now(), + 'name': self.description, + 'user_id': self.env.uid, + 'unit_amount': self.time_spent, + } + + timesheet = self.env['account.analytic.line'].create(values) + + self.ticket_id.write({ + 'timer_start': False, + 'timer_pause': False + }) + self.ticket_id.timesheet_ids = [(4, timesheet.id, None)] + self.ticket_id.user_timer_id.unlink() + return timesheet diff --git a/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet_views.xml b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet_views.xml new file mode 100644 index 000000000..c4738a0a7 --- /dev/null +++ b/odex25_helpdesk/odex25_helpdesk_timesheet/wizard/odex25_helpdesk_ticket_create_timesheet_views.xml @@ -0,0 +1,22 @@ + + + + + odex25_helpdesk.ticket.create.timesheet.wizard.form + odex25_helpdesk.ticket.create.timesheet + +
+ + + + + +
+
+
+
+
+ +
diff --git a/odex25_helpdesk/odex25_hr_gantt/__init__.py b/odex25_helpdesk/odex25_hr_gantt/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/odex25_helpdesk/odex25_hr_gantt/__manifest__.py b/odex25_helpdesk/odex25_hr_gantt/__manifest__.py new file mode 100644 index 000000000..d91bde236 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/__manifest__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Employees in Gantt', + 'category': 'Hidden', + 'summary': 'Employees in Gantt', + 'version': '1.0', + 'description': """ """, + 'depends': ['hr', 'odex25_web_gantt'], + 'data': ['views/assets.xml'], + 'qweb': ['static/src/xml/odex25_hr_gantt.xml'], + 'auto_install': True, +} diff --git a/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_renderer.js b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_renderer.js new file mode 100644 index 000000000..179f41b8a --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_renderer.js @@ -0,0 +1,14 @@ +odoo.define('odex25_hr_gantt.GanttRenderer', function (require) { + 'use strict'; + + const GanttRenderer = require('odex25_web_gantt.GanttRenderer'); + const HrGanttRow = require('odex25_hr_gantt.GanttRow'); + + const HrGanttRenderer = GanttRenderer.extend({ + config: Object.assign({}, GanttRenderer.prototype.config, { + GanttRow: HrGanttRow + }), + }); + + return HrGanttRenderer; +}); diff --git a/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_row.js b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_row.js new file mode 100644 index 000000000..155acecf8 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_row.js @@ -0,0 +1,59 @@ +odoo.define('odex25_hr_gantt.GanttRow', function (require) { + 'use strict'; + + const GanttRow = require('odex25_web_gantt.GanttRow'); + const StandaloneM2OAvatarEmployee = require('hr.StandaloneM2OAvatarEmployee'); + + const HrGanttRow = GanttRow.extend({ + template: 'HrGanttView.Row', + + /** + * @override + */ + init(parent, pillsInfo, viewInfo, options) { + this._super(...arguments); + const isGroupedByEmployee = pillsInfo.groupedByField === 'employee_id'; + const isEmptyGroup = pillsInfo.groupId === 'empty'; + this.showEmployeeAvatar = (isGroupedByEmployee && !isEmptyGroup && !!pillsInfo.resId); + }, + + /** + * @override + */ + willStart() { + const defs = [this._super(...arguments)]; + if (this.showEmployeeAvatar) { + defs.push(this._preloadAvatarWidget()); + } + return Promise.all(defs); + }, + + /** + * @override + */ + start() { + if (this.showEmployeeAvatar) { + this.avatarWidget.$el.appendTo(this.$('.o_gantt_row_employee_avatar')); + } + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Initialize the avatar widget in virtual DOM. + * + * @private + * @returns {Promise} + */ + async _preloadAvatarWidget() { + const employee = [this.resId, this.name]; + this.avatarWidget = new StandaloneM2OAvatarEmployee(this, employee); + return this.avatarWidget.appendTo(document.createDocumentFragment()); + }, + }); + + return HrGanttRow; +}); diff --git a/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_view.js b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_view.js new file mode 100644 index 000000000..a825ef4c7 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/static/src/js/odex25_hr_gantt_view.js @@ -0,0 +1,16 @@ +odoo.define('odex25_hr_gantt.GanttView', function (require) { + 'use strict'; + + const viewRegistry = require('web.view_registry'); + const GanttView = require('odex25_web_gantt.GanttView'); + const HrGanttRenderer = require('odex25_hr_gantt.GanttRenderer'); + + const HrGanttView = GanttView.extend({ + config: Object.assign({}, GanttView.prototype.config, { + Renderer: HrGanttRenderer, + }), + }); + + viewRegistry.add('odex25_hr_gantt', HrGanttView); + return HrGanttView; +}); diff --git a/odex25_helpdesk/odex25_hr_gantt/static/src/xml/odex25_hr_gantt.xml b/odex25_helpdesk/odex25_hr_gantt/static/src/xml/odex25_hr_gantt.xml new file mode 100644 index 000000000..6ab662c02 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/static/src/xml/odex25_hr_gantt.xml @@ -0,0 +1,10 @@ + + + + + +
+ + + + diff --git a/odex25_helpdesk/odex25_hr_gantt/static/tests/odex25_hr_gantt_tests.js b/odex25_helpdesk/odex25_hr_gantt/static/tests/odex25_hr_gantt_tests.js new file mode 100644 index 000000000..034f05694 --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/static/tests/odex25_hr_gantt_tests.js @@ -0,0 +1,123 @@ +odoo.define('odex25_hr_gantt.Tests', function (require) { + 'use strict'; + + const { createView } = require('web.test_utils'); + const HrGanttView = require('odex25_hr_gantt.GanttView'); + + let initialDate = new Date(2018, 11, 20, 8, 0, 0); + initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset() * 60 * 1000); + + QUnit.module('odex25_hr_gantt', {}, function () { + QUnit.module('GanttView', { + beforeEach: function () { + this.data = { + tasks: { + fields: { + name: {string: 'Name', type: 'char'}, + start: {string: 'Start Date', type: 'datetime'}, + stop: {string: 'Stop Date', type: 'datetime'}, + employee_id: {string: "Employee", type: 'many2one', relation: 'hr.employee'}, + foo: {string: "Foo", type: 'char'}, + }, + records: [ + {id: 1, name: 'Task 1', start: '2018-11-30 18:30:00', stop: '2018-12-31 18:29:59', employee_id: 11, foo: 'Foo 1'}, + {id: 2, name: 'Task 2', start: '2018-12-17 11:30:00', stop: '2018-12-22 06:29:59', employee_id: 7, foo: 'Foo 2'}, + {id: 3, name: 'Task 3', start: '2018-12-27 06:30:00', stop: '2019-01-03 06:29:59', employee_id: 23, foo: 'Foo 1'}, + {id: 4, name: 'Task 4', start: '2018-12-19 18:30:00', stop: '2018-12-20 06:29:59', employee_id: 11, foo: 'Foo 3'} + ], + }, + 'hr.employee': { + fields: {}, + records: [ + {id: 11, name: "Mario"}, + {id: 7, name: "Luigi"}, + {id: 23, name: "Yoshi"}, + ], + }, + }; + }, + }); + + QUnit.test('hr gantt view not grouped', async function (assert) { + assert.expect(1); + + const gantt = await createView({ + View: HrGanttView, + model: 'tasks', + data: this.data, + arch: '', + viewOptions: { + initialDate: initialDate, + }, + }); + + assert.containsNone(gantt, '.o_standalone_avatar_employee', + 'should have 0 employee avatars'); + + gantt.destroy(); + }); + + QUnit.test('hr gantt view grouped by employee only', async function (assert) { + assert.expect(1); + + const gantt = await createView({ + View: HrGanttView, + model: 'tasks', + data: this.data, + arch: '', + viewOptions: { + initialDate: initialDate, + }, + groupBy: ['employee_id'], + }); + + assert.containsN(gantt, + '.o_gantt_row .o_gantt_row_sidebar .o_gantt_row_title .o_gantt_row_employee_avatar .o_standalone_avatar_employee', + 3, 'should have 3 employee avatars'); + + gantt.destroy(); + }); + + QUnit.test('hr gantt view grouped by employee > foo', async function (assert) { + assert.expect(1); + + const gantt = await createView({ + View: HrGanttView, + model: 'tasks', + data: this.data, + arch: '', + viewOptions: { + initialDate: initialDate, + }, + groupBy: ['employee_id', 'foo'], + }); + + assert.containsN(gantt, + '.o_gantt_row_group .o_gantt_row_sidebar .o_gantt_row_title .o_gantt_row_employee_avatar .o_standalone_avatar_employee', + 3, 'should have 3 employee avatars'); + + gantt.destroy(); + }); + + QUnit.test('hr gantt view grouped by foo > employee', async function (assert) { + assert.expect(1); + + const gantt = await createView({ + View: HrGanttView, + model: 'tasks', + data: this.data, + arch: '', + viewOptions: { + initialDate: initialDate, + }, + groupBy: ['foo', 'employee_id'], + }); + + assert.containsN(gantt, + '.o_gantt_row_nogroup .o_gantt_row_sidebar .o_gantt_row_title .o_gantt_row_employee_avatar .o_standalone_avatar_employee', + 4, 'should have 4 employee avatars'); + + gantt.destroy(); + }); + }); +}); diff --git a/odex25_helpdesk/odex25_hr_gantt/views/assets.xml b/odex25_helpdesk/odex25_hr_gantt/views/assets.xml new file mode 100644 index 000000000..dfee8558c --- /dev/null +++ b/odex25_helpdesk/odex25_hr_gantt/views/assets.xml @@ -0,0 +1,16 @@ + + + + diff --git a/odex25_helpdesk/odex25_timesheet_grid/views/hr_timesheet_views.xml b/odex25_helpdesk/odex25_timesheet_grid/views/hr_timesheet_views.xml new file mode 100644 index 000000000..21120e554 --- /dev/null +++ b/odex25_helpdesk/odex25_timesheet_grid/views/hr_timesheet_views.xml @@ -0,0 +1,718 @@ + + + + + + + account.analytic.line.tree.hr_timesheet + account.analytic.line + + + + validated + timesheet_tree + 1 + + + + + + + + + + {'readonly': [('id', '!=', False), ('validated', '=', True)]} + + + {'readonly': [('id', '!=', False), ('validated', '=', True)]} + + + {'readonly': [('id', '!=', False), ('validated', '=', True)]} + + + {'readonly': [('id', '!=', False), ('validated', '=', True)]} + + + { + 'readonly': [ + ('id', '!=', False), + '|', + ('validated', '=', True), + ('timer_start', '!=', False) + ]} + + timesheet_uom_timer + + + + + + + account.analytic.line.kanban.odex25_timesheet_grid + account.analytic.line + + + + timesheet_kanban_view + 1 + + + + + + + + + + + timesheet_uom_timer + + + + + + + account.analytic.line.kanban.odex25_timesheet_grid + account.analytic.line + + + + + + + {'readonly': [('id', '!=', False), ('timer_start', '!=', False)]} + + + + + + + account.analytic.line.form + account.analytic.line + 4567 + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + account.analytic.line.grid.project + account.analytic.line + + + + + + + + + + + + + + + + account.analytic.line.grid.project + account.analytic.line + + + + + + + + + + + + + + + account.analytic.line.grid.project.readonly + account.analytic.line + + primary + + + false + false + + + + + + + + + + + account.analytic.line.grid.project.no.section + account.analytic.line + + primary + + + + + + + + + account.analytic.line.grid.employee + account.analytic.line + + + + + + + + + + + + + + + account.analytic.line.grid.employee.readonly + account.analytic.line + + primary + + + false + false + + + + + account.analytic.line.grid.employee.manager + account.analytic.line + + primary + + + 1 + + + + + + true + true + + + + + account.analytic.line.grid.employee.validation + account.analytic.line + + primary + + + + + + + +
diff --git a/odex25_helpdesk/odex_subscription_service/views/res_settings.xml b/odex25_helpdesk/odex_subscription_service/views/res_settings.xml new file mode 100644 index 000000000..b76b769ba --- /dev/null +++ b/odex25_helpdesk/odex_subscription_service/views/res_settings.xml @@ -0,0 +1,37 @@ + + + + + Config Settings for Subscription Services + res.config.settings + + + + +
+ +

Subscription Services

+
+
+
+
+
+
+ Notify Days Before Subscription End +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/odex25_helpdesk/odex_subscription_service/views/subscription_portal_templates.xml b/odex25_helpdesk/odex_subscription_service/views/subscription_portal_templates.xml new file mode 100644 index 000000000..f74abc7f3 --- /dev/null +++ b/odex25_helpdesk/odex_subscription_service/views/subscription_portal_templates.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + diff --git a/odex25_helpdesk/odex_subscription_service/views/subscription_service_views.xml b/odex25_helpdesk/odex_subscription_service/views/subscription_service_views.xml new file mode 100644 index 000000000..81497d6e3 --- /dev/null +++ b/odex25_helpdesk/odex_subscription_service/views/subscription_service_views.xml @@ -0,0 +1,250 @@ + + + + subscription.service.form.view + subscription.service + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + + subscription.service.list + subscription.service + + + + + + + + + + + + + + + + subscription.service.search + subscription.service + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + subscription.service.graph + subscription.service + + + + + + + + + + subscription.service.pivot + subscription.service + + + + + + + + + + Subscriptions Services + subscription.service + ir.actions.act_window + tree,form,pivot,graph + {'search_default_user_id':uid, 'user_id': uid,} + +

+ Click to create a new contract. +

+ Use contracts to follow tasks, issues, timesheets or invoicing based on + work done, expenses and/or purchase orders. Odoo will automatically manage + the alerts for the renewal of the contracts to the right salesperson. +

+
+
+ + + + + + + + Subscriptions to Renew + subscription.service + ir.actions.act_window + tree,form,pivot,graph + {'search_default_user_id':uid, 'search_default_pending':1, 'search_default_renew':1} + +

+ Click to define a new contract. +

+ You will find here the contracts to be renewed because the + end date is passed or the working effort is higher than the + maximum authorized one. +

+ Odoo automatically sets contracts to be renewed in a pending + state. After the negociation, the salesman should close or renew + pending contracts. +

+
+
+ + + + + + +