[Rt-commit] rt branch, 4.4/sla, created. rt-4.2.11-141-g8bfb984
? sunnavy
sunnavy at bestpractical.com
Sun Aug 2 15:17:53 EDT 2015
The branch, 4.4/sla has been created
at 8bfb984257cd45a4ef9811a4ba10607b4e3b570a (commit)
- Log -----------------------------------------------------------------
commit c0b308ef30c4a617bf353142b9c07871486c93bb
Author: sunnavy <sunnavy at bestpractical.com>
Date: Thu Jul 9 01:43:20 2015 +0800
import sla extension
this version works exactly like the sla extension.
it's roughly a copy/paste with necessary renamings
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 1c88a11..5deaade 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -118,11 +118,10 @@ Set($Timezone, "US/Eastern");
Once a plugin has been downloaded and installed, use C<Plugin()> to add
to the enabled C<@Plugins> list:
- Plugin( "RT::Extension::SLA" );
Plugin( "RT::Authen::ExternalAuth" );
-RT will also accept the distribution name (i.e. C<RT-Extension-SLA>)
-instead of the package name (C<RT::Extension::SLA>).
+RT will also accept the distribution name (i.e. C<RT-Authen-ExternalAuth>)
+instead of the package name (C<RT::Authen::ExternalAuth>).
=cut
diff --git a/etc/RT_SiteConfig.pm b/etc/RT_SiteConfig.pm
index a5e8c46..0f839a6 100644
--- a/etc/RT_SiteConfig.pm
+++ b/etc/RT_SiteConfig.pm
@@ -25,7 +25,6 @@ Set( $rtname, 'example.com');
# You must install Plugins on your own, this is only an example
# of the correct syntax to use when activating them:
-# Plugin( "RT::Extension::SLA" );
# Plugin( "RT::Authen::ExternalAuth" );
1;
diff --git a/etc/initialdata b/etc/initialdata
index 5df770b..21650a6 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -14,6 +14,17 @@
@Groups = (
);
+ at CustomFields = (
+ {
+ Name => 'SLA',
+ Queue => 0,
+ Type => 'SelectSingle',
+ Disabled => 0,
+ Description => 'Service Level Agreement',
+ Values => [ ],
+ },
+);
+
@Queues = ({ Name => 'General',
Description => 'The default queue',
CorrespondAddress => "",
@@ -111,6 +122,18 @@
{ Name => 'Send Forward', # loc
Description => 'Send forwarded message', # loc
ExecModule => 'SendForward', },
+ { Name => '[SLA] Set default service level', # loc
+ Description => 'Set service level according to the config' , # loc
+ ExecModule => 'SLA_SetDefault',
+ },
+ { Name => '[SLA] Set starts date', # loc
+ Description => 'Set the starts date according to an agreement' , # loc
+ ExecModule => 'SLA_SetStarts',
+ },
+ { Name => '[SLA] Set due date', # loc
+ Description => 'Set the due date according to an agreement' , # loc
+ ExecModule => 'SLA_SetDue',
+ },
);
@ScripConditions = (
@@ -219,6 +242,21 @@
ApplicableTransTypes => 'Status,Set',
ExecModule => 'ReopenTicket',
},
+ { Name => '[SLA] Require default', # loc
+ Description => 'Detect a situation when we should set default service level' , # loc
+ ApplicableTransTypes => 'Create',
+ ExecModule => 'SLA_RequireDefault',
+ },
+ { Name => '[SLA] Require Starts set', # loc
+ Description => 'Detect a situation when we should set Starts date' , # loc
+ ApplicableTransTypes => 'Create,CustomField',
+ ExecModule => 'SLA_RequireStartsSet',
+ },
+ { Name => '[SLA] Require Due set', # loc
+ Description => 'Detect a situation when we should set Due date' , # loc
+ ApplicableTransTypes => 'Create,CustomField,Correspond,Set,Status',
+ ExecModule => 'SLA_RequireDueSet',
+ },
);
@@ -792,6 +830,18 @@ Hour: { $SubscriptionObj->SubValue('Hour') }
ScripCondition => 'On Forward Ticket',
ScripAction => 'Send Forward',
Template => 'Forward Ticket' },
+ { Description => "[SLA] Set default service level if needed",
+ ScripCondition => '[SLA] Require Default',
+ ScripAction => '[SLA] Set default service level',
+ Template => 'Blank' },
+ { Description => "[SLA] Set starts date if needed",
+ ScripCondition => '[SLA] Require starts set',
+ ScripAction => '[SLA] Set starts date',
+ Template => 'Blank' },
+ { Description => "[SLA] Set due date if needed",
+ ScripCondition => '[SLA] Require due set',
+ ScripAction => '[SLA] Set due date',
+ Template => 'Blank' },
);
@ACL = (
diff --git a/etc/upgrade/4.3.7/content b/etc/upgrade/4.3.7/content
new file mode 100644
index 0000000..2c24760
--- /dev/null
+++ b/etc/upgrade/4.3.7/content
@@ -0,0 +1,62 @@
+use strict;
+use warnings;
+
+our @CustomFields = (
+ {
+ Name => 'SLA',
+ Queue => 0,
+ Type => 'SelectSingle',
+ Disabled => 0,
+ Description => 'Service Level Agreement',
+ Values => [ ],
+ },
+);
+
+our @ScripConditions = (
+ { Name => '[SLA] Require default', # loc
+ Description => 'Detect a situation when we should set default service level' , # loc
+ ApplicableTransTypes => 'Create',
+ ExecModule => 'SLA_RequireDefault',
+ },
+ { Name => '[SLA] Require Starts set', # loc
+ Description => 'Detect a situation when we should set Starts date' , # loc
+ ApplicableTransTypes => 'Create,CustomField',
+ ExecModule => 'SLA_RequireStartsSet',
+ },
+ { Name => '[SLA] Require Due set', # loc
+ Description => 'Detect a situation when we should set Due date' , # loc
+ ApplicableTransTypes => 'Create,CustomField,Correspond,Set,Status',
+ ExecModule => 'SLA_RequireDueSet',
+ },
+);
+
+our @ScripActions = (
+ { Name => '[SLA] Set default service level', # loc
+ Description => 'Set service level according to the config' , # loc
+ ExecModule => 'SLA_SetDefault',
+ },
+ { Name => '[SLA] Set starts date', # loc
+ Description => 'Set the starts date according to an agreement' , # loc
+ ExecModule => 'SLA_SetStarts',
+ },
+ { Name => '[SLA] Set due date', # loc
+ Description => 'Set the due date according to an agreement' , # loc
+ ExecModule => 'SLA_SetDue',
+ },
+);
+
+our @Scrips = (
+ { Description => "[SLA] Set default service level if needed",
+ ScripCondition => '[SLA] Require Default',
+ ScripAction => '[SLA] Set default service level',
+ Template => 'Blank' },
+ { Description => "[SLA] Set starts date if needed",
+ ScripCondition => '[SLA] Require starts set',
+ ScripAction => '[SLA] Set starts date',
+ Template => 'Blank' },
+ { Description => "[SLA] Set due date if needed",
+ ScripCondition => '[SLA] Require due set',
+ ScripAction => '[SLA] Set due date',
+ Template => 'Blank' },
+);
+
diff --git a/lib/RT/Action/SLA.pm b/lib/RT/Action/SLA.pm
new file mode 100644
index 0000000..d54216e
--- /dev/null
+++ b/lib/RT/Action/SLA.pm
@@ -0,0 +1,102 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+
+use strict;
+use warnings;
+
+package RT::Action::SLA;
+
+use base qw(RT::SLA RT::Action);
+
+=head1 NAME
+
+RT::Action::SLA - base class for all actions in the extension
+
+=head1 DESCRIPTION
+
+It's not a real action, but container for subclassing which provide
+help methods for other actions.
+
+=head1 METHODS
+
+=head2 SetDateField NAME VALUE
+
+Sets specified ticket's date field to the value, doesn't update
+if field is set already. VALUE is unix time.
+
+=cut
+
+sub SetDateField {
+ my $self = shift;
+ my ($type, $value) = (@_);
+
+ my $ticket = $self->TicketObj;
+
+ my $method = $type .'Obj';
+ if ( defined $value ) {
+ return 1 if $ticket->$method->Unix == $value;
+ } else {
+ return 1 if $ticket->$method->Unix <= 0;
+ }
+
+ my $date = RT::Date->new( $RT::SystemUser );
+ $date->Set( Format => 'unix', Value => $value );
+
+ $method = 'Set'. $type;
+ return 1 if $ticket->$type eq $date->ISO;
+ my ($status, $msg) = $ticket->$method( $date->ISO );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't set $type date: $msg");
+ return 0;
+ }
+
+ return 1;
+}
+
+1;
diff --git a/lib/RT/Action/SLA_SetDefault.pm b/lib/RT/Action/SLA_SetDefault.pm
new file mode 100644
index 0000000..1f06a19
--- /dev/null
+++ b/lib/RT/Action/SLA_SetDefault.pm
@@ -0,0 +1,104 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetDefault;
+
+use base qw(RT::Action::SLA);
+
+=head1 NAME
+
+RT::Action::SLA_SetDefault - set default SLA value
+
+=head1 DESCRIPTION
+
+Sets a default level of service. Transaction's created field is used
+to calculate if things happen in hours or out of. Default value then
+figured from L<InHoursDefault|XXX> and L<OutOfHoursDefault|XXX> options.
+
+This action doesn't check if the ticket has a value already, so you
+have to use it with condition that checks this fact for you, however
+such behaviour allows you to force setting up default using custom
+condition. The default condition for this action is
+L<RT::Condition::SLA_RequireDefault>.
+
+=cut
+
+sub Prepare { return 1 }
+sub Commit {
+ my $self = shift;
+
+ my $cf = $self->GetCustomField;
+ unless ( $cf->id ) {
+ $RT::Logger->warning("SLA scrip applied to a queue that has no SLA CF");
+ return 1;
+ }
+
+ my $level = $self->GetDefaultServiceLevel;
+ unless ( $level ) {
+ $RT::Logger->info(
+ "No default service level for ticket #". $self->TicketObj->id
+ ." in queue ". $self->TicketObj->QueueObj->Name );
+ return 1;
+ }
+
+ my ($status, $msg) = $self->TicketObj->AddCustomFieldValue(
+ Field => $cf->id,
+ Value => $level,
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't set service level: $msg");
+ return 0;
+ }
+
+ return 1;
+};
+
+1;
diff --git a/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
new file mode 100644
index 0000000..c790df8
--- /dev/null
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -0,0 +1,159 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetDue;
+
+use base qw(RT::Action::SLA);
+
+=head2 Prepare
+
+Checks if the ticket has service level defined.
+
+=cut
+
+sub Prepare {
+ my $self = shift;
+
+ unless ( $self->TicketObj->FirstCustomFieldValue('SLA') ) {
+ $RT::Logger->error('SLA::SetDue scrip has been applied to ticket #'
+ . $self->TicketObj->id . ' that has no SLA defined');
+ return 0;
+ }
+
+ return 1;
+}
+
+=head2 Commit
+
+Set the Due date accordingly to SLA.
+
+=cut
+
+sub Commit {
+ my $self = shift;
+
+ my $ticket = $self->TicketObj;
+ my $txn = $self->TransactionObj;
+ my $level = $ticket->FirstCustomFieldValue('SLA');
+
+ my ($last_reply, $is_outside) = $self->LastEffectiveAct;
+ $RT::Logger->debug(
+ 'Last effective '. ($is_outside? '':'non-') .'outside actors\' reply'
+ .' to ticket #'. $ticket->id .' is txn #'. $last_reply->id
+ );
+
+ my $response_due = $self->Due(
+ Ticket => $ticket,
+ Level => $level,
+ Type => $is_outside? 'Response' : 'KeepInLoop',
+ Time => $last_reply->CreatedObj->Unix,
+ );
+
+ my $resolve_due = $self->Due(
+ Ticket => $ticket,
+ Level => $level,
+ Type => 'Resolve',
+ Time => $ticket->CreatedObj->Unix,
+ );
+
+ my $due;
+ $due = $response_due if defined $response_due;
+ $due = $resolve_due unless defined $due;
+ $due = $resolve_due if defined $due && defined $resolve_due && $resolve_due < $due;
+
+ return $self->SetDateField( Due => $due );
+}
+
+sub IsOutsideActor {
+ my $self = shift;
+ my $txn = shift || $self->TransactionObj;
+
+ my $actor = $txn->CreatorObj->PrincipalObj;
+
+ # owner is always treated as inside actor
+ return 0 if $actor->id == $self->TicketObj->Owner;
+
+ if ( $RT::ServiceAgreements{'AssumeOutsideActor'} ) {
+ # All non-admincc users are outside actors
+ return 0 if $self->TicketObj ->AdminCc->HasMemberRecursively( $actor )
+ or $self->TicketObj->QueueObj->AdminCc->HasMemberRecursively( $actor );
+
+ return 1;
+ } else {
+ # Only requestors are outside actors
+ return 1 if $self->TicketObj->Requestors->HasMemberRecursively( $actor );
+ return 0;
+ }
+}
+
+sub LastEffectiveAct {
+ my $self = shift;
+
+ my $txns = $self->TicketObj->Transactions;
+ $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+ $txns->Limit( FIELD => 'Type', VALUE => 'Create' );
+ $txns->OrderByCols(
+ { FIELD => 'Created', ORDER => 'DESC' },
+ { FIELD => 'id', ORDER => 'DESC' },
+ );
+
+ my $res;
+ while ( my $txn = $txns->Next ) {
+ unless ( $self->IsOutsideActor( $txn ) ) {
+ last if $res;
+ return ($txn);
+ }
+ $res = $txn;
+ }
+ return ($res, 1);
+}
+
+1;
diff --git a/lib/RT/Action/SLA_SetStarts.pm b/lib/RT/Action/SLA_SetStarts.pm
new file mode 100644
index 0000000..59b2fe1
--- /dev/null
+++ b/lib/RT/Action/SLA_SetStarts.pm
@@ -0,0 +1,92 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetStarts;
+
+use base qw(RT::Action::SLA);
+
+=head1 NAME
+
+RT::Action::SLA_SetStarts - set starts date field of a ticket according to SLA
+
+=head1 DESCRIPTION
+
+Look up the SLA of the ticket and set the Starts date accordingly. Nothing happens
+if the ticket has no SLA defined.
+
+Note that this action doesn't check if Starts field is set already, so you can
+use it to set the field in a force mode or can protect field using a condition
+that checks value of Starts.
+
+=cut
+
+sub Prepare { return 1 }
+
+sub Commit {
+ my $self = shift;
+
+ my $ticket = $self->TicketObj;
+
+ my $level = $ticket->FirstCustomFieldValue('SLA');
+ unless ( $level ) {
+ $RT::Logger->debug('Ticket #'. $ticket->id .' has no service level defined, skip setting Starts');
+ return 1;
+ }
+
+ my $starts = $self->Starts(
+ Ticket => $ticket,
+ Level => $level,
+ Time => $ticket->CreatedObj->Unix,
+ );
+
+ return $self->SetDateField( Starts => $starts );
+}
+
+1;
diff --git a/lib/RT/Condition/SLA.pm b/lib/RT/Condition/SLA.pm
new file mode 100644
index 0000000..b164fae
--- /dev/null
+++ b/lib/RT/Condition/SLA.pm
@@ -0,0 +1,82 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Condition::SLA;
+use base qw(RT::SLA RT::Condition);
+
+=head1 SLAIsApplied
+
+=cut
+
+sub SLAIsApplied { return 1 }
+
+=head1 IsCustomFieldChange
+
+=cut
+
+sub IsCustomFieldChange {
+ my $self = shift;
+ my $cf_name = shift;
+
+ my $txn = $self->TransactionObj;
+
+ return 0 unless $txn->Type eq 'CustomField';
+
+ my $cf = $self->GetCustomField( CustomField => $cf_name );
+ unless ( $cf->id ) {
+ $RT::Logger->debug("Custom field '$cf_name' is not applied to ticket #". $self->TicketObj->id);
+ return 0;
+ }
+ return 0 unless $cf->id == $txn->Field;
+ return 1;
+}
+
+1;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
new file mode 100644
index 0000000..bc49970
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -0,0 +1,74 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireDefault;
+use base qw(RT::Condition::SLA);
+
+=head1 IsApplicable
+
+Applies the current scrip when SLA is not set. Returns true on create,
+but only if SLA CustomField is applied to the ticket and it has no
+value set.
+
+=cut
+
+sub IsApplicable {
+ my $self = shift;
+ return 0 unless $self->TransactionObj->Type eq 'Create';
+ my $ticket = $self->TicketObj;
+ return 0 unless lc($ticket->Type) eq 'ticket';
+ return 0 if $ticket->FirstCustomFieldValue('SLA');
+ return 0 unless $self->SLAIsApplied;
+ return 1;
+}
+
+1;
+
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
new file mode 100644
index 0000000..d436c27
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -0,0 +1,83 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireDueSet;
+
+use base qw(RT::Condition::SLA);
+
+=head1 NAME
+
+RT::Condition::SLA_RequireDueSet - checks if Due date require update
+
+=head1 DESCRIPTION
+
+Checks if Due date require update. This should be done when we create
+a ticket and it has service level value or when we set service level.
+
+=cut
+
+sub IsApplicable {
+ my $self = shift;
+ return 0 unless $self->SLAIsApplied;
+
+ my $type = $self->TransactionObj->Type;
+ if ( $type eq 'Create' || $type eq 'Correspond' ) {
+ return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 0;
+ }
+ elsif ( $type eq 'Status' || ($type eq 'Set' && $self->TransactionObj->Field eq 'Status') ) {
+ return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 0;
+ }
+ return 1 if $self->IsCustomFieldChange('SLA');
+ return 0;
+}
+
+1;
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
new file mode 100644
index 0000000..895ccaa
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -0,0 +1,72 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireStartsSet;
+
+use base qw(RT::Condition::SLA);
+
+=head1 NAME
+
+RT::Condition::SLA_RequireStartsSet - checks if Starts date is not set
+
+=head1 DESCRIPTION
+
+Applies if Starts date is not set for the ticket.
+
+=cut
+
+sub IsApplicable {
+ my $self = shift;
+ return 0 if $self->TicketObj->StartsObj->Unix > 0;
+ return 0 unless $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 1;
+}
+
+1;
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 206f17f..ba9d023 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1190,6 +1190,30 @@ sub SetDefaultValue {
}
}
+sub SLA {
+ my $self = shift;
+ my $value = shift;
+ return undef unless $self->CurrentUserHasRight('SeeQueue');
+
+ my $attr = $self->FirstAttribute('SLA') or return undef;
+ return $attr->Content;
+}
+
+sub SetSLA {
+ my $self = shift;
+ my $value = shift;
+
+ return ( 0, $self->loc('Permission Denied') )
+ unless $self->CurrentUserHasRight('AdminQueue');
+
+ my ($status, $msg) = $self->SetAttribute(
+ Name => 'SLA',
+ Description => 'Default Queue SLA',
+ Content => $value,
+ );
+ return ($status, $msg) unless $status;
+ return ($status, $self->loc("Queue's default service level has been changed"));
+}
RT::Base->_ImportOverlays();
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
new file mode 100644
index 0000000..817e5e6
--- /dev/null
+++ b/lib/RT/SLA.pm
@@ -0,0 +1,595 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::SLA;
+
+=head1 NAME
+
+RT::SLA - Service Level Agreements for RT
+
+=head1 DESCRIPTION
+
+Automated due dates using service levels.
+
+=head1 CONFIGURATION
+
+Service level agreements of tickets is controlled by an SLA custom field (CF).
+This field is created during C<make initdb> step (above) and applied globally.
+This CF MUST be of C<select one value> type. Values of the CF define the
+service levels.
+
+It's possible to define different set of levels for different
+queues. You can create several CFs with the same name and
+different set of values. But if you move tickets between
+queues a lot then it's going to be a problem and it's preferred
+to use B<ONE> SLA custom field.
+
+There is no WebUI in the current version. Almost everything is
+controlled in the RT's config using option C<%RT::ServiceAgreements>
+and C<%RT::ServiceBusinessHours>. For example:
+
+ %RT::ServiceAgreements = (
+ Default => '4h',
+ QueueDefault => {
+ 'Incident' => '2h',
+ },
+ Levels => {
+ '2h' => { Resolve => { RealMinutes => 60*2 } },
+ '4h' => { Resolve => { RealMinutes => 60*4 } },
+ },
+ );
+
+In this example I<Incident> is the name of the queue, and I<2h> is the name of
+the SLA which will be applied to this queue by default.
+
+Each service level can be described using several options:
+L<Starts|/"Starts (interval, first business minute)">,
+L<Resolve|/"Resolve and Response (interval, no defaults)">,
+L<Response|/"Resolve and Response (interval, no defaults)">,
+L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
+L<OutOfHours|/"OutOfHours (struct, no default)">
+and L<ServiceBusinessHours|/"Configuring business hours">.
+
+=head2 Starts (interval, first business minute)
+
+By default when a ticket is created Starts date is set to
+first business minute after time of creation. In other
+words if a ticket is created during business hours then
+Starts will be equal to Created time, otherwise Starts will
+be beginning of the next business day.
+
+However, if you provide 24/7 support then you most
+probably would be interested in Starts to be always equal
+to Created time.
+
+Starts option can be used to adjust behaviour. Format
+of the option is the same as format for deadlines which
+described later in details. RealMinutes, BusinessMinutes
+options and OutOfHours modifiers can be used here like
+for any other deadline. For example:
+
+ 'standard' => {
+ # give people 15 minutes
+ Starts => { BusinessMinutes => 15 },
+ },
+
+You can still use old option StartImmediately to set
+Starts date equal to Created date.
+
+Example:
+
+ '24/7' => {
+ StartImmediately => 1,
+ Response => { RealMinutes => 30 },
+ },
+
+But it's the same as:
+
+ '24/7' => {
+ Starts => { RealMinutes => 0 },
+ Response => { RealMinutes => 30 },
+ },
+
+=head2 Resolve and Response (interval, no defaults)
+
+These two options define deadlines for resolve of a ticket
+and reply to customer(requestors) questions accordingly.
+
+You can define them using real time, business or both. Read more
+about the latter L<below|/"Using both Resolve and Response in the same level">.
+
+The Due date field is used to store calculated deadlines.
+
+=head3 Resolve
+
+Defines deadline when a ticket should be resolved. This option is
+quite simple and straightforward when used without L</Response>.
+
+Example:
+
+ # 8 business hours
+ 'simple' => { Resolve => 60*8 },
+ ...
+ # one real week
+ 'hard' => { Resolve => { RealMinutes => 60*24*7 } },
+
+=head3 Response
+
+In many companies providing support service(s) resolve time of a ticket
+is less important than time of response to requestors from staff
+members.
+
+You can use Response option to define such deadlines. The Due date is
+set when a ticket is created, unset when a worker replies, and re-set
+when the requestor replies again -- until the ticket is closed, when the
+ticket's Due date is unset.
+
+B<NOTE> that this behaviour changes when Resolve and Response options
+are combined; see L</"Using both Resolve and Response in the same
+level">.
+
+Note that by default, only the requestors on the ticket are considered
+"outside actors" and thus require a Response due date; all other email
+addresses are treated as workers of the ticket, and thus count as
+meeting the SLA. If you'd like to invert this logic, so that the Owner
+and AdminCcs are the only worker email addresses, and all others are
+external, see the L</AssumeOutsideActor> configuration.
+
+The owner is never treated as an outside actor; if they are also the
+requestor of the ticket, it will have no SLA.
+
+If an outside actor replies multiple times, their later replies are
+ignored; the deadline is awlways calculated from the oldest
+correspondence from the outside actor.
+
+
+=head3 Using both Resolve and Response in the same level
+
+Resolve and Response can be combined. In such case due date is set
+according to the earliest of two deadlines and never is dropped to
+'not set'.
+
+If a ticket met its Resolve deadline then due date stops "flipping",
+is freezed and the ticket becomes overdue. Before that moment when
+an inside actor replies to a ticket, due date is changed to Resolve
+deadline instead of 'Not Set', as well this happens when a ticket
+is closed. So all the time due date is defined.
+
+Example:
+
+ 'standard delivery' => {
+ Response => { RealMinutes => 60*1 }, # one hour
+ Resolve => { RealMinutes => 60*24 }, # 24 real hours
+ },
+
+A client orders goods and due date of the order is set to the next one
+hour, you have this hour to process the order and write a reply.
+As soon as goods are delivered you resolve tickets and usually meet
+Resolve deadline, but if you don't resolve or user replies then most
+probably there are problems with delivery of the goods. And if after
+a week you keep replying to the client and always meeting one hour
+response deadline that doesn't mean the ticket is not over due.
+Due date was frozen 24 hours after creation of the order.
+
+=head3 Using business and real time in one option
+
+It's quite rare situation when people need it, but we've decided
+that business is applied first and then real time when deadline
+described using both types of time. For example:
+
+ 'delivery' => {
+ Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
+ },
+ 'fast delivery' {
+ StartImmediately => 1,
+ Resolve => { RealMinutes => 60*8 },
+ },
+
+For delivery requests which come into the system during business
+hours these levels define the same deadlines, otherwise the first
+level set deadline to 8 real hours starting from the next business
+day, when tickets with the second level should be resolved in the
+next 8 hours after creation.
+
+=head2 Keep in loop (interval, no defaults)
+
+If response deadline is used then Due date is changed to repsonse
+deadline or to "Not Set" when staff replies to a ticket. In some
+cases you want to keep requestors in loop and keed them up to date
+every few hours. KeepInLoop option can be used to achieve this.
+
+ 'incident' => {
+ Response => { RealMinutes => 60*1 }, # one hour
+ KeepInLoop => { RealMinutes => 60*2 }, # two hours
+ Resolve => { RealMinutes => 60*24 }, # 24 real hours
+ },
+
+In the above example Due is set to one hour after creation, reply
+of a inside actor moves Due date two hours forward, outside actors'
+replies move Due date to one hour and resolve deadine is 24 hours.
+
+=head2 Modifying Agreements
+
+=head3 OutOfHours (struct, no default)
+
+Out of hours modifier. Adds more real or business minutes to resolve
+and/or reply options if event happens out of business hours, read also
+</"Configuring business hours"> below.
+
+Example:
+
+ 'level x' => {
+ OutOfHours => { Resolve => { RealMinutes => +60*24 } },
+ Resolve => { RealMinutes => 60*24 },
+ },
+
+If a request comes into the system during night then supporters have two
+hours, otherwise only one.
+
+ 'level x' => {
+ OutOfHours => { Response => { BusinessMinutes => +60*2 } },
+ Resolve => { BusinessMinutes => 60 },
+ },
+
+Supporters have two additional hours in the morning to deal with bunch
+of requests that came into the system during the last night.
+
+=head3 IgnoreOnStatuses (array, no default)
+
+Allows you to ignore a deadline when ticket has certain status. Example:
+
+ 'level x' => {
+ KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
+ },
+
+In above example KeepInLoop deadline is ignored if ticket is stalled.
+
+B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
+Due date is calculated from the last action (reply, SLA change, etc) which fits
+the SLA type (Response, Starts, KeepInLoop, etc). This means if a ticket in
+the above example flips from stalled to open without a reply, the ticket will
+probably be overdue. In most cases this shouldn't be a problem since moving
+out of stalled-like statuses is often the result of RT's auto-open on reply
+scrip, therefore ensuring there's a new reply to calculate Due from. The
+overall effect is that ignored statuses don't let the Due date drift
+arbitrarily, which could wreak havoc on your SLA performance.
+C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
+overdue" issue by excluding the time spent on ignored statuses.
+
+ 'level x' => {
+ KeepInLoop => {
+ BusinessMinutes => 60,
+ ExcludeTimeOnIgnoredStatuses => 1,
+ IgnoreOnStatuses => ['stalled'],
+ },
+ },
+
+=head2 Configuring business hours
+
+In the config you can set one or more work schedules. Use the following
+format:
+
+ %RT::ServiceBusinessHours = (
+ 'Default' => {
+ ... description ...
+ },
+ 'Support' => {
+ ... description ...
+ },
+ 'Sales' => {
+ ... description ...
+ },
+ );
+
+Read more about how to describe a schedule in L<Business::Hours>.
+
+=head3 Defining different business hours for service levels
+
+Each level supports BusinessHours option to specify your own business
+hours.
+
+ 'level x' => {
+ BusinessHours => 'work just in Monday',
+ Resolve => { BusinessMinutes => 60 },
+ },
+
+then %RT::ServiceBusinessHours should have the corresponding definition:
+
+ %RT::ServiceBusinessHours = (
+ 'work just in Monday' => {
+ 1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
+ },
+ );
+
+Default Business Hours setting is in $RT::ServiceBusinessHours{'Default'}.
+
+=head2 Defining service levels per queue
+
+In the config you can set per queue defaults, using:
+
+ %RT::ServiceAgreements = (
+ Default => 'global default level of service',
+ QueueDefault => {
+ 'queue name' => 'default value for this queue',
+ ...
+ },
+ ...
+ };
+
+=head2 AssumeOutsideActor
+
+When using a L<Response|/"Resolve and Response (interval, no defaults)">
+configuration, the due date is unset when anyone who is not a requestor
+replies. If it is common for non-requestors to reply to tickets, and
+this should I<not> satisfy the SLA, you may wish to set
+C<AssumeOutsideActor>. This causes the extension to assume that the
+Response SLA has only been met when the owner or AdminCc reply.
+
+ %RT::ServiceAgreements = (
+ AssumeOutsideActor => 1,
+ ...
+ };
+
+=head2 Access control
+
+You can totally hide SLA custom field from users and use per queue
+defaults, just revoke SeeCustomField and ModifyCustomField.
+
+If you want people to see the current service level ticket is assigned
+to then grant SeeCustomField right.
+
+You may want to allow customers or managers to escalate thier tickets.
+Just grant them ModifyCustomField right.
+
+=cut
+
+sub BusinessHours {
+ my $self = shift;
+ my $name = shift || 'Default';
+
+ require Business::Hours;
+ my $res = new Business::Hours;
+ $res->business_hours( %{ $RT::ServiceBusinessHours{ $name } } )
+ if $RT::ServiceBusinessHours{ $name };
+ return $res;
+}
+
+sub Agreement {
+ my $self = shift;
+ my %args = (
+ Level => undef,
+ Type => 'Response',
+ Time => undef,
+ Ticket => undef,
+ Queue => undef,
+ @_
+ );
+
+ my $meta = $RT::ServiceAgreements{'Levels'}{ $args{'Level'} };
+ return undef unless $meta;
+
+ if ( exists $meta->{'StartImmediately'} || !defined $meta->{'Starts'} ) {
+ $meta->{'Starts'} = {
+ delete $meta->{'StartImmediately'}
+ ? ( )
+ : ( BusinessMinutes => 0 )
+ ,
+ };
+ }
+
+ return undef unless $meta->{ $args{'Type'} };
+
+ my %res;
+ if ( ref $meta->{ $args{'Type'} } ) {
+ %res = %{ $meta->{ $args{'Type'} } };
+ } elsif ( $meta->{ $args{'Type'} } =~ /^\d+$/ ) {
+ %res = ( BusinessMinutes => $meta->{ $args{'Type'} } );
+ } else {
+ $RT::Logger->error("Levels of SLA should be either number or hash ref");
+ return undef;
+ }
+
+ if ( $args{'Ticket'} && $res{'IgnoreOnStatuses'} ) {
+ my $status = $args{'Ticket'}->Status;
+ return undef if grep $_ eq $status, @{$res{'IgnoreOnStatuses'}};
+ }
+
+ $res{'OutOfHours'} = $meta->{'OutOfHours'}{ $args{'Type'} };
+
+ $args{'Queue'} ||= $args{'Ticket'}->QueueObj if $args{'Ticket'};
+ if ( $args{'Queue'} && ref $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+ $res{'Timezone'} = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name }{'Timezone'};
+ }
+ $res{'Timezone'} ||= $meta->{'Timezone'} || $RT::Timezone;
+
+ $res{'BusinessHours'} = $meta->{'BusinessHours'};
+
+ return \%res;
+}
+
+sub Due {
+ my $self = shift;
+ return $self->CalculateTime( @_ );
+}
+
+sub Starts {
+ my $self = shift;
+ return $self->CalculateTime( @_, Type => 'Starts' );
+}
+
+sub CalculateTime {
+ my $self = shift;
+ my %args = (@_);
+ my $agreement = $args{'Agreement'} || $self->Agreement( @_ );
+ return undef unless $agreement and ref $agreement eq 'HASH';
+
+ my $res = $args{'Time'};
+
+ my $ok = eval {
+ local $ENV{'TZ'} = $ENV{'TZ'};
+ if ( $agreement->{'Timezone'} && $agreement->{'Timezone'} ne ($ENV{'TZ'}||'') ) {
+ $ENV{'TZ'} = $agreement->{'Timezone'};
+ require POSIX; POSIX::tzset();
+ }
+
+ my $bhours = $self->BusinessHours( $agreement->{'BusinessHours'} );
+
+ if ( $agreement->{'OutOfHours'} && $bhours->first_after( $res ) != $res ) {
+ foreach ( qw(RealMinutes BusinessMinutes) ) {
+ next unless my $mod = $agreement->{'OutOfHours'}{ $_ };
+ ($agreement->{ $_ } ||= 0) += $mod;
+ }
+ }
+
+ if ( $args{ Ticket }
+ && $agreement->{ IgnoreOnStatuses }
+ && $agreement->{ ExcludeTimeOnIgnoredStatuses } )
+ {
+ my $txns = RT::Transactions->new( RT->SystemUser );
+ $txns->LimitToTicket($args{Ticket}->id);
+ $txns->Limit(
+ FIELD => 'Field',
+ VALUE => 'Status',
+ );
+ my $date = RT::Date->new( RT->SystemUser );
+ $date->Set( Value => $args{ Time } );
+ $txns->Limit(
+ FIELD => 'Created',
+ OPERATOR => '>=',
+ VALUE => $date->ISO( Timezone => 'UTC' ),
+ );
+
+ my $last_time = $args{ Time };
+ while ( my $txn = $txns->Next ) {
+ if ( grep( { $txn->OldValue eq $_ } @{ $agreement->{ IgnoreOnStatuses } } ) ) {
+ if ( !grep( { $txn->NewValue eq $_ } @{ $agreement->{ IgnoreOnStatuses } } ) ) {
+ if ( defined $agreement->{ 'BusinessMinutes' } ) {
+
+ # re-init $bhours to make sure we don't have a cached start/end,
+ # so the time here is not outside the calculated business hours
+
+ my $bhours = $self->BusinessHours( $agreement->{ 'BusinessHours' } );
+ my $time = $bhours->between( $last_time, $txn->CreatedObj->Unix );
+ if ( $time > 0 ) {
+ $res = $bhours->add_seconds( $res, $time );
+ }
+ }
+ else {
+ my $time = $txn->CreatedObj->Unix - $last_time;
+ $res += $time;
+ }
+ $last_time = $txn->CreatedObj->Unix;
+ }
+ }
+ else {
+ $last_time = $txn->CreatedObj->Unix;
+ }
+ }
+ }
+
+ if ( defined $agreement->{'BusinessMinutes'} ) {
+ if ( $agreement->{'BusinessMinutes'} ) {
+ $res = $bhours->add_seconds(
+ $res, 60 * $agreement->{'BusinessMinutes'},
+ );
+ }
+ else {
+ $res = $bhours->first_after( $res );
+ }
+ }
+ $res += 60 * $agreement->{'RealMinutes'}
+ if defined $agreement->{'RealMinutes'};
+ 1;
+ };
+
+ POSIX::tzset() if $agreement->{'Timezone'}
+ && $agreement->{'Timezone'} ne ($ENV{'TZ'}||'');
+ die $@ unless $ok;
+
+ return $res;
+}
+
+sub GetCustomField {
+ my $self = shift;
+ my %args = (Ticket => undef, CustomField => 'SLA', @_);
+ unless ( $args{'Ticket'} ) {
+ $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
+ }
+ unless ( $args{'Ticket'} ) {
+ return RT::CustomField->new( $RT::SystemUser );
+ }
+ my $cfs = $args{'Ticket'}->QueueObj->TicketCustomFields;
+ $cfs->Limit( FIELD => 'Name', VALUE => $args{'CustomField'}, CASESENSITIVE => 0 );
+ return $cfs->First || RT::CustomField->new( $RT::SystemUser );
+}
+
+sub GetDefaultServiceLevel {
+ my $self = shift;
+ my %args = (Ticket => undef, Queue => undef, @_);
+ unless ( $args{'Queue'} || $args{'Ticket'} ) {
+ $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
+ }
+ if ( !$args{'Queue'} && $args{'Ticket'} ) {
+ $args{'Queue'} = $args{'Ticket'}->QueueObj;
+ }
+ if ( $args{'Queue'} ) {
+ return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
+
+ if ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+ return $info unless ref $info;
+ return $info->{'Level'} || $RT::ServiceAgreements{'Default'};
+ }
+ }
+ return $RT::ServiceAgreements{'Default'};
+}
+
+RT::Base->_ImportOverlays();
+
+1;
commit 25a0ac4674fe9f20fab97c48df00066ea25830de
Author: sunnavy <sunnavy at bestpractical.com>
Date: Sun Aug 2 22:05:14 2015 +0800
use SLA column in Tickets table instead
diff --git a/etc/initialdata b/etc/initialdata
index 21650a6..94a2a52 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -14,17 +14,6 @@
@Groups = (
);
- at CustomFields = (
- {
- Name => 'SLA',
- Queue => 0,
- Type => 'SelectSingle',
- Disabled => 0,
- Description => 'Service Level Agreement',
- Values => [ ],
- },
-);
-
@Queues = ({ Name => 'General',
Description => 'The default queue',
CorrespondAddress => "",
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index cc623e8..3ceae2e 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -270,6 +270,7 @@ CREATE TABLE Tickets (
TimeEstimated NUMBER(11,0) DEFAULT 0 NOT NULL,
TimeWorked NUMBER(11,0) DEFAULT 0 NOT NULL,
Status VARCHAR2(64),
+ SLA VARCHAR2(64),
TimeLeft NUMBER(11,0) DEFAULT 0 NOT NULL,
Told DATE,
Starts DATE,
diff --git a/etc/schema.Pg b/etc/schema.Pg
index cd526e2..81ebbf8 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -423,6 +423,7 @@ CREATE TABLE Tickets (
TimeEstimated integer NOT NULL DEFAULT 0 ,
TimeWorked integer NOT NULL DEFAULT 0 ,
Status varchar(64) NULL ,
+ SLA varchar(64) NULL ,
TimeLeft integer NOT NULL DEFAULT 0 ,
Told TIMESTAMP NULL ,
Starts TIMESTAMP NULL ,
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index 5b2bb0f..93c8909 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -300,6 +300,7 @@ CREATE TABLE Tickets (
TimeEstimated integer NULL DEFAULT 0 ,
TimeWorked integer NULL DEFAULT 0 ,
Status varchar(64) collate NOCASE NULL ,
+ SLA varchar(64) collate NOCASE NULL ,
TimeLeft integer NULL DEFAULT 0 ,
Told DATETIME NULL ,
Starts DATETIME NULL ,
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 032c4d4..93fd164 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -281,6 +281,7 @@ CREATE TABLE Tickets (
TimeEstimated integer NOT NULL DEFAULT 0 ,
TimeWorked integer NOT NULL DEFAULT 0 ,
Status varchar(64) NULL ,
+ SLA varchar(64) NULL ,
TimeLeft integer NOT NULL DEFAULT 0 ,
Told DATETIME NULL ,
Starts DATETIME NULL ,
diff --git a/etc/upgrade/4.3.7/content b/etc/upgrade/4.3.8/content
similarity index 89%
rename from etc/upgrade/4.3.7/content
rename to etc/upgrade/4.3.8/content
index 2c24760..492cabb 100644
--- a/etc/upgrade/4.3.7/content
+++ b/etc/upgrade/4.3.8/content
@@ -1,17 +1,6 @@
use strict;
use warnings;
-our @CustomFields = (
- {
- Name => 'SLA',
- Queue => 0,
- Type => 'SelectSingle',
- Disabled => 0,
- Description => 'Service Level Agreement',
- Values => [ ],
- },
-);
-
our @ScripConditions = (
{ Name => '[SLA] Require default', # loc
Description => 'Detect a situation when we should set default service level' , # loc
diff --git a/etc/upgrade/4.3.8/schema.Oracle b/etc/upgrade/4.3.8/schema.Oracle
new file mode 100644
index 0000000..fb41770
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.Oracle
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD SLA VARCHAR2(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.Pg b/etc/upgrade/4.3.8/schema.Pg
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.Pg
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.SQLite b/etc/upgrade/4.3.8/schema.SQLite
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.SQLite
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.mysql b/etc/upgrade/4.3.8/schema.mysql
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.mysql
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/lib/RT/Action/SLA_SetDefault.pm b/lib/RT/Action/SLA_SetDefault.pm
index 1f06a19..ea918a2 100644
--- a/lib/RT/Action/SLA_SetDefault.pm
+++ b/lib/RT/Action/SLA_SetDefault.pm
@@ -75,12 +75,6 @@ sub Prepare { return 1 }
sub Commit {
my $self = shift;
- my $cf = $self->GetCustomField;
- unless ( $cf->id ) {
- $RT::Logger->warning("SLA scrip applied to a queue that has no SLA CF");
- return 1;
- }
-
my $level = $self->GetDefaultServiceLevel;
unless ( $level ) {
$RT::Logger->info(
@@ -89,10 +83,7 @@ sub Commit {
return 1;
}
- my ($status, $msg) = $self->TicketObj->AddCustomFieldValue(
- Field => $cf->id,
- Value => $level,
- );
+ my ($status, $msg) = $self->TicketObj->SetSLA($level);
unless ( $status ) {
$RT::Logger->error("Couldn't set service level: $msg");
return 0;
diff --git a/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
index c790df8..75eaadd 100644
--- a/lib/RT/Action/SLA_SetDue.pm
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -62,7 +62,7 @@ Checks if the ticket has service level defined.
sub Prepare {
my $self = shift;
- unless ( $self->TicketObj->FirstCustomFieldValue('SLA') ) {
+ unless ( $self->TicketObj->SLA ) {
$RT::Logger->error('SLA::SetDue scrip has been applied to ticket #'
. $self->TicketObj->id . ' that has no SLA defined');
return 0;
@@ -82,7 +82,7 @@ sub Commit {
my $ticket = $self->TicketObj;
my $txn = $self->TransactionObj;
- my $level = $ticket->FirstCustomFieldValue('SLA');
+ my $level = $ticket->SLA;
my ($last_reply, $is_outside) = $self->LastEffectiveAct;
$RT::Logger->debug(
diff --git a/lib/RT/Action/SLA_SetStarts.pm b/lib/RT/Action/SLA_SetStarts.pm
index 59b2fe1..f64b63f 100644
--- a/lib/RT/Action/SLA_SetStarts.pm
+++ b/lib/RT/Action/SLA_SetStarts.pm
@@ -74,7 +74,7 @@ sub Commit {
my $ticket = $self->TicketObj;
- my $level = $ticket->FirstCustomFieldValue('SLA');
+ my $level = $ticket->SLA;
unless ( $level ) {
$RT::Logger->debug('Ticket #'. $ticket->id .' has no service level defined, skip setting Starts');
return 1;
diff --git a/lib/RT/Condition/SLA.pm b/lib/RT/Condition/SLA.pm
index b164fae..60a4a56 100644
--- a/lib/RT/Condition/SLA.pm
+++ b/lib/RT/Condition/SLA.pm
@@ -58,25 +58,4 @@ use base qw(RT::SLA RT::Condition);
sub SLAIsApplied { return 1 }
-=head1 IsCustomFieldChange
-
-=cut
-
-sub IsCustomFieldChange {
- my $self = shift;
- my $cf_name = shift;
-
- my $txn = $self->TransactionObj;
-
- return 0 unless $txn->Type eq 'CustomField';
-
- my $cf = $self->GetCustomField( CustomField => $cf_name );
- unless ( $cf->id ) {
- $RT::Logger->debug("Custom field '$cf_name' is not applied to ticket #". $self->TicketObj->id);
- return 0;
- }
- return 0 unless $cf->id == $txn->Field;
- return 1;
-}
-
1;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
index bc49970..bf9792b 100644
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -65,8 +65,7 @@ sub IsApplicable {
return 0 unless $self->TransactionObj->Type eq 'Create';
my $ticket = $self->TicketObj;
return 0 unless lc($ticket->Type) eq 'ticket';
- return 0 if $ticket->FirstCustomFieldValue('SLA');
- return 0 unless $self->SLAIsApplied;
+ return 0 if $ticket->SLA;
return 1;
}
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
index d436c27..618bf0a 100644
--- a/lib/RT/Condition/SLA_RequireDueSet.pm
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -69,14 +69,16 @@ sub IsApplicable {
my $type = $self->TransactionObj->Type;
if ( $type eq 'Create' || $type eq 'Correspond' ) {
- return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 1 if $self->TicketObj->SLA;
return 0;
}
elsif ( $type eq 'Status' || ($type eq 'Set' && $self->TransactionObj->Field eq 'Status') ) {
- return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 1 if $self->TicketObj->SLA;
return 0;
}
- return 1 if $self->IsCustomFieldChange('SLA');
+ elsif ( $type eq 'SLA' || ($type eq 'Set' && $self->TransactionObj->Field eq 'SLA') ) {
+ return 1;
+ }
return 0;
}
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
index 895ccaa..3959a2e 100644
--- a/lib/RT/Condition/SLA_RequireStartsSet.pm
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -65,7 +65,7 @@ Applies if Starts date is not set for the ticket.
sub IsApplicable {
my $self = shift;
return 0 if $self->TicketObj->StartsObj->Unix > 0;
- return 0 unless $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 0 unless $self->TicketObj->SLA;
return 1;
}
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 193b2c2..14bfd8f 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -2140,6 +2140,7 @@ sub CreateTicket {
Type => $ARGS{'Type'} || 'ticket',
Queue => $ARGS{'Queue'},
Owner => $ARGS{'Owner'},
+ SLA => $ARGS{'SLA'},
# note: name change
Requestor => $ARGS{'Requestors'},
@@ -2861,6 +2862,7 @@ sub ProcessTicketBasics {
Type
Status
Queue
+ SLA
);
# Canonicalize Queue and Owner to their IDs if they aren't numeric
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index 817e5e6..aa9ab4f 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -377,17 +377,6 @@ Response SLA has only been met when the owner or AdminCc reply.
...
};
-=head2 Access control
-
-You can totally hide SLA custom field from users and use per queue
-defaults, just revoke SeeCustomField and ModifyCustomField.
-
-If you want people to see the current service level ticket is assigned
-to then grant SeeCustomField right.
-
-You may want to allow customers or managers to escalate thier tickets.
-Just grant them ModifyCustomField right.
-
=cut
sub BusinessHours {
@@ -556,20 +545,6 @@ sub CalculateTime {
return $res;
}
-sub GetCustomField {
- my $self = shift;
- my %args = (Ticket => undef, CustomField => 'SLA', @_);
- unless ( $args{'Ticket'} ) {
- $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
- }
- unless ( $args{'Ticket'} ) {
- return RT::CustomField->new( $RT::SystemUser );
- }
- my $cfs = $args{'Ticket'}->QueueObj->TicketCustomFields;
- $cfs->Limit( FIELD => 'Name', VALUE => $args{'CustomField'}, CASESENSITIVE => 0 );
- return $cfs->First || RT::CustomField->new( $RT::SystemUser );
-}
-
sub GetDefaultServiceLevel {
my $self = shift;
my %args = (Ticket => undef, Queue => undef, @_);
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 742c562..dd63eee 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -90,6 +90,7 @@ use RT::Transactions;
use RT::Reminders;
use RT::URI::fsck_com_rt;
use RT::URI;
+use RT::SLA;
use MIME::Entity;
use Devel::GlobalDestruction;
@@ -235,6 +236,7 @@ sub Create {
Starts => undef,
Started => undef,
Resolved => undef,
+ SLA => undef,
MIMEObj => undef,
_RecordTransaction => 1,
@_
@@ -387,7 +389,8 @@ sub Create {
Starts => $Starts->ISO,
Started => $Started->ISO,
Resolved => $Resolved->ISO,
- Due => $Due->ISO
+ Due => $Due->ISO,
+ SLA => $args{SLA},
);
# Parameters passed in during an import that we probably don't want to touch, otherwise
@@ -3461,6 +3464,8 @@ sub _CoreAccessible {
{read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
Status =>
{read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
+ SLA =>
+ {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
TimeLeft =>
{read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
Told =>
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index c641cd2..47027eb 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -104,6 +104,7 @@ __PACKAGE__->RegisterCustomFieldJoin(@$_) for
our %FIELD_METADATA = (
Status => [ 'STRING', ], #loc_left_pair
+ SLA => [ 'STRING', ], #loc_left_pair
Queue => [ 'ENUM' => 'Queue', ], #loc_left_pair
Type => [ 'ENUM', ], #loc_left_pair
Creator => [ 'ENUM' => 'User', ], #loc_left_pair
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Elements/SelectSLA
similarity index 53%
copy from share/html/Ticket/Elements/ShowBasics
copy to share/html/Elements/SelectSLA
index 546f581..c708ea5 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Elements/SelectSLA
@@ -45,53 +45,25 @@
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<table>
- <tr class="id">
- <td class="label"><&|/l&>Id</&>:</td>
- <td class="value"><%$Ticket->Id %></td>
- </tr>
- <tr class="status">
- <td class="label"><&|/l&>Status</&>:</td>
- <td class="value"><% loc($Ticket->Status) %></td>
- </tr>
-% if ($Ticket->TimeEstimated) {
- <tr class="time estimated">
- <td class="label"><&|/l&>Estimated</&>:</td>
- <td class="value"><& ShowTime, minutes => $Ticket->TimeEstimated &></td>
- </tr>
+<select name="<%$Name%>" id="<%$Name%>">
+% if ($DefaultValue) {
+<option value=""<% !$Default ? qq[ selected="selected"] : '' |n %>><%$DefaultLabel |n%></option>
% }
-% $m->callback( %ARGS, CallbackName => 'AfterTimeEstimated', TicketObj => $Ticket );
-% if ($Ticket->TimeWorked) {
- <tr class="time worked">
- <td class="label"><&|/l&>Worked</&>:</td>
- <td class="value"><& ShowTime, minutes => $Ticket->TimeWorked &></td>
- </tr>
+% for my $sla ( sort keys %{$RT::ServiceAgreements{'Levels'}} ) {
+<option <% $sla eq $Default ? qq[ selected="selected"] : '' |n %> value="<% $sla %>" ><% $sla %></option>
% }
-% $m->callback( %ARGS, CallbackName => 'AfterTimeWorked', TicketObj => $Ticket );
-% if ($Ticket->TimeLeft) {
- <tr class="time left">
- <td class="label"><&|/l&>Left</&>:</td>
- <td class="value"><& ShowTime, minutes => $Ticket->TimeLeft &></td>
- </tr>
-% }
- <tr class="priority">
- <td class="label"><&|/l&>Priority</&>:</td>
- <td class="value"><& ShowPriority, Ticket => $Ticket &></td>
- </tr>
-%# This will check SeeQueue at the ticket role level, queue level, and global level
-% if ($Ticket->CurrentUserHasRight('SeeQueue')) {
- <tr class="queue">
- <td class="label"><&|/l&>Queue</&>:</td>
- <td class="value"><& ShowQueue, Ticket => $Ticket, QueueObj => $Ticket->QueueObj &></td>
- </tr>
-% }
- <& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket, Grouping => 'Basics', Table => 0 &>
-% if ($UngroupedCFs) {
- <& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket, Grouping => '', Table => 0 &>
-% }
-% $m->callback( %ARGS, CallbackName => 'EndOfList', TicketObj => $Ticket );
-</table>
+</select>
+
+<%INIT>
+$Default ||= $DECODED_ARGS->{SLA} if $DefaultFromArgs;
+$Default ||= $TicketObj->SLA if $TicketObj;
+</%INIT>
<%ARGS>
-$Ticket => undef
-$UngroupedCFs => 0
+$DefaultFromArgs => 1
+$TicketObj => undef
+$QueueObj => undef
+$Name => 'SLA'
+$Default => undef
+$DefaultValue => 1
+$DefaultLabel => '-'
</%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 21d2e6b..fbb2b38 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -94,6 +94,15 @@
DefaultValue => 0,
QueueObj => $QueueObj,
},
+ },
+ { name => 'SLA',
+ comp => '/Elements/SelectSLA',
+ args => {
+ Name => "SLA",
+ Default => $ARGS{SLA} || RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj),
+ DefaultValue => RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj) ? 0 : 1,
+ QueueObj => $QueueObj,
+ },
}
]
&>
diff --git a/share/html/Ticket/Elements/EditBasics b/share/html/Ticket/Elements/EditBasics
index cfb446a..ae5e121 100644
--- a/share/html/Ticket/Elements/EditBasics
+++ b/share/html/Ticket/Elements/EditBasics
@@ -85,6 +85,15 @@ unless ( @fields ) {
DefaultValue => 0,
}
},
+ { name => 'SLA',
+ comp => '/Elements/SelectSLA',
+ args => {
+ Name => 'SLA',
+ Default => $defaults{SLA},
+ DefaultFromArgs => 0,
+ TicketObj => $TicketObj,
+ },
+ },
# Time Estimated, Worked, and Left
(
map {
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Ticket/Elements/ShowBasics
index 546f581..6690cff 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Ticket/Elements/ShowBasics
@@ -54,6 +54,10 @@
<td class="label"><&|/l&>Status</&>:</td>
<td class="value"><% loc($Ticket->Status) %></td>
</tr>
+ <tr class="sla">
+ <td class="label"><&|/l&>SLA</&>:</td>
+ <td class="value"><% loc($Ticket->SLA) %></td>
+ </tr>
% if ($Ticket->TimeEstimated) {
<tr class="time estimated">
<td class="label"><&|/l&>Estimated</&>:</td>
commit 6e301495892e8ec0b6fb1f1a395ee13bf26cf925
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 3 00:37:55 2015 +0800
allow to disable SLA for some queues
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 3ceae2e..b8d9093 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -33,6 +33,7 @@ CREATE TABLE Queues (
Created DATE,
LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
LastUpdated DATE,
+ SLADisabled NUMBER(11,0) DEFAULT 0 NOT NULL,
Disabled NUMBER(11,0) DEFAULT 0 NOT NULL
);
CREATE UNIQUE INDEX Queues1 ON Queues (LOWER(Name));
diff --git a/etc/schema.Pg b/etc/schema.Pg
index 81ebbf8..3f7faeb 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -56,6 +56,7 @@ CREATE TABLE Queues (
Created TIMESTAMP NULL ,
LastUpdatedBy integer NOT NULL DEFAULT 0 ,
LastUpdated TIMESTAMP NULL ,
+ SLADisabled integer NOT NULL DEFAULT 0 ,
Disabled integer NOT NULL DEFAULT 0 ,
PRIMARY KEY (id)
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index 93c8909..d5ce0ca 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -34,6 +34,7 @@ CREATE TABLE Queues (
Created DATETIME NULL ,
LastUpdatedBy integer NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
+ SLADisabled int2 NOT NULL DEFAULT 0,
Disabled int2 NOT NULL DEFAULT 0
) ;
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 93fd164..616676d 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -30,6 +30,7 @@ CREATE TABLE Queues (
Created DATETIME NULL ,
LastUpdatedBy integer NOT NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
+ SLADisabled int2 NOT NULL DEFAULT 0 ,
Disabled int2 NOT NULL DEFAULT 0 ,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARACTER SET utf8;
diff --git a/etc/upgrade/4.3.8/schema.Oracle b/etc/upgrade/4.3.8/schema.Oracle
index fb41770..1e78116 100644
--- a/etc/upgrade/4.3.8/schema.Oracle
+++ b/etc/upgrade/4.3.8/schema.Oracle
@@ -1 +1,2 @@
ALTER TABLE Tickets ADD SLA VARCHAR2(64) NULL;
+ALTER TABLE Queues ADD SLADisabled NUMBER(11,0) DEFAULT 0 NOT NULL;
diff --git a/etc/upgrade/4.3.8/schema.Pg b/etc/upgrade/4.3.8/schema.Pg
index 21b2af6..7df4453 100644
--- a/etc/upgrade/4.3.8/schema.Pg
+++ b/etc/upgrade/4.3.8/schema.Pg
@@ -1 +1,2 @@
ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled integer NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.3.8/schema.SQLite b/etc/upgrade/4.3.8/schema.SQLite
index 21b2af6..7d89190 100644
--- a/etc/upgrade/4.3.8/schema.SQLite
+++ b/etc/upgrade/4.3.8/schema.SQLite
@@ -1 +1,2 @@
ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled int2 NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.3.8/schema.mysql b/etc/upgrade/4.3.8/schema.mysql
index 21b2af6..7d89190 100644
--- a/etc/upgrade/4.3.8/schema.mysql
+++ b/etc/upgrade/4.3.8/schema.mysql
@@ -1 +1,2 @@
ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled int2 NOT NULL DEFAULT 0;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
index bf9792b..5513c2b 100644
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -65,6 +65,7 @@ sub IsApplicable {
return 0 unless $self->TransactionObj->Type eq 'Create';
my $ticket = $self->TicketObj;
return 0 unless lc($ticket->Type) eq 'ticket';
+ return 0 if $ticket->QueueObj->SLADisabled;
return 0 if $ticket->SLA;
return 1;
}
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
index 618bf0a..77583c8 100644
--- a/lib/RT/Condition/SLA_RequireDueSet.pm
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -66,6 +66,7 @@ a ticket and it has service level value or when we set service level.
sub IsApplicable {
my $self = shift;
return 0 unless $self->SLAIsApplied;
+ return 0 if $self->TicketObj->QueueObj->SLADisabled;
my $type = $self->TransactionObj->Type;
if ( $type eq 'Create' || $type eq 'Correspond' ) {
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
index 3959a2e..883f45a 100644
--- a/lib/RT/Condition/SLA_RequireStartsSet.pm
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -65,6 +65,7 @@ Applies if Starts date is not set for the ticket.
sub IsApplicable {
my $self = shift;
return 0 if $self->TicketObj->StartsObj->Unix > 0;
+ return 0 if $self->TicketObj->QueueObj->SLADisabled;
return 0 unless $self->TicketObj->SLA;
return 1;
}
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index ba9d023..d4064c2 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1014,6 +1014,8 @@ sub _CoreAccessible {
{read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
LastUpdated =>
{read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
+ SLADisabled =>
+ {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
Disabled =>
{read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
diff --git a/share/html/Admin/Queues/Modify.html b/share/html/Admin/Queues/Modify.html
index 8545744..5a483be 100644
--- a/share/html/Admin/Queues/Modify.html
+++ b/share/html/Admin/Queues/Modify.html
@@ -92,6 +92,15 @@
<br /><span><em><&|/l , RT->Config->Get('CommentAddress')&>(If left blank, will default to [_1])</&></em></span></td>
</tr>
+<tr><td align="right"><input type="checkbox" class="checkbox" id="SLAEnabled" name="SLAEnabled" value="1"
+% if ( $Create || !$QueueObj->SLADisabled ) {
+checked="checked"
+% }
+ /></td>
+<td colspan="3"><label for="SLAEnabled"><&|/l&>SLA Enabled (Unchecking this box disables SLA for this queue)</&></label><br />
+<input type="hidden" class="hidden" name="SetSLAEnabled" value="1" />
+</td></tr>
+
% my $CFs = $QueueObj->CustomFields;
% while (my $CF = $CFs->Next) {
<tr valign="top"><td align="right">
@@ -176,12 +185,15 @@ unless ($Create) {
if ( $QueueObj->Id ) {
$title = loc('Configuration for queue [_1]', $QueueObj->Name );
my @attribs= qw(Description CorrespondAddress CommentAddress Name
- Sign SignAuto Encrypt Lifecycle SubjectTag Disabled);
+ Sign SignAuto Encrypt Lifecycle SubjectTag SLADisabled Disabled);
# we're asking about enabled on the web page but really care about disabled
if ( $SetEnabled ) {
$Disabled = $ARGS{'Disabled'} = $Enabled? 0: 1;
}
+ if ( $SetSLAEnabled ) {
+ $ARGS{'SLADisabled'} = $SLAEnabled? 0: 1;
+ }
if ( $SetCrypt ) {
$ARGS{$_} = 0 foreach grep !defined $ARGS{$_} || !length $ARGS{$_},
qw(Sign SignAuto Encrypt);
@@ -244,7 +256,9 @@ $Create => undef
$Description => undef
$CorrespondAddress => undef
$CommentAddress => undef
+$SetSLAEnabled => undef
$SetEnabled => undef
$SetCrypt => undef
+$SLAEnabled => undef
$Enabled => undef
</%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index fbb2b38..8dba39f 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -95,6 +95,7 @@
QueueObj => $QueueObj,
},
},
+ $QueueObj->SLADisabled ? () : (
{ name => 'SLA',
comp => '/Elements/SelectSLA',
args => {
@@ -103,7 +104,7 @@
DefaultValue => RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj) ? 0 : 1,
QueueObj => $QueueObj,
},
- }
+ }),
]
&>
diff --git a/share/html/Ticket/Elements/EditBasics b/share/html/Ticket/Elements/EditBasics
index ae5e121..7b5708b 100644
--- a/share/html/Ticket/Elements/EditBasics
+++ b/share/html/Ticket/Elements/EditBasics
@@ -85,6 +85,7 @@ unless ( @fields ) {
DefaultValue => 0,
}
},
+ $TicketObj->QueueObj->SLADisabled ? () : (
{ name => 'SLA',
comp => '/Elements/SelectSLA',
args => {
@@ -93,7 +94,7 @@ unless ( @fields ) {
DefaultFromArgs => 0,
TicketObj => $TicketObj,
},
- },
+ }),
# Time Estimated, Worked, and Left
(
map {
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Ticket/Elements/ShowBasics
index 6690cff..3d2c652 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Ticket/Elements/ShowBasics
@@ -54,10 +54,12 @@
<td class="label"><&|/l&>Status</&>:</td>
<td class="value"><% loc($Ticket->Status) %></td>
</tr>
+% if ( !$Ticket->QueueObj->SLADisabled ) {
<tr class="sla">
<td class="label"><&|/l&>SLA</&>:</td>
<td class="value"><% loc($Ticket->SLA) %></td>
</tr>
+% }
% if ($Ticket->TimeEstimated) {
<tr class="time estimated">
<td class="label"><&|/l&>Estimated</&>:</td>
commit f1cd8c158a9fc958c95ea0ca8bf12cf34631ebe8
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 3 00:59:22 2015 +0800
update doc/config and move it to RT_Config.pm
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 5deaade..76585a6 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2918,9 +2918,341 @@ Set(%Lifecycles,
},
);
+=head1 SLA
+=over 4
+
+=item C<%ServiceAgreements>
+
+There is no WebUI in the current version. Almost everything is controlled in
+the C<%ServiceAgreements> and C<%ServiceBusinessHours>. For example:
+
+ Set( %ServiceAgreements = (
+ Default => '4h',
+ QueueDefault => {
+ 'Incident' => '2h',
+ },
+ Levels => {
+ '2h' => { Resolve => { RealMinutes => 60*2 } },
+ '4h' => { Resolve => { RealMinutes => 60*4 } },
+ },
+ ));
+
+In this example I<Incident> is the name of the queue, and I<2h> is the name of
+the SLA which will be applied to this queue by default.
+
+Each service level can be described using several options:
+L<Starts|/"Starts (interval, first business minute)">,
+L<Resolve|/"Resolve and Response (interval, no defaults)">,
+L<Response|/"Resolve and Response (interval, no defaults)">,
+L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
+L<OutOfHours|/"OutOfHours (struct, no default)">
+and L<ServiceBusinessHours|/"Configuring business hours">.
+
+=over 4
+
+=item Starts (interval, first business minute)
+
+By default when a ticket is created Starts date is set to
+first business minute after time of creation. In other
+words if a ticket is created during business hours then
+Starts will be equal to Created time, otherwise Starts will
+be beginning of the next business day.
+
+However, if you provide 24/7 support then you most
+probably would be interested in Starts to be always equal
+to Created time.
+
+Starts option can be used to adjust behaviour. Format
+of the option is the same as format for deadlines which
+described later in details. RealMinutes, BusinessMinutes
+options and OutOfHours modifiers can be used here like
+for any other deadline. For example:
+
+ 'standard' => {
+ # give people 15 minutes
+ Starts => { BusinessMinutes => 15 },
+ },
+
+You can still use old option StartImmediately to set
+Starts date equal to Created date.
+
+Example:
+
+ '24/7' => {
+ StartImmediately => 1,
+ Response => { RealMinutes => 30 },
+ },
+
+But it's the same as:
+
+ '24/7' => {
+ Starts => { RealMinutes => 0 },
+ Response => { RealMinutes => 30 },
+ },
+
+=item Resolve and Response (interval, no defaults)
+
+These two options define deadlines for resolve of a ticket
+and reply to customer(requestors) questions accordingly.
+
+You can define them using real time, business or both. Read more
+about the latter L<below|/"Using both Resolve and Response in the same level">.
+
+The Due date field is used to store calculated deadlines.
+
+=over 4
+
+=item Resolve
+
+Defines deadline when a ticket should be resolved. This option is
+quite simple and straightforward when used without L</Response>.
+
+Example:
+
+ # 8 business hours
+ 'simple' => { Resolve => 60*8 },
+ ...
+ # one real week
+ 'hard' => { Resolve => { RealMinutes => 60*24*7 } },
+
+=item Response
+
+In many companies providing support service(s) resolve time of a ticket
+is less important than time of response to requestors from staff
+members.
+
+You can use Response option to define such deadlines. The Due date is
+set when a ticket is created, unset when a worker replies, and re-set
+when the requestor replies again -- until the ticket is closed, when the
+ticket's Due date is unset.
+
+B<NOTE> that this behaviour changes when Resolve and Response options
+are combined; see L</"Using both Resolve and Response in the same
+level">.
+
+Note that by default, only the requestors on the ticket are considered
+"outside actors" and thus require a Response due date; all other email
+addresses are treated as workers of the ticket, and thus count as
+meeting the SLA. If you'd like to invert this logic, so that the Owner
+and AdminCcs are the only worker email addresses, and all others are
+external, see the L</AssumeOutsideActor> configuration.
+
+The owner is never treated as an outside actor; if they are also the
+requestor of the ticket, it will have no SLA.
+
+If an outside actor replies multiple times, their later replies are
+ignored; the deadline is awlways calculated from the oldest
+correspondence from the outside actor.
+
+
+=item Using both Resolve and Response in the same level
+
+Resolve and Response can be combined. In such case due date is set
+according to the earliest of two deadlines and never is dropped to
+'not set'.
+
+If a ticket met its Resolve deadline then due date stops "flipping",
+is freezed and the ticket becomes overdue. Before that moment when
+an inside actor replies to a ticket, due date is changed to Resolve
+deadline instead of 'Not Set', as well this happens when a ticket
+is closed. So all the time due date is defined.
+
+Example:
+
+ 'standard delivery' => {
+ Response => { RealMinutes => 60*1 }, # one hour
+ Resolve => { RealMinutes => 60*24 }, # 24 real hours
+ },
+
+A client orders goods and due date of the order is set to the next one
+hour, you have this hour to process the order and write a reply.
+As soon as goods are delivered you resolve tickets and usually meet
+Resolve deadline, but if you don't resolve or user replies then most
+probably there are problems with delivery of the goods. And if after
+a week you keep replying to the client and always meeting one hour
+response deadline that doesn't mean the ticket is not over due.
+Due date was frozen 24 hours after creation of the order.
+
+=item Using business and real time in one option
+
+It's quite rare situation when people need it, but we've decided
+that business is applied first and then real time when deadline
+described using both types of time. For example:
+
+ 'delivery' => {
+ Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
+ },
+ 'fast delivery' {
+ StartImmediately => 1,
+ Resolve => { RealMinutes => 60*8 },
+ },
+
+For delivery requests which come into the system during business
+hours these levels define the same deadlines, otherwise the first
+level set deadline to 8 real hours starting from the next business
+day, when tickets with the second level should be resolved in the
+next 8 hours after creation.
+
+=back
+
+=item Keep in loop (interval, no defaults)
+
+If response deadline is used then Due date is changed to repsonse
+deadline or to "Not Set" when staff replies to a ticket. In some
+cases you want to keep requestors in loop and keed them up to date
+every few hours. KeepInLoop option can be used to achieve this.
+
+ 'incident' => {
+ Response => { RealMinutes => 60*1 }, # one hour
+ KeepInLoop => { RealMinutes => 60*2 }, # two hours
+ Resolve => { RealMinutes => 60*24 }, # 24 real hours
+ },
+In the above example Due is set to one hour after creation, reply
+of a inside actor moves Due date two hours forward, outside actors'
+replies move Due date to one hour and resolve deadine is 24 hours.
+=item Modifying Agreements
+
+=over 4
+
+=item OutOfHours (struct, no default)
+
+Out of hours modifier. Adds more real or business minutes to resolve
+and/or reply options if event happens out of business hours, read also
+</"Configuring business hours"> below.
+
+Example:
+
+ 'level x' => {
+ OutOfHours => { Resolve => { RealMinutes => +60*24 } },
+ Resolve => { RealMinutes => 60*24 },
+ },
+
+If a request comes into the system during night then supporters have two
+hours, otherwise only one.
+
+ 'level x' => {
+ OutOfHours => { Response => { BusinessMinutes => +60*2 } },
+ Resolve => { BusinessMinutes => 60 },
+ },
+
+Supporters have two additional hours in the morning to deal with bunch
+of requests that came into the system during the last night.
+
+=item IgnoreOnStatuses (array, no default)
+
+Allows you to ignore a deadline when ticket has certain status. Example:
+
+ 'level x' => {
+ KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
+ },
+
+In above example KeepInLoop deadline is ignored if ticket is stalled.
+
+B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
+Due date is calculated from the last action (reply, SLA change, etc) which fits
+the SLA type (Response, Starts, KeepInLoop, etc). This means if a ticket in
+the above example flips from stalled to open without a reply, the ticket will
+probably be overdue. In most cases this shouldn't be a problem since moving
+out of stalled-like statuses is often the result of RT's auto-open on reply
+scrip, therefore ensuring there's a new reply to calculate Due from. The
+overall effect is that ignored statuses don't let the Due date drift
+arbitrarily, which could wreak havoc on your SLA performance.
+C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
+overdue" issue by excluding the time spent on ignored statuses, e.g.
+
+ 'level x' => {
+ KeepInLoop => {
+ BusinessMinutes => 60,
+ ExcludeTimeOnIgnoredStatuses => 1,
+ IgnoreOnStatuses => ['stalled'],
+ },
+ },
+
+=back
+
+=item Defining service levels per queue
+
+In the config you can set per queue defaults, using:
+
+ Set( %ServiceAgreements, (
+ Default => 'global default level of service',
+ QueueDefault => {
+ 'queue name' => 'default value for this queue',
+ ...
+ },
+ ...
+ ));
+
+=item AssumeOutsideActor
+
+When using a L<Response|/"Resolve and Response (interval, no defaults)">
+configuration, the due date is unset when anyone who is not a requestor
+replies. If it is common for non-requestors to reply to tickets, and
+this should I<not> satisfy the SLA, you may wish to set
+C<AssumeOutsideActor>. This causes the extension to assume that the
+Response SLA has only been met when the owner or AdminCc reply.
+
+ Set ( %ServiceAgreements = (
+ AssumeOutsideActor => 1,
+ ...
+ );
+
+=back
+
+=cut
+
+Set( %ServiceAgreements, );
+
+=item C<%ServiceBusinessHours>
+
+In the config you can set one or more work schedules, e.g.
+
+ Set( %ServiceBusinessHours, (
+ 'Default' => {
+ ... description ...
+ },
+ 'Support' => {
+ ... description ...
+ },
+ 'Sales' => {
+ ... description ...
+ },
+ );
+
+Read more about how to describe a schedule in L<Business::Hours>.
+
+=over 4
+
+=item Defining different business hours for service levels
+
+Each level supports BusinessHours option to specify your own business
+hours.
+
+ 'level x' => {
+ BusinessHours => 'work just in Monday',
+ Resolve => { BusinessMinutes => 60 },
+ },
+
+then L<%ServiceBusinessHours> should have the corresponding definition:
+
+ Set( %ServiceBusinessHours, (
+ 'work just in Monday' => {
+ 1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
+ },
+ );
+
+Default Business Hours setting is in $ServiceBusinessHours{'Default'}.
+
+=back
+
+=cut
+
+Set( %ServiceBusinessHours, );
+
+=back
=head1 Administrative interface
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index aa9ab4f..e9a7abf 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -58,325 +58,6 @@ RT::SLA - Service Level Agreements for RT
Automated due dates using service levels.
-=head1 CONFIGURATION
-
-Service level agreements of tickets is controlled by an SLA custom field (CF).
-This field is created during C<make initdb> step (above) and applied globally.
-This CF MUST be of C<select one value> type. Values of the CF define the
-service levels.
-
-It's possible to define different set of levels for different
-queues. You can create several CFs with the same name and
-different set of values. But if you move tickets between
-queues a lot then it's going to be a problem and it's preferred
-to use B<ONE> SLA custom field.
-
-There is no WebUI in the current version. Almost everything is
-controlled in the RT's config using option C<%RT::ServiceAgreements>
-and C<%RT::ServiceBusinessHours>. For example:
-
- %RT::ServiceAgreements = (
- Default => '4h',
- QueueDefault => {
- 'Incident' => '2h',
- },
- Levels => {
- '2h' => { Resolve => { RealMinutes => 60*2 } },
- '4h' => { Resolve => { RealMinutes => 60*4 } },
- },
- );
-
-In this example I<Incident> is the name of the queue, and I<2h> is the name of
-the SLA which will be applied to this queue by default.
-
-Each service level can be described using several options:
-L<Starts|/"Starts (interval, first business minute)">,
-L<Resolve|/"Resolve and Response (interval, no defaults)">,
-L<Response|/"Resolve and Response (interval, no defaults)">,
-L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
-L<OutOfHours|/"OutOfHours (struct, no default)">
-and L<ServiceBusinessHours|/"Configuring business hours">.
-
-=head2 Starts (interval, first business minute)
-
-By default when a ticket is created Starts date is set to
-first business minute after time of creation. In other
-words if a ticket is created during business hours then
-Starts will be equal to Created time, otherwise Starts will
-be beginning of the next business day.
-
-However, if you provide 24/7 support then you most
-probably would be interested in Starts to be always equal
-to Created time.
-
-Starts option can be used to adjust behaviour. Format
-of the option is the same as format for deadlines which
-described later in details. RealMinutes, BusinessMinutes
-options and OutOfHours modifiers can be used here like
-for any other deadline. For example:
-
- 'standard' => {
- # give people 15 minutes
- Starts => { BusinessMinutes => 15 },
- },
-
-You can still use old option StartImmediately to set
-Starts date equal to Created date.
-
-Example:
-
- '24/7' => {
- StartImmediately => 1,
- Response => { RealMinutes => 30 },
- },
-
-But it's the same as:
-
- '24/7' => {
- Starts => { RealMinutes => 0 },
- Response => { RealMinutes => 30 },
- },
-
-=head2 Resolve and Response (interval, no defaults)
-
-These two options define deadlines for resolve of a ticket
-and reply to customer(requestors) questions accordingly.
-
-You can define them using real time, business or both. Read more
-about the latter L<below|/"Using both Resolve and Response in the same level">.
-
-The Due date field is used to store calculated deadlines.
-
-=head3 Resolve
-
-Defines deadline when a ticket should be resolved. This option is
-quite simple and straightforward when used without L</Response>.
-
-Example:
-
- # 8 business hours
- 'simple' => { Resolve => 60*8 },
- ...
- # one real week
- 'hard' => { Resolve => { RealMinutes => 60*24*7 } },
-
-=head3 Response
-
-In many companies providing support service(s) resolve time of a ticket
-is less important than time of response to requestors from staff
-members.
-
-You can use Response option to define such deadlines. The Due date is
-set when a ticket is created, unset when a worker replies, and re-set
-when the requestor replies again -- until the ticket is closed, when the
-ticket's Due date is unset.
-
-B<NOTE> that this behaviour changes when Resolve and Response options
-are combined; see L</"Using both Resolve and Response in the same
-level">.
-
-Note that by default, only the requestors on the ticket are considered
-"outside actors" and thus require a Response due date; all other email
-addresses are treated as workers of the ticket, and thus count as
-meeting the SLA. If you'd like to invert this logic, so that the Owner
-and AdminCcs are the only worker email addresses, and all others are
-external, see the L</AssumeOutsideActor> configuration.
-
-The owner is never treated as an outside actor; if they are also the
-requestor of the ticket, it will have no SLA.
-
-If an outside actor replies multiple times, their later replies are
-ignored; the deadline is awlways calculated from the oldest
-correspondence from the outside actor.
-
-
-=head3 Using both Resolve and Response in the same level
-
-Resolve and Response can be combined. In such case due date is set
-according to the earliest of two deadlines and never is dropped to
-'not set'.
-
-If a ticket met its Resolve deadline then due date stops "flipping",
-is freezed and the ticket becomes overdue. Before that moment when
-an inside actor replies to a ticket, due date is changed to Resolve
-deadline instead of 'Not Set', as well this happens when a ticket
-is closed. So all the time due date is defined.
-
-Example:
-
- 'standard delivery' => {
- Response => { RealMinutes => 60*1 }, # one hour
- Resolve => { RealMinutes => 60*24 }, # 24 real hours
- },
-
-A client orders goods and due date of the order is set to the next one
-hour, you have this hour to process the order and write a reply.
-As soon as goods are delivered you resolve tickets and usually meet
-Resolve deadline, but if you don't resolve or user replies then most
-probably there are problems with delivery of the goods. And if after
-a week you keep replying to the client and always meeting one hour
-response deadline that doesn't mean the ticket is not over due.
-Due date was frozen 24 hours after creation of the order.
-
-=head3 Using business and real time in one option
-
-It's quite rare situation when people need it, but we've decided
-that business is applied first and then real time when deadline
-described using both types of time. For example:
-
- 'delivery' => {
- Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
- },
- 'fast delivery' {
- StartImmediately => 1,
- Resolve => { RealMinutes => 60*8 },
- },
-
-For delivery requests which come into the system during business
-hours these levels define the same deadlines, otherwise the first
-level set deadline to 8 real hours starting from the next business
-day, when tickets with the second level should be resolved in the
-next 8 hours after creation.
-
-=head2 Keep in loop (interval, no defaults)
-
-If response deadline is used then Due date is changed to repsonse
-deadline or to "Not Set" when staff replies to a ticket. In some
-cases you want to keep requestors in loop and keed them up to date
-every few hours. KeepInLoop option can be used to achieve this.
-
- 'incident' => {
- Response => { RealMinutes => 60*1 }, # one hour
- KeepInLoop => { RealMinutes => 60*2 }, # two hours
- Resolve => { RealMinutes => 60*24 }, # 24 real hours
- },
-
-In the above example Due is set to one hour after creation, reply
-of a inside actor moves Due date two hours forward, outside actors'
-replies move Due date to one hour and resolve deadine is 24 hours.
-
-=head2 Modifying Agreements
-
-=head3 OutOfHours (struct, no default)
-
-Out of hours modifier. Adds more real or business minutes to resolve
-and/or reply options if event happens out of business hours, read also
-</"Configuring business hours"> below.
-
-Example:
-
- 'level x' => {
- OutOfHours => { Resolve => { RealMinutes => +60*24 } },
- Resolve => { RealMinutes => 60*24 },
- },
-
-If a request comes into the system during night then supporters have two
-hours, otherwise only one.
-
- 'level x' => {
- OutOfHours => { Response => { BusinessMinutes => +60*2 } },
- Resolve => { BusinessMinutes => 60 },
- },
-
-Supporters have two additional hours in the morning to deal with bunch
-of requests that came into the system during the last night.
-
-=head3 IgnoreOnStatuses (array, no default)
-
-Allows you to ignore a deadline when ticket has certain status. Example:
-
- 'level x' => {
- KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
- },
-
-In above example KeepInLoop deadline is ignored if ticket is stalled.
-
-B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
-Due date is calculated from the last action (reply, SLA change, etc) which fits
-the SLA type (Response, Starts, KeepInLoop, etc). This means if a ticket in
-the above example flips from stalled to open without a reply, the ticket will
-probably be overdue. In most cases this shouldn't be a problem since moving
-out of stalled-like statuses is often the result of RT's auto-open on reply
-scrip, therefore ensuring there's a new reply to calculate Due from. The
-overall effect is that ignored statuses don't let the Due date drift
-arbitrarily, which could wreak havoc on your SLA performance.
-C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
-overdue" issue by excluding the time spent on ignored statuses.
-
- 'level x' => {
- KeepInLoop => {
- BusinessMinutes => 60,
- ExcludeTimeOnIgnoredStatuses => 1,
- IgnoreOnStatuses => ['stalled'],
- },
- },
-
-=head2 Configuring business hours
-
-In the config you can set one or more work schedules. Use the following
-format:
-
- %RT::ServiceBusinessHours = (
- 'Default' => {
- ... description ...
- },
- 'Support' => {
- ... description ...
- },
- 'Sales' => {
- ... description ...
- },
- );
-
-Read more about how to describe a schedule in L<Business::Hours>.
-
-=head3 Defining different business hours for service levels
-
-Each level supports BusinessHours option to specify your own business
-hours.
-
- 'level x' => {
- BusinessHours => 'work just in Monday',
- Resolve => { BusinessMinutes => 60 },
- },
-
-then %RT::ServiceBusinessHours should have the corresponding definition:
-
- %RT::ServiceBusinessHours = (
- 'work just in Monday' => {
- 1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
- },
- );
-
-Default Business Hours setting is in $RT::ServiceBusinessHours{'Default'}.
-
-=head2 Defining service levels per queue
-
-In the config you can set per queue defaults, using:
-
- %RT::ServiceAgreements = (
- Default => 'global default level of service',
- QueueDefault => {
- 'queue name' => 'default value for this queue',
- ...
- },
- ...
- };
-
-=head2 AssumeOutsideActor
-
-When using a L<Response|/"Resolve and Response (interval, no defaults)">
-configuration, the due date is unset when anyone who is not a requestor
-replies. If it is common for non-requestors to reply to tickets, and
-this should I<not> satisfy the SLA, you may wish to set
-C<AssumeOutsideActor>. This causes the extension to assume that the
-Response SLA has only been met when the owner or AdminCc reply.
-
- %RT::ServiceAgreements = (
- AssumeOutsideActor => 1,
- ...
- };
-
=cut
sub BusinessHours {
commit 8be9a47d80869d91629b37a25d7c3009d9d3b94e
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 3 01:52:58 2015 +0800
upgrade-sla script to migrate SLA values from custom fields
diff --git a/.gitignore b/.gitignore
index 2156779..54bde7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
/etc/upgrade/switch-templates-to
/etc/upgrade/time-worked-history
/etc/upgrade/upgrade-articles
+/etc/upgrade/upgrade-sla
/etc/upgrade/vulnerable-passwords
/lib/RT/Generated.pm
/Makefile
diff --git a/configure.ac b/configure.ac
index 9c57eda..064274e 100755
--- a/configure.ac
+++ b/configure.ac
@@ -437,6 +437,7 @@ AC_CONFIG_FILES([
etc/upgrade/time-worked-history
etc/upgrade/upgrade-articles
etc/upgrade/vulnerable-passwords
+ etc/upgrade/upgrade-sla
sbin/rt-attributes-viewer
sbin/rt-preferences-viewer
sbin/rt-session-viewer
diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
new file mode 100644
index 0000000..9cc1f28
--- /dev/null
+++ b/etc/upgrade/upgrade-sla.in
@@ -0,0 +1,91 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use 5.10.1;
+use strict;
+use warnings;
+
+use lib "@LOCAL_LIB_PATH@";
+use lib "@RT_LIB_PATH@";
+
+use RT::Interface::CLI qw(Init);
+Init();
+
+my $tickets = RT::Tickets->new(RT->SystemUser);
+$tickets->FromSQL('CF.SLA IS NOT NULL AND SLA IS NULL');
+while ( my $ticket = $tickets->Next ) {
+ my ($ret, $msg) = $ticket->SetSLA($ticket->FirstCustomFieldValue('SLA'));
+ unless ( $ret ) {
+ RT->Logger->error("Failed to upgrade SLA for ticket #" . $ticket->id . ": $msg");
+ }
+}
+
+my $queues = RT::Queues->new(RT->SystemUser);
+$queues->UnLimit;
+
+my %cfs_to_disable;
+while ( my $queue = $queues->Next ) {
+ my $cfs = $queue->TicketCustomFields;
+ $cfs->Limit(FIELD => 'Name', VALUE => 'SLA', CASESENSITIVE => 0 );
+ if ( my $cf = $cfs->First ) {
+ $cfs_to_disable{$cf->id} ||= $cf;
+ }
+ elsif ( !$queue->SLADisabled ) {
+ my ($ret, $msg) = $queue->SetSLADisabled(1);
+ unless ( $ret ) {
+ RT->Logger->error("Failed to disable SLA for queue #" . $queue->id . ": $msg");
+ }
+ }
+}
+
+for my $cf ( values %cfs_to_disable ) {
+ my ($ret, $msg) = $cf->SetDisabled(1);
+ unless ( $ret ) {
+ RT->Logger->error("Failed to disable custom field SLA #" . $cf->id . ": $msg");
+ }
+}
commit 6eaad0416162dad8d1b4540825f8a2ee480a7f1f
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 3 02:07:26 2015 +0800
upgrading note for core sla
diff --git a/docs/UPGRADING-4.4 b/docs/UPGRADING-4.4
index 78c2f85..3827cf4 100644
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@ -37,6 +37,17 @@ Homepage component "Quicksearch" has been renamed to "QueueList" to reflect
what it actually is. Please update C<$HomepageComponents> accordingly if you
customized it in site config.
+=item *
+
+SLA is in core now, so C<SLA> became a core field. if you installed
+C<RT::Extension::SLA> before, you need to remove it from your plugins, adjust
+configs accordingly and run F<etc/upgrade/upgrade-sla>. see also the SLA
+section in F<RT_Config.pm>.
+
+Note that with core SLA, you can't define different set of levels for
+different queues. i.e. all the queues share the same set of levels defined in
+C<%ServiceAgreements>.
+
=back
=cut
commit 8bfb984257cd45a4ef9811a4ba10607b4e3b570a
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Aug 3 02:24:08 2015 +0800
fix uninitialized warnings to make tests happy
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index e9a7abf..55cf793 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -237,8 +237,8 @@ sub GetDefaultServiceLevel {
}
if ( $args{'Queue'} ) {
return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
-
- if ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+ if ( $RT::ServiceAgreements{'QueueDefault'} &&
+ ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } )) {
return $info unless ref $info;
return $info->{'Level'} || $RT::ServiceAgreements{'Default'};
}
-----------------------------------------------------------------------
More information about the rt-commit
mailing list