[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