[Rt-commit] rt branch, 4.4/sla, created. rt-4.2.11-131-g93d4f6f
? sunnavy
sunnavy at bestpractical.com
Thu Jul 9 09:27:46 EDT 2015
The branch, 4.4/sla has been created
at 93d4f6f99a7742d1ec23dffc4fab5850fe3f16ff (commit)
- Log -----------------------------------------------------------------
commit e19d40d5b247480d7bc19f54e8ab4e703775321e
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 87b9520..94a5084 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 dd1daf5..59e7854 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..82b186a
--- /dev/null
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -0,0 +1,182 @@
+# 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 $meta =
+ $RT::ServiceAgreements{ 'Levels' }{ $level }
+ ? $RT::ServiceAgreements{ 'Levels' }{ $level }{ $is_outside ? 'Response' : 'KeepInLoop' }
+ : undef;
+ if ( $meta && $meta->{IgnoreOnStatuses} && $meta->{RecalculateDueOnIgnoredStatusChange} ) {
+ my $last_ignored_status_txn = $self->LastIgnoredStatusAct(@{$meta->{IgnoreOnStatuses}});
+ $last_reply = $last_ignored_status_txn
+ if $last_ignored_status_txn && $last_reply->Created lt $last_ignored_status_txn->Created;
+ }
+
+ 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);
+}
+
+sub LastIgnoredStatusAct {
+ my $self = shift;
+ my @statuses = @_;
+ my $txns = $self->TicketObj->Transactions;
+ $txns->Limit( FIELD => 'FIELD', VALUE => 'Status' );
+ $txns->Limit( FIELD => 'OldValue', OPERATOR => 'IN', VALUE => \@statuses );
+ $txns->OrderByCols(
+ { FIELD => 'Created', ORDER => 'DESC' },
+ { FIELD => 'id', ORDER => 'DESC' },
+ );
+ return $txns->First;
+}
+
+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..961fbfe
--- /dev/null
+++ b/lib/RT/SLA.pm
@@ -0,0 +1,551 @@
+# 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.
+The option C<RecalculateDueOnIgnoredStatusChange> could get around the
+"probably be overdue" issue by considering the last ignored status date too.
+e.g.
+
+ 'level x' => {
+ KeepInLoop => {
+ BusinessMinutes => 60,
+ RecalculateDueOnIgnoredStatusChange => 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 ( 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 0b4a9d1e33144face29f75101b538474e5850472
Author: sunnavy <sunnavy at bestpractical.com>
Date: Thu Jul 9 03:17:58 2015 +0800
make cf "SLA" configurable and don't create it by default
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 94a5084..192806a 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2920,7 +2920,20 @@ Set(%Lifecycles,
+=head1 SLA
+=over 4
+
+=item C<$SLACustomField>
+
+Service level agreements of tickets is controlled by an SLA custom field (CF).
+This specifies the custom field name, by default it's "SLA"
+
+=cut
+
+Set( $SLACustomField, 'SLA' );
+
+=back
=head1 Administrative interface
diff --git a/etc/initialdata b/etc/initialdata
index 59e7854..2d09f30 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/upgrade/4.3.7/content b/etc/upgrade/4.3.7/content
index 2c24760..492cabb 100644
--- a/etc/upgrade/4.3.7/content
+++ b/etc/upgrade/4.3.7/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/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
index 82b186a..52c1d67 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->FirstCustomFieldValue(RT->Config->Get('SLACustomField')) ) {
$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->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
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..ffd4b18 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->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
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..2bbac6b 100644
--- a/lib/RT/Condition/SLA.pm
+++ b/lib/RT/Condition/SLA.pm
@@ -56,7 +56,13 @@ use base qw(RT::SLA RT::Condition);
=cut
-sub SLAIsApplied { return 1 }
+sub SLAIsApplied {
+ my $self = shift;
+ return 0 unless RT->Config->Get('SLACustomField');
+ my $cf = RT::CustomField->new(RT->SystemUser);
+ $cf->Load(RT->Config->Get('SLACustomField'));
+ return $cf->id ? 1 : 0;
+}
=head1 IsCustomFieldChange
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
index bc49970..e9a8a92 100644
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -62,11 +62,11 @@ value set.
sub IsApplicable {
my $self = shift;
+ return 0 unless $self->SLAIsApplied;
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->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
return 1;
}
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
index d436c27..734b9f3 100644
--- a/lib/RT/Condition/SLA_RequireDueSet.pm
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -69,14 +69,14 @@ 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->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
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->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
return 0;
}
- return 1 if $self->IsCustomFieldChange('SLA');
+ return 1 if $self->IsCustomFieldChange(RT->Config->Get('SLACustomField'));
return 0;
}
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
index 895ccaa..b89256a 100644
--- a/lib/RT/Condition/SLA_RequireStartsSet.pm
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -64,8 +64,9 @@ Applies if Starts date is not set for the ticket.
sub IsApplicable {
my $self = shift;
+ return 0 unless $self->SLAIsApplied;
return 0 if $self->TicketObj->StartsObj->Unix > 0;
- return 0 unless $self->TicketObj->FirstCustomFieldValue('SLA');
+ return 0 unless $self->TicketObj->FirstCustomFieldValue(RT->Config->Get('SLACustomField'));
return 1;
}
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index 961fbfe..3b00428 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -61,9 +61,9 @@ 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.
+You need to create/apply it and specify it in config like:
+
+ Set($SLACustomField, 'SLA');
It's possible to define different set of levels for different
queues. You can create several CFs with the same name and
@@ -514,7 +514,7 @@ sub CalculateTime {
sub GetCustomField {
my $self = shift;
- my %args = (Ticket => undef, CustomField => 'SLA', @_);
+ my %args = (Ticket => undef, CustomField => RT->Config->Get('SLACustomField'), @_);
unless ( $args{'Ticket'} ) {
$args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
}
commit 93d4f6f99a7742d1ec23dffc4fab5850fe3f16ff
Author: sunnavy <sunnavy at bestpractical.com>
Date: Thu Jul 9 04:29:23 2015 +0800
migrate ServiceAgreements/BusinessHours and doc to standard config
diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 192806a..665af7b 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2933,6 +2933,356 @@ This specifies the custom field name, by default it's "SLA"
Set( $SLACustomField, 'SLA' );
+=item C<%ServiceAgreements>
+
+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 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.
+The option C<RecalculateDueOnIgnoredStatusChange> could get around the
+"probably be overdue" issue by considering the last ignored status date too.
+e.g.
+
+ 'level x' => {
+ KeepInLoop => {
+ BusinessMinutes => 60,
+ RecalculateDueOnIgnoredStatusChange => 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, );
+
+=item 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 their tickets.
+Just grant them ModifyCustomField right.
+
+=cut
+
=back
=head1 Administrative interface
diff --git a/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
index 52c1d67..00576bb 100644
--- a/lib/RT/Action/SLA_SetDue.pm
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -91,8 +91,8 @@ sub Commit {
);
my $meta =
- $RT::ServiceAgreements{ 'Levels' }{ $level }
- ? $RT::ServiceAgreements{ 'Levels' }{ $level }{ $is_outside ? 'Response' : 'KeepInLoop' }
+ RT->Config->Get('ServiceAgreements') && RT->Config->Get('ServiceAgreements')->{ 'Levels' }{ $level }
+ ? RT->Config->Get('ServiceAgreements')->{ 'Levels' }{ $level }{ $is_outside ? 'Response' : 'KeepInLoop' }
: undef;
if ( $meta && $meta->{IgnoreOnStatuses} && $meta->{RecalculateDueOnIgnoredStatusChange} ) {
my $last_ignored_status_txn = $self->LastIgnoredStatusAct(@{$meta->{IgnoreOnStatuses}});
@@ -131,7 +131,7 @@ sub IsOutsideActor {
# owner is always treated as inside actor
return 0 if $actor->id == $self->TicketObj->Owner;
- if ( $RT::ServiceAgreements{'AssumeOutsideActor'} ) {
+ if ( RT->Config->Get('ServiceAgreements')->{'AssumeOutsideActor'} ) {
# All non-admincc users are outside actors
return 0 if $self->TicketObj ->AdminCc->HasMemberRecursively( $actor )
or $self->TicketObj->QueueObj->AdminCc->HasMemberRecursively( $actor );
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index 3b00428..fb5f8fb 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -58,338 +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).
-You need to create/apply it and specify it in config like:
-
- Set($SLACustomField, 'SLA');
-
-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.
-The option C<RecalculateDueOnIgnoredStatusChange> could get around the
-"probably be overdue" issue by considering the last ignored status date too.
-e.g.
-
- 'level x' => {
- KeepInLoop => {
- BusinessMinutes => 60,
- RecalculateDueOnIgnoredStatusChange => 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 {
@@ -398,8 +66,8 @@ sub BusinessHours {
require Business::Hours;
my $res = new Business::Hours;
- $res->business_hours( %{ $RT::ServiceBusinessHours{ $name } } )
- if $RT::ServiceBusinessHours{ $name };
+ $res->business_hours( %{ RT->Config->Get('ServiceBusinessHours')->{ $name } } )
+ if RT->Config->Get('ServiceBusinessHours') && RT->Config->Get('ServiceBusinessHours')->{ $name };
return $res;
}
@@ -414,7 +82,7 @@ sub Agreement {
@_
);
- my $meta = $RT::ServiceAgreements{'Levels'}{ $args{'Level'} };
+ my $meta = RT->Config->Get('ServiceAgreements') ? RT->Config->Get('ServiceAgreements')->{'Levels'}{ $args{'Level'} } : undef;
return undef unless $meta;
if ( exists $meta->{'StartImmediately'} || !defined $meta->{'Starts'} ) {
@@ -446,8 +114,8 @@ sub Agreement {
$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'};
+ if ( $args{'Queue'} && ref RT->Config->Get('ServiceAgreements')->{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+ $res{'Timezone'} = RT->Config->Get('ServiceAgreements')->{'QueueDefault'}{ $args{'Queue'}->Name }{'Timezone'};
}
$res{'Timezone'} ||= $meta->{'Timezone'} || $RT::Timezone;
@@ -538,12 +206,12 @@ sub GetDefaultServiceLevel {
if ( $args{'Queue'} ) {
return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
- if ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+ if ( my $info = RT->Config->Get('ServiceAgreements')->{'QueueDefault'}{ $args{'Queue'}->Name } ) {
return $info unless ref $info;
- return $info->{'Level'} || $RT::ServiceAgreements{'Default'};
+ return $info->{'Level'} || RT->Config->Get('ServiceAgreements')->{'Default'};
}
}
- return $RT::ServiceAgreements{'Default'};
+ return RT->Config->Get('ServiceAgreements')->{'Default'};
}
RT::Base->_ImportOverlays();
-----------------------------------------------------------------------
More information about the rt-commit
mailing list