mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2025-02-17 18:50:46 +01:00
commit
ed321159fe
@ -291,6 +291,56 @@ Options that change ticket properties
|
|||||||
(7, _('7. Hot')),
|
(7, _('7. Hot')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Time Tracking Options
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- **HELPDESK_FOLLOWUP_TIME_SPENT_AUTO** If ``True``, calculate follow-up 'time_spent' with previous follow-up or ticket creation time.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_AUTO = False``
|
||||||
|
|
||||||
|
- **HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS** If defined, calculates follow-up 'time_spent' according to open hours.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {}``
|
||||||
|
|
||||||
|
If HELPDESK_FOLLOWUP_TIME_SPENT_AUTO is ``True``, you may set open hours to remove off hours from 'time_spent'::
|
||||||
|
|
||||||
|
HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS = {
|
||||||
|
"monday": (8.5, 19),
|
||||||
|
"tuesday": (8.5, 19),
|
||||||
|
"wednesday": (8.5, 19),
|
||||||
|
"thursday": (8.5, 19),
|
||||||
|
"friday": (8.5, 19),
|
||||||
|
"saturday": (0, 0),
|
||||||
|
"sunday": (0, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
Valid hour values must be set between 0 and 23.9999.
|
||||||
|
In this example 8.5 is interpreted as 8:30AM, saturdays and sundays don't count.
|
||||||
|
|
||||||
|
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS** List of days in format "%Y-%m-%d" to exclude from automatic follow-up 'time_spent' calculation.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ()``
|
||||||
|
|
||||||
|
This example removes Christmas and New Year's Eve in 2024::
|
||||||
|
|
||||||
|
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = ("2024-12-25", "2024-12-31",)
|
||||||
|
|
||||||
|
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES** List of ticket statuses to exclude from automatic follow-up 'time_spent' calculation.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = ()``
|
||||||
|
|
||||||
|
This example will have follow-ups to resolved ticket status not to be counted in::
|
||||||
|
|
||||||
|
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = (HELPDESK_TICKET_RESOLVED_STATUS,)
|
||||||
|
|
||||||
|
- **HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES** List of ticket queues slugs to exclude from automatic follow-up 'time_spent' calculation.
|
||||||
|
|
||||||
|
**Default:** ``HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ()``
|
||||||
|
|
||||||
|
This example will have follow-ups to ticket queue 'time-not-counting-queue' not to be counted in::
|
||||||
|
|
||||||
|
HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = ('time-not-counting-queue',)
|
||||||
|
|
||||||
Staff Ticket Creation Settings
|
Staff Ticket Creation Settings
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ lib.py - Common functions (eg multipart e-mail)
|
|||||||
|
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
from helpdesk.settings import CUSTOMFIELD_DATE_FORMAT, CUSTOMFIELD_DATETIME_FORMAT, CUSTOMFIELD_TIME_FORMAT
|
||||||
import logging
|
import logging
|
||||||
@ -173,11 +173,10 @@ def format_time_spent(time_spent):
|
|||||||
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
|
"""Format time_spent attribute to "[H]HHh:MMm" text string to be allign in
|
||||||
all graphical outputs
|
all graphical outputs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if time_spent:
|
if time_spent:
|
||||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
time_spent = "{0:02d}h:{1:02d}m".format(
|
||||||
time_spent.seconds // 3600,
|
int(time_spent.total_seconds()) // 3600,
|
||||||
time_spent.seconds // 60
|
int(time_spent.total_seconds()) % 3600 // 60
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
time_spent = ""
|
time_spent = ""
|
||||||
@ -194,3 +193,45 @@ def convert_value(value):
|
|||||||
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
return value.strftime(CUSTOMFIELD_TIME_FORMAT)
|
||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def daily_time_spent_calculation(earliest, latest, open_hours):
|
||||||
|
"""Returns the number of seconds for a single day time interval according to open hours."""
|
||||||
|
|
||||||
|
# avoid rendering day in different locale
|
||||||
|
weekday = ('monday', 'tuesday', 'wednesday', 'thursday',
|
||||||
|
'friday', 'saturday', 'sunday')[earliest.weekday()]
|
||||||
|
|
||||||
|
# enforce correct settings
|
||||||
|
MIDNIGHT = 23.9999
|
||||||
|
start, end = open_hours.get(weekday, (0, MIDNIGHT))
|
||||||
|
if not 0 <= start <= end <= MIDNIGHT:
|
||||||
|
raise ImproperlyConfigured("HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS"
|
||||||
|
f" setting for {weekday} out of (0, 23.9999) boundary")
|
||||||
|
|
||||||
|
# transform decimals to minutes and seconds
|
||||||
|
start_hour, start_minute, start_second = int(start), int(start % 1 * 60), int(start * 60 % 1 * 60)
|
||||||
|
end_hour, end_minute, end_second = int(end), int(end % 1 * 60), int(end * 60 % 1 * 60)
|
||||||
|
|
||||||
|
# translate time for delta calculation
|
||||||
|
earliest_f = earliest.hour + earliest.minute / 60 + earliest.second / 3600
|
||||||
|
latest_f = latest.hour + latest.minute / 60 + latest.second / 3600
|
||||||
|
|
||||||
|
if earliest_f < start:
|
||||||
|
earliest = earliest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||||
|
elif earliest_f >= end:
|
||||||
|
earliest = earliest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||||
|
|
||||||
|
if latest_f < start:
|
||||||
|
latest = latest.replace(hour=start_hour, minute=start_minute, second=start_second)
|
||||||
|
elif latest_f >= end:
|
||||||
|
latest = latest.replace(hour=end_hour, minute=end_minute, second=end_second)
|
||||||
|
|
||||||
|
day_delta = latest - earliest
|
||||||
|
|
||||||
|
# returns up to 86399 seconds, add one second if full day
|
||||||
|
time_spent_seconds = day_delta.seconds
|
||||||
|
if time_spent_seconds == 86399:
|
||||||
|
time_spent_seconds += 1
|
||||||
|
|
||||||
|
return time_spent_seconds
|
@ -8,7 +8,7 @@ models.py - Model (and hence database) definitions. This is the core of the
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from .lib import convert_value
|
from .lib import format_time_spent, convert_value, daily_time_spent_calculation
|
||||||
from .templated_email import send_templated_mail
|
from .templated_email import send_templated_mail
|
||||||
from .validators import validate_file_extension
|
from .validators import validate_file_extension
|
||||||
from .webhooks import send_new_ticket_webhook
|
from .webhooks import send_new_ticket_webhook
|
||||||
@ -33,17 +33,6 @@ from rest_framework import serializers
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def format_time_spent(time_spent):
|
|
||||||
if time_spent:
|
|
||||||
time_spent = "{0:02d}h:{1:02d}m".format(
|
|
||||||
time_spent.seconds // 3600,
|
|
||||||
time_spent.seconds % 3600 // 60
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
time_spent = ""
|
|
||||||
return time_spent
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeHtml(Extension):
|
class EscapeHtml(Extension):
|
||||||
def extendMarkdown(self, md):
|
def extendMarkdown(self, md):
|
||||||
md.preprocessors.deregister('html_block')
|
md.preprocessors.deregister('html_block')
|
||||||
@ -1000,9 +989,12 @@ class FollowUp(models.Model):
|
|||||||
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
return u"%s#followup%s" % (self.ticket.get_absolute_url(), self.id)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
t = self.ticket
|
self.ticket.modified = timezone.now()
|
||||||
t.modified = timezone.now()
|
self.ticket.save()
|
||||||
t.save()
|
|
||||||
|
if helpdesk_settings.FOLLOWUP_TIME_SPENT_AUTO and not self.time_spent:
|
||||||
|
self.time_spent = self.time_spent_calculation()
|
||||||
|
|
||||||
super(FollowUp, self).save(*args, **kwargs)
|
super(FollowUp, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def get_markdown(self):
|
def get_markdown(self):
|
||||||
@ -1012,6 +1004,62 @@ class FollowUp(models.Model):
|
|||||||
def time_spent_formated(self):
|
def time_spent_formated(self):
|
||||||
return format_time_spent(self.time_spent)
|
return format_time_spent(self.time_spent)
|
||||||
|
|
||||||
|
def time_spent_calculation(self):
|
||||||
|
"Returns timedelta according to rules settings."
|
||||||
|
|
||||||
|
# extract earliest from previous follow-up or ticket
|
||||||
|
try:
|
||||||
|
prev_fup_qs = self.ticket.followup_set.all()
|
||||||
|
if self.id:
|
||||||
|
prev_fup_qs = prev_fup_qs.filter(id__lt=self.id)
|
||||||
|
prev_fup = prev_fup_qs.latest("date")
|
||||||
|
earliest = prev_fup.date
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
earliest = self.ticket.created
|
||||||
|
|
||||||
|
# extract previous status from follow-up or ticket
|
||||||
|
try:
|
||||||
|
prev_fup_qs = self.ticket.followup_set.exclude(new_status__isnull=True)
|
||||||
|
if self.id:
|
||||||
|
prev_fup_qs = prev_fup_qs.filter(id__lt=self.id)
|
||||||
|
prev_fup = prev_fup_qs.latest("date")
|
||||||
|
prev_status = prev_fup.new_status
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
prev_status = self.ticket.status
|
||||||
|
|
||||||
|
# latest time is current follow-up date
|
||||||
|
latest = self.date
|
||||||
|
|
||||||
|
time_spent_seconds = 0
|
||||||
|
open_hours = helpdesk_settings.FOLLOWUP_TIME_SPENT_OPENING_HOURS
|
||||||
|
holidays = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS
|
||||||
|
exclude_statuses = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES
|
||||||
|
exclude_queues = helpdesk_settings.FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES
|
||||||
|
|
||||||
|
# split time interval by days
|
||||||
|
days = latest.toordinal() - earliest.toordinal()
|
||||||
|
for day in range(days + 1):
|
||||||
|
if day == 0:
|
||||||
|
start_day_time = earliest
|
||||||
|
if days == 0:
|
||||||
|
# close single day case
|
||||||
|
end_day_time = latest
|
||||||
|
else:
|
||||||
|
end_day_time = earliest.replace(hour=23, minute=59, second=59)
|
||||||
|
elif day == days:
|
||||||
|
start_day_time = latest.replace(hour=0, minute=0, second=0)
|
||||||
|
end_day_time = latest
|
||||||
|
else:
|
||||||
|
middle_day_time = earliest + datetime.timedelta(days=day)
|
||||||
|
start_day_time = middle_day_time.replace(hour=0, minute=0, second=0)
|
||||||
|
end_day_time = middle_day_time.replace(hour=23, minute=59, second=59)
|
||||||
|
|
||||||
|
if (start_day_time.strftime("%Y-%m-%d") not in holidays and
|
||||||
|
prev_status not in exclude_statuses and
|
||||||
|
self.ticket.queue.slug not in exclude_queues):
|
||||||
|
time_spent_seconds += daily_time_spent_calculation(start_day_time, end_day_time, open_hours)
|
||||||
|
|
||||||
|
return datetime.timedelta(seconds=time_spent_seconds)
|
||||||
|
|
||||||
class TicketChange(models.Model):
|
class TicketChange(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -151,6 +151,36 @@ TICKET_PRIORITY_CHOICES = getattr(settings,
|
|||||||
'HELPDESK_TICKET_PRIORITY_CHOICES',
|
'HELPDESK_TICKET_PRIORITY_CHOICES',
|
||||||
DEFAULT_TICKET_PRIORITY_CHOICES)
|
DEFAULT_TICKET_PRIORITY_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# time tracking options #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
# Follow-ups automatic time_spent calculation
|
||||||
|
FOLLOWUP_TIME_SPENT_AUTO = getattr(settings,
|
||||||
|
'HELPDESK_FOLLOWUP_TIME_SPENT_AUTO',
|
||||||
|
False)
|
||||||
|
|
||||||
|
# Calculate time_spent according to open hours
|
||||||
|
FOLLOWUP_TIME_SPENT_OPENING_HOURS = getattr(settings,
|
||||||
|
'HELPDESK_FOLLOWUP_TIME_SPENT_OPENING_HOURS',
|
||||||
|
{})
|
||||||
|
|
||||||
|
# Holidays don't count for time_spent calculation
|
||||||
|
FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS = getattr(settings,
|
||||||
|
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_HOLIDAYS',
|
||||||
|
())
|
||||||
|
|
||||||
|
# Time doesn't count for listed ticket statuses
|
||||||
|
FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES = getattr(settings,
|
||||||
|
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_STATUSES',
|
||||||
|
())
|
||||||
|
|
||||||
|
# Time doesn't count for listed queues slugs
|
||||||
|
FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES = getattr(settings,
|
||||||
|
'HELPDESK_FOLLOWUP_TIME_SPENT_EXCLUDE_QUEUES',
|
||||||
|
())
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# options for public pages #
|
# options for public pages #
|
||||||
############################
|
############################
|
||||||
|
Loading…
Reference in New Issue
Block a user