[Rt-commit] rt branch, 4.4/sla, created. rt-4.2.11-141-g8bfb984

? sunnavy sunnavy at bestpractical.com
Sun Aug 2 15:17:53 EDT 2015


The branch, 4.4/sla has been created
        at  8bfb984257cd45a4ef9811a4ba10607b4e3b570a (commit)

- Log -----------------------------------------------------------------
commit c0b308ef30c4a617bf353142b9c07871486c93bb
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jul 9 01:43:20 2015 +0800

    import sla extension
    
    this version works exactly like the sla extension.
    it's roughly a copy/paste with necessary renamings

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 1c88a11..5deaade 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -118,11 +118,10 @@ Set($Timezone, "US/Eastern");
 Once a plugin has been downloaded and installed, use C<Plugin()> to add
 to the enabled C<@Plugins> list:
 
-    Plugin( "RT::Extension::SLA" );
     Plugin( "RT::Authen::ExternalAuth" );
 
-RT will also accept the distribution name (i.e. C<RT-Extension-SLA>)
-instead of the package name (C<RT::Extension::SLA>).
+RT will also accept the distribution name (i.e. C<RT-Authen-ExternalAuth>)
+instead of the package name (C<RT::Authen::ExternalAuth>).
 
 =cut
 
diff --git a/etc/RT_SiteConfig.pm b/etc/RT_SiteConfig.pm
index a5e8c46..0f839a6 100644
--- a/etc/RT_SiteConfig.pm
+++ b/etc/RT_SiteConfig.pm
@@ -25,7 +25,6 @@ Set( $rtname, 'example.com');
 
 # You must install Plugins on your own, this is only an example
 # of the correct syntax to use when activating them:
-#     Plugin( "RT::Extension::SLA" );
 #     Plugin( "RT::Authen::ExternalAuth" );
 
 1;
diff --git a/etc/initialdata b/etc/initialdata
index 5df770b..21650a6 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -14,6 +14,17 @@
 @Groups = (
 );
 
+ at CustomFields = (
+    {
+        Name        => 'SLA',
+        Queue       => 0,
+        Type        => 'SelectSingle',
+        Disabled    => 0,
+        Description => 'Service Level Agreement',
+        Values      => [ ],
+    },
+);
+
 @Queues = ({ Name              => 'General',
              Description       => 'The default queue',
              CorrespondAddress => "",
@@ -111,6 +122,18 @@
     { Name        => 'Send Forward',                 # loc
       Description => 'Send forwarded message',       # loc
       ExecModule  => 'SendForward', },
+    {  Name        => '[SLA] Set default service level', # loc
+       Description => 'Set service level according to the config' , # loc
+       ExecModule  => 'SLA_SetDefault',
+    },
+    {  Name        => '[SLA] Set starts date', # loc
+       Description => 'Set the starts date according to an agreement' , # loc
+       ExecModule  => 'SLA_SetStarts',
+    },
+    {  Name        => '[SLA] Set due date', # loc
+       Description => 'Set the due date according to an agreement' , # loc
+       ExecModule  => 'SLA_SetDue',
+    },
 );
 
 @ScripConditions = (
@@ -219,6 +242,21 @@
        ApplicableTransTypes => 'Status,Set',
        ExecModule           => 'ReopenTicket',
     },
+    {  Name        => '[SLA] Require default', # loc
+       Description => 'Detect a situation when we should set default service level' , # loc
+       ApplicableTransTypes => 'Create',
+       ExecModule => 'SLA_RequireDefault',
+    },
+    {  Name        => '[SLA] Require Starts set', # loc
+       Description => 'Detect a situation when we should set Starts date' , # loc
+       ApplicableTransTypes => 'Create,CustomField',
+       ExecModule => 'SLA_RequireStartsSet',
+    },
+    {  Name        => '[SLA] Require Due set', # loc
+       Description => 'Detect a situation when we should set Due date' , # loc
+       ApplicableTransTypes => 'Create,CustomField,Correspond,Set,Status',
+       ExecModule => 'SLA_RequireDueSet',
+    },
 
 );
 
@@ -792,6 +830,18 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
        ScripCondition => 'On Forward Ticket',
        ScripAction    => 'Send Forward',
        Template       => 'Forward Ticket' },
+    {  Description       => "[SLA] Set default service level if needed",
+       ScripCondition    => '[SLA] Require Default',
+       ScripAction       => '[SLA] Set default service level',
+       Template          => 'Blank' },
+    {  Description       => "[SLA] Set starts date if needed",
+       ScripCondition    => '[SLA] Require starts set',
+       ScripAction       => '[SLA] Set starts date',
+       Template          => 'Blank' },
+    {  Description       => "[SLA] Set due date if needed",
+       ScripCondition    => '[SLA] Require due set',
+       ScripAction       => '[SLA] Set due date',
+       Template          => 'Blank' },
 );
 
 @ACL = (
diff --git a/etc/upgrade/4.3.7/content b/etc/upgrade/4.3.7/content
new file mode 100644
index 0000000..2c24760
--- /dev/null
+++ b/etc/upgrade/4.3.7/content
@@ -0,0 +1,62 @@
+use strict;
+use warnings;
+
+our @CustomFields = (
+    {
+        Name        => 'SLA',
+        Queue       => 0,
+        Type        => 'SelectSingle',
+        Disabled    => 0,
+        Description => 'Service Level Agreement',
+        Values      => [ ],
+    },
+);
+
+our @ScripConditions = (
+    {  Name        => '[SLA] Require default', # loc
+       Description => 'Detect a situation when we should set default service level' , # loc
+       ApplicableTransTypes => 'Create',
+       ExecModule => 'SLA_RequireDefault',
+    },
+    {  Name        => '[SLA] Require Starts set', # loc
+       Description => 'Detect a situation when we should set Starts date' , # loc
+       ApplicableTransTypes => 'Create,CustomField',
+       ExecModule => 'SLA_RequireStartsSet',
+    },
+    {  Name        => '[SLA] Require Due set', # loc
+       Description => 'Detect a situation when we should set Due date' , # loc
+       ApplicableTransTypes => 'Create,CustomField,Correspond,Set,Status',
+       ExecModule => 'SLA_RequireDueSet',
+    },
+);
+
+our @ScripActions = (
+    {  Name        => '[SLA] Set default service level', # loc
+       Description => 'Set service level according to the config' , # loc
+       ExecModule  => 'SLA_SetDefault',
+    },
+    {  Name        => '[SLA] Set starts date', # loc
+       Description => 'Set the starts date according to an agreement' , # loc
+       ExecModule  => 'SLA_SetStarts',
+    },
+    {  Name        => '[SLA] Set due date', # loc
+       Description => 'Set the due date according to an agreement' , # loc
+       ExecModule  => 'SLA_SetDue',
+    },
+);
+
+our @Scrips = (
+    {  Description       => "[SLA] Set default service level if needed",
+       ScripCondition    => '[SLA] Require Default',
+       ScripAction       => '[SLA] Set default service level',
+       Template          => 'Blank' },
+    {  Description       => "[SLA] Set starts date if needed",
+       ScripCondition    => '[SLA] Require starts set',
+       ScripAction       => '[SLA] Set starts date',
+       Template          => 'Blank' },
+    {  Description       => "[SLA] Set due date if needed",
+       ScripCondition    => '[SLA] Require due set',
+       ScripAction       => '[SLA] Set due date',
+       Template          => 'Blank' },
+);
+
diff --git a/lib/RT/Action/SLA.pm b/lib/RT/Action/SLA.pm
new file mode 100644
index 0000000..d54216e
--- /dev/null
+++ b/lib/RT/Action/SLA.pm
@@ -0,0 +1,102 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+
+use strict;
+use warnings;
+
+package RT::Action::SLA;
+
+use base qw(RT::SLA RT::Action);
+
+=head1 NAME
+
+RT::Action::SLA - base class for all actions in the extension
+
+=head1 DESCRIPTION
+
+It's not a real action, but container for subclassing which provide
+help methods for other actions.
+
+=head1 METHODS
+
+=head2 SetDateField NAME VALUE
+
+Sets specified ticket's date field to the value, doesn't update
+if field is set already. VALUE is unix time.
+
+=cut
+
+sub SetDateField {
+    my $self = shift;
+    my ($type, $value) = (@_);
+
+    my $ticket = $self->TicketObj;
+
+    my $method = $type .'Obj';
+    if ( defined $value ) {
+        return 1 if $ticket->$method->Unix == $value;
+    } else {
+        return 1 if $ticket->$method->Unix <= 0;
+    }
+
+    my $date = RT::Date->new( $RT::SystemUser );
+    $date->Set( Format => 'unix', Value => $value );
+
+    $method = 'Set'. $type;
+    return 1 if $ticket->$type eq $date->ISO;
+    my ($status, $msg) = $ticket->$method( $date->ISO );
+    unless ( $status ) {
+        $RT::Logger->error("Couldn't set $type date: $msg");
+        return 0;
+    }
+
+    return 1;
+}
+
+1;
diff --git a/lib/RT/Action/SLA_SetDefault.pm b/lib/RT/Action/SLA_SetDefault.pm
new file mode 100644
index 0000000..1f06a19
--- /dev/null
+++ b/lib/RT/Action/SLA_SetDefault.pm
@@ -0,0 +1,104 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetDefault;
+
+use base qw(RT::Action::SLA);
+
+=head1 NAME
+
+RT::Action::SLA_SetDefault - set default SLA value
+
+=head1 DESCRIPTION
+
+Sets a default level of service. Transaction's created field is used
+to calculate if things happen in hours or out of. Default value then
+figured from L<InHoursDefault|XXX> and L<OutOfHoursDefault|XXX> options.
+
+This action doesn't check if the ticket has a value already, so you
+have to use it with condition that checks this fact for you, however
+such behaviour allows you to force setting up default using custom
+condition. The default condition for this action is
+L<RT::Condition::SLA_RequireDefault>.
+
+=cut
+
+sub Prepare { return 1 }
+sub Commit {
+    my $self = shift;
+
+    my $cf = $self->GetCustomField;
+    unless ( $cf->id ) {
+        $RT::Logger->warning("SLA scrip applied to a queue that has no SLA CF");
+        return 1;
+    }
+
+    my $level = $self->GetDefaultServiceLevel;
+    unless ( $level ) {
+        $RT::Logger->info(
+            "No default service level for ticket #". $self->TicketObj->id 
+            ." in queue ". $self->TicketObj->QueueObj->Name );
+        return 1;
+    }
+
+    my ($status, $msg) = $self->TicketObj->AddCustomFieldValue(
+        Field => $cf->id,
+        Value => $level,
+    );
+    unless ( $status ) {
+        $RT::Logger->error("Couldn't set service level: $msg");
+        return 0;
+    }
+
+    return 1;
+};
+
+1;
diff --git a/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
new file mode 100644
index 0000000..c790df8
--- /dev/null
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -0,0 +1,159 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetDue;
+
+use base qw(RT::Action::SLA);
+
+=head2 Prepare
+
+Checks if the ticket has service level defined.
+
+=cut
+
+sub Prepare {
+    my $self = shift;
+
+    unless ( $self->TicketObj->FirstCustomFieldValue('SLA') ) {
+        $RT::Logger->error('SLA::SetDue scrip has been applied to ticket #'
+            . $self->TicketObj->id . ' that has no SLA defined');
+        return 0;
+    }
+
+    return 1;
+}
+
+=head2 Commit
+
+Set the Due date accordingly to SLA.
+
+=cut
+
+sub Commit {
+    my $self = shift;
+
+    my $ticket = $self->TicketObj;
+    my $txn = $self->TransactionObj;
+    my $level = $ticket->FirstCustomFieldValue('SLA');
+
+    my ($last_reply, $is_outside) = $self->LastEffectiveAct;
+    $RT::Logger->debug(
+        'Last effective '. ($is_outside? '':'non-') .'outside actors\' reply'
+        .' to ticket #'. $ticket->id .' is txn #'. $last_reply->id
+    );
+
+    my $response_due = $self->Due(
+        Ticket => $ticket,
+        Level => $level,
+        Type => $is_outside? 'Response' : 'KeepInLoop',
+        Time => $last_reply->CreatedObj->Unix,
+    );
+
+    my $resolve_due = $self->Due(
+        Ticket => $ticket,
+        Level => $level,
+        Type => 'Resolve',
+        Time => $ticket->CreatedObj->Unix,
+    );
+
+    my $due;
+    $due = $response_due if defined $response_due;
+    $due = $resolve_due unless defined $due;
+    $due = $resolve_due if defined $due && defined $resolve_due && $resolve_due < $due;
+
+    return $self->SetDateField( Due => $due );
+}
+
+sub IsOutsideActor {
+    my $self = shift;
+    my $txn = shift || $self->TransactionObj;
+
+    my $actor = $txn->CreatorObj->PrincipalObj;
+
+    # owner is always treated as inside actor
+    return 0 if $actor->id == $self->TicketObj->Owner;
+
+    if ( $RT::ServiceAgreements{'AssumeOutsideActor'} ) {
+        # All non-admincc users are outside actors
+        return 0 if $self->TicketObj          ->AdminCc->HasMemberRecursively( $actor )
+                 or $self->TicketObj->QueueObj->AdminCc->HasMemberRecursively( $actor );
+
+        return 1;
+    } else {
+        # Only requestors are outside actors
+        return 1 if $self->TicketObj->Requestors->HasMemberRecursively( $actor );
+        return 0;
+    }
+}
+
+sub LastEffectiveAct {
+    my $self = shift;
+
+    my $txns = $self->TicketObj->Transactions;
+    $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+    $txns->Limit( FIELD => 'Type', VALUE => 'Create' );
+    $txns->OrderByCols(
+        { FIELD => 'Created', ORDER => 'DESC' },
+        { FIELD => 'id', ORDER => 'DESC' },
+    );
+
+    my $res;
+    while ( my $txn = $txns->Next ) {
+        unless ( $self->IsOutsideActor( $txn ) ) {
+            last if $res;
+            return ($txn);
+        }
+        $res = $txn;
+    }
+    return ($res, 1);
+}
+
+1;
diff --git a/lib/RT/Action/SLA_SetStarts.pm b/lib/RT/Action/SLA_SetStarts.pm
new file mode 100644
index 0000000..59b2fe1
--- /dev/null
+++ b/lib/RT/Action/SLA_SetStarts.pm
@@ -0,0 +1,92 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Action::SLA_SetStarts;
+
+use base qw(RT::Action::SLA);
+
+=head1 NAME
+
+RT::Action::SLA_SetStarts - set starts date field of a ticket according to SLA
+
+=head1 DESCRIPTION
+
+Look up the SLA of the ticket and set the Starts date accordingly. Nothing happens
+if the ticket has no SLA defined.
+
+Note that this action doesn't check if Starts field is set already, so you can
+use it to set the field in a force mode or can protect field using a condition
+that checks value of Starts.
+
+=cut
+
+sub Prepare { return 1 }
+
+sub Commit {
+    my $self = shift;
+
+    my $ticket = $self->TicketObj;
+
+    my $level = $ticket->FirstCustomFieldValue('SLA');
+    unless ( $level ) {
+        $RT::Logger->debug('Ticket #'. $ticket->id .' has no service level defined, skip setting Starts');
+        return 1;
+    }
+
+    my $starts = $self->Starts(
+        Ticket => $ticket,
+        Level => $level,
+        Time => $ticket->CreatedObj->Unix,
+    );
+
+    return $self->SetDateField( Starts => $starts );
+}
+
+1;
diff --git a/lib/RT/Condition/SLA.pm b/lib/RT/Condition/SLA.pm
new file mode 100644
index 0000000..b164fae
--- /dev/null
+++ b/lib/RT/Condition/SLA.pm
@@ -0,0 +1,82 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Condition::SLA;
+use base qw(RT::SLA RT::Condition);
+
+=head1 SLAIsApplied
+
+=cut
+
+sub SLAIsApplied { return 1 }
+
+=head1 IsCustomFieldChange
+
+=cut
+
+sub IsCustomFieldChange {
+    my $self = shift;
+    my $cf_name = shift;
+
+    my $txn = $self->TransactionObj;
+    
+    return 0 unless $txn->Type eq 'CustomField';
+
+    my $cf = $self->GetCustomField( CustomField => $cf_name );
+    unless ( $cf->id ) {
+        $RT::Logger->debug("Custom field '$cf_name' is not applied to ticket #". $self->TicketObj->id);
+        return 0;
+    }
+    return 0 unless $cf->id == $txn->Field;
+    return 1;
+}
+
+1;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
new file mode 100644
index 0000000..bc49970
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -0,0 +1,74 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireDefault;
+use base qw(RT::Condition::SLA);
+
+=head1 IsApplicable
+
+Applies the current scrip when SLA is not set. Returns true on create,
+but only if SLA CustomField is applied to the ticket and it has no
+value set.
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    return 0 unless $self->TransactionObj->Type eq 'Create';
+    my $ticket = $self->TicketObj;
+    return 0 unless lc($ticket->Type) eq 'ticket';
+    return 0 if $ticket->FirstCustomFieldValue('SLA');
+    return 0 unless $self->SLAIsApplied;
+    return 1;
+}
+
+1;
+
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
new file mode 100644
index 0000000..d436c27
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -0,0 +1,83 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireDueSet;
+
+use base qw(RT::Condition::SLA);
+
+=head1 NAME
+
+RT::Condition::SLA_RequireDueSet - checks if Due date require update
+
+=head1 DESCRIPTION
+
+Checks if Due date require update. This should be done when we create
+a ticket and it has service level value or when we set service level.
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    return 0 unless $self->SLAIsApplied;
+
+    my $type = $self->TransactionObj->Type;
+    if ( $type eq 'Create' || $type eq 'Correspond' ) {
+        return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+        return 0;
+    }
+    elsif ( $type eq 'Status' || ($type eq 'Set' && $self->TransactionObj->Field eq 'Status') ) {
+        return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+        return 0;
+    }
+    return 1 if $self->IsCustomFieldChange('SLA');
+    return 0;
+}
+
+1;
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
new file mode 100644
index 0000000..895ccaa
--- /dev/null
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -0,0 +1,72 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::Condition::SLA_RequireStartsSet;
+
+use base qw(RT::Condition::SLA);
+
+=head1 NAME
+
+RT::Condition::SLA_RequireStartsSet - checks if Starts date is not set
+
+=head1 DESCRIPTION
+
+Applies if Starts date is not set for the ticket.
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    return 0 if $self->TicketObj->StartsObj->Unix > 0;
+    return 0 unless $self->TicketObj->FirstCustomFieldValue('SLA');
+    return 1;
+}
+
+1;
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index 206f17f..ba9d023 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1190,6 +1190,30 @@ sub SetDefaultValue {
     }
 }
 
+sub SLA {
+    my $self = shift;
+    my $value = shift;
+    return undef unless $self->CurrentUserHasRight('SeeQueue');
+
+    my $attr = $self->FirstAttribute('SLA') or return undef;
+    return $attr->Content;
+}
+
+sub SetSLA {
+    my $self = shift;
+    my $value = shift;
+
+    return ( 0, $self->loc('Permission Denied') )
+        unless $self->CurrentUserHasRight('AdminQueue');
+
+    my ($status, $msg) = $self->SetAttribute(
+        Name        => 'SLA',
+        Description => 'Default Queue SLA',
+        Content     => $value,
+    );
+    return ($status, $msg) unless $status;
+    return ($status, $self->loc("Queue's default service level has been changed"));
+}
 
 RT::Base->_ImportOverlays();
 
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
new file mode 100644
index 0000000..817e5e6
--- /dev/null
+++ b/lib/RT/SLA.pm
@@ -0,0 +1,595 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+package RT::SLA;
+
+=head1 NAME
+
+RT::SLA - Service Level Agreements for RT
+
+=head1 DESCRIPTION
+
+Automated due dates using service levels.
+
+=head1 CONFIGURATION
+
+Service level agreements of tickets is controlled by an SLA custom field (CF).
+This field is created during C<make initdb> step (above) and applied globally.
+This CF MUST be of C<select one value> type. Values of the CF define the
+service levels.
+
+It's possible to define different set of levels for different
+queues. You can create several CFs with the same name and
+different set of values. But if you move tickets between
+queues a lot then it's going to be a problem and it's preferred
+to use B<ONE> SLA custom field.
+
+There is no WebUI in the current version. Almost everything is
+controlled in the RT's config using option C<%RT::ServiceAgreements>
+and C<%RT::ServiceBusinessHours>. For example:
+
+    %RT::ServiceAgreements = (
+        Default => '4h',
+        QueueDefault => {
+            'Incident' => '2h',
+        },
+        Levels => {
+            '2h' => { Resolve => { RealMinutes => 60*2 } },
+            '4h' => { Resolve => { RealMinutes => 60*4 } },
+        },
+    );
+
+In this example I<Incident> is the name of the queue, and I<2h> is the name of
+the SLA which will be applied to this queue by default.
+
+Each service level can be described using several options:
+L<Starts|/"Starts (interval, first business minute)">,
+L<Resolve|/"Resolve and Response (interval, no defaults)">,
+L<Response|/"Resolve and Response (interval, no defaults)">,
+L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
+L<OutOfHours|/"OutOfHours (struct, no default)">
+and L<ServiceBusinessHours|/"Configuring business hours">.
+
+=head2 Starts (interval, first business minute)
+
+By default when a ticket is created Starts date is set to
+first business minute after time of creation. In other
+words if a ticket is created during business hours then
+Starts will be equal to Created time, otherwise Starts will
+be beginning of the next business day.
+
+However, if you provide 24/7 support then you most
+probably would be interested in Starts to be always equal
+to Created time.
+
+Starts option can be used to adjust behaviour. Format
+of the option is the same as format for deadlines which
+described later in details. RealMinutes, BusinessMinutes
+options and OutOfHours modifiers can be used here like
+for any other deadline. For example:
+
+    'standard' => {
+        # give people 15 minutes
+        Starts   => { BusinessMinutes => 15  },
+    },
+
+You can still use old option StartImmediately to set
+Starts date equal to Created date.
+
+Example:
+
+    '24/7' => {
+        StartImmediately => 1,
+        Response => { RealMinutes => 30 },
+    },
+
+But it's the same as:
+
+    '24/7' => {
+        Starts => { RealMinutes => 0 },
+        Response => { RealMinutes => 30 },
+    },
+
+=head2 Resolve and Response (interval, no defaults)
+
+These two options define deadlines for resolve of a ticket
+and reply to customer(requestors) questions accordingly.
+
+You can define them using real time, business or both. Read more
+about the latter L<below|/"Using both Resolve and Response in the same level">.
+
+The Due date field is used to store calculated deadlines.
+
+=head3 Resolve
+
+Defines deadline when a ticket should be resolved. This option is
+quite simple and straightforward when used without L</Response>.
+
+Example:
+
+    # 8 business hours
+    'simple' => { Resolve => 60*8 },
+    ...
+    # one real week
+    'hard' => { Resolve => { RealMinutes => 60*24*7 } },
+
+=head3 Response
+
+In many companies providing support service(s) resolve time of a ticket
+is less important than time of response to requestors from staff
+members.
+
+You can use Response option to define such deadlines.  The Due date is
+set when a ticket is created, unset when a worker replies, and re-set
+when the requestor replies again -- until the ticket is closed, when the
+ticket's Due date is unset.
+
+B<NOTE> that this behaviour changes when Resolve and Response options
+are combined; see L</"Using both Resolve and Response in the same
+level">.
+
+Note that by default, only the requestors on the ticket are considered
+"outside actors" and thus require a Response due date; all other email
+addresses are treated as workers of the ticket, and thus count as
+meeting the SLA.  If you'd like to invert this logic, so that the Owner
+and AdminCcs are the only worker email addresses, and all others are
+external, see the L</AssumeOutsideActor> configuration.
+
+The owner is never treated as an outside actor; if they are also the
+requestor of the ticket, it will have no SLA.
+
+If an outside actor replies multiple times, their later replies are
+ignored; the deadline is awlways calculated from the oldest
+correspondence from the outside actor.
+
+
+=head3 Using both Resolve and Response in the same level
+
+Resolve and Response can be combined. In such case due date is set
+according to the earliest of two deadlines and never is dropped to
+'not set'.
+
+If a ticket met its Resolve deadline then due date stops "flipping",
+is freezed and the ticket becomes overdue. Before that moment when
+an inside actor replies to a ticket, due date is changed to Resolve
+deadline instead of 'Not Set', as well this happens when a ticket
+is closed. So all the time due date is defined.
+
+Example:
+
+    'standard delivery' => {
+        Response => { RealMinutes => 60*1  }, # one hour
+        Resolve  => { RealMinutes => 60*24 }, # 24 real hours
+    },
+
+A client orders goods and due date of the order is set to the next one
+hour, you have this hour to process the order and write a reply.
+As soon as goods are delivered you resolve tickets and usually meet
+Resolve deadline, but if you don't resolve or user replies then most
+probably there are problems with delivery of the goods. And if after
+a week you keep replying to the client and always meeting one hour
+response deadline that doesn't mean the ticket is not over due.
+Due date was frozen 24 hours after creation of the order.
+
+=head3 Using business and real time in one option
+
+It's quite rare situation when people need it, but we've decided
+that business is applied first and then real time when deadline
+described using both types of time. For example:
+
+    'delivery' => {
+        Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
+    },
+    'fast delivery' {
+        StartImmediately => 1,
+        Resolve => { RealMinutes => 60*8 },
+    },
+
+For delivery requests which come into the system during business
+hours these levels define the same deadlines, otherwise the first
+level set deadline to 8 real hours starting from the next business
+day, when tickets with the second level should be resolved in the
+next 8 hours after creation.
+
+=head2 Keep in loop (interval, no defaults)
+
+If response deadline is used then Due date is changed to repsonse
+deadline or to "Not Set" when staff replies to a ticket. In some
+cases you want to keep requestors in loop and keed them up to date
+every few hours. KeepInLoop option can be used to achieve this.
+
+    'incident' => {
+        Response   => { RealMinutes => 60*1  }, # one hour
+        KeepInLoop => { RealMinutes => 60*2 }, # two hours
+        Resolve    => { RealMinutes => 60*24 }, # 24 real hours
+    },
+
+In the above example Due is set to one hour after creation, reply
+of a inside actor moves Due date two hours forward, outside actors'
+replies move Due date to one hour and resolve deadine is 24 hours.
+
+=head2 Modifying Agreements
+
+=head3 OutOfHours (struct, no default)
+
+Out of hours modifier. Adds more real or business minutes to resolve
+and/or reply options if event happens out of business hours, read also
+</"Configuring business hours"> below.
+
+Example:
+
+    'level x' => {
+        OutOfHours => { Resolve => { RealMinutes => +60*24 } },
+        Resolve    => { RealMinutes => 60*24 },
+    },
+
+If a request comes into the system during night then supporters have two
+hours, otherwise only one.
+
+    'level x' => {
+        OutOfHours => { Response => { BusinessMinutes => +60*2 } },
+        Resolve    => { BusinessMinutes => 60 },
+    },
+
+Supporters have two additional hours in the morning to deal with bunch
+of requests that came into the system during the last night.
+
+=head3 IgnoreOnStatuses (array, no default)
+
+Allows you to ignore a deadline when ticket has certain status. Example:
+
+    'level x' => {
+        KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
+    },
+
+In above example KeepInLoop deadline is ignored if ticket is stalled.
+
+B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
+Due date is calculated from the last action (reply, SLA change, etc) which fits
+the SLA type (Response, Starts, KeepInLoop, etc).  This means if a ticket in
+the above example flips from stalled to open without a reply, the ticket will
+probably be overdue.  In most cases this shouldn't be a problem since moving
+out of stalled-like statuses is often the result of RT's auto-open on reply
+scrip, therefore ensuring there's a new reply to calculate Due from.  The
+overall effect is that ignored statuses don't let the Due date drift
+arbitrarily, which could wreak havoc on your SLA performance.
+C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
+overdue" issue by excluding the time spent on ignored statuses.
+
+        'level x' => {
+            KeepInLoop => {
+                BusinessMinutes => 60,
+                ExcludeTimeOnIgnoredStatuses => 1,
+                IgnoreOnStatuses => ['stalled'],
+            },
+        },
+
+=head2 Configuring business hours
+
+In the config you can set one or more work schedules. Use the following
+format:
+
+    %RT::ServiceBusinessHours = (
+        'Default' => {
+            ... description ...
+        },
+        'Support' => {
+            ... description ...
+        },
+        'Sales' => {
+            ... description ...
+        },
+    );
+
+Read more about how to describe a schedule in L<Business::Hours>.
+
+=head3 Defining different business hours for service levels
+
+Each level supports BusinessHours option to specify your own business
+hours.
+
+    'level x' => {
+        BusinessHours => 'work just in Monday',
+        Resolve    => { BusinessMinutes => 60 },
+    },
+
+then %RT::ServiceBusinessHours should have the corresponding definition:
+
+    %RT::ServiceBusinessHours = (
+        'work just in Monday' => {
+            1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
+        },
+    );
+
+Default Business Hours setting is in $RT::ServiceBusinessHours{'Default'}.
+
+=head2 Defining service levels per queue
+
+In the config you can set per queue defaults, using:
+
+    %RT::ServiceAgreements = (
+        Default => 'global default level of service',
+        QueueDefault => {
+            'queue name' => 'default value for this queue',
+            ...
+        },
+        ...
+    };
+
+=head2 AssumeOutsideActor
+
+When using a L<Response|/"Resolve and Response (interval, no defaults)">
+configuration, the due date is unset when anyone who is not a requestor
+replies.  If it is common for non-requestors to reply to tickets, and
+this should I<not> satisfy the SLA, you may wish to set
+C<AssumeOutsideActor>.  This causes the extension to assume that the
+Response SLA has only been met when the owner or AdminCc reply.
+
+    %RT::ServiceAgreements = (
+        AssumeOutsideActor => 1,
+        ...
+    };
+
+=head2 Access control
+
+You can totally hide SLA custom field from users and use per queue
+defaults, just revoke SeeCustomField and ModifyCustomField.
+
+If you want people to see the current service level ticket is assigned
+to then grant SeeCustomField right.
+
+You may want to allow customers or managers to escalate thier tickets.
+Just grant them ModifyCustomField right.
+
+=cut
+
+sub BusinessHours {
+    my $self = shift;
+    my $name = shift || 'Default';
+
+    require Business::Hours;
+    my $res = new Business::Hours;
+    $res->business_hours( %{ $RT::ServiceBusinessHours{ $name } } )
+        if $RT::ServiceBusinessHours{ $name };
+    return $res;
+}
+
+sub Agreement {
+    my $self = shift;
+    my %args = (
+        Level => undef,
+        Type => 'Response',
+        Time => undef,
+        Ticket => undef,
+        Queue  => undef,
+        @_
+    );
+
+    my $meta = $RT::ServiceAgreements{'Levels'}{ $args{'Level'} };
+    return undef unless $meta;
+
+    if ( exists $meta->{'StartImmediately'} || !defined $meta->{'Starts'} ) {
+        $meta->{'Starts'} = {
+            delete $meta->{'StartImmediately'}
+                ? ( )
+                : ( BusinessMinutes => 0 )
+            ,
+        };
+    }
+
+    return undef unless $meta->{ $args{'Type'} };
+
+    my %res;
+    if ( ref $meta->{ $args{'Type'} } ) {
+        %res = %{ $meta->{ $args{'Type'} } };
+    } elsif ( $meta->{ $args{'Type'} } =~ /^\d+$/ ) {
+        %res = ( BusinessMinutes => $meta->{ $args{'Type'} } );
+    } else {
+        $RT::Logger->error("Levels of SLA should be either number or hash ref");
+        return undef;
+    }
+
+    if ( $args{'Ticket'} && $res{'IgnoreOnStatuses'} ) {
+        my $status = $args{'Ticket'}->Status;
+        return undef if grep $_ eq $status, @{$res{'IgnoreOnStatuses'}};
+    }
+
+    $res{'OutOfHours'} = $meta->{'OutOfHours'}{ $args{'Type'} };
+
+    $args{'Queue'} ||= $args{'Ticket'}->QueueObj if $args{'Ticket'};
+    if ( $args{'Queue'} && ref $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+        $res{'Timezone'} = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name }{'Timezone'};
+    }
+    $res{'Timezone'} ||= $meta->{'Timezone'} || $RT::Timezone;
+
+    $res{'BusinessHours'} = $meta->{'BusinessHours'};
+
+    return \%res;
+}
+
+sub Due {
+    my $self = shift;
+    return $self->CalculateTime( @_ );
+}
+
+sub Starts {
+    my $self = shift;
+    return $self->CalculateTime( @_, Type => 'Starts' );
+}
+
+sub CalculateTime {
+    my $self = shift;
+    my %args = (@_);
+    my $agreement = $args{'Agreement'} || $self->Agreement( @_ );
+    return undef unless $agreement and ref $agreement eq 'HASH';
+
+    my $res = $args{'Time'};
+
+    my $ok = eval {
+        local $ENV{'TZ'} = $ENV{'TZ'};
+        if ( $agreement->{'Timezone'} && $agreement->{'Timezone'} ne ($ENV{'TZ'}||'') ) {
+            $ENV{'TZ'} = $agreement->{'Timezone'};
+            require POSIX; POSIX::tzset();
+        }
+
+        my $bhours = $self->BusinessHours( $agreement->{'BusinessHours'} );
+
+        if ( $agreement->{'OutOfHours'} && $bhours->first_after( $res ) != $res ) {
+            foreach ( qw(RealMinutes BusinessMinutes) ) {
+                next unless my $mod = $agreement->{'OutOfHours'}{ $_ };
+                ($agreement->{ $_ } ||= 0) += $mod;
+            }
+        }
+
+        if (   $args{ Ticket }
+            && $agreement->{ IgnoreOnStatuses }
+            && $agreement->{ ExcludeTimeOnIgnoredStatuses } )
+        {
+            my $txns = RT::Transactions->new( RT->SystemUser );
+            $txns->LimitToTicket($args{Ticket}->id);
+            $txns->Limit(
+                FIELD => 'Field',
+                VALUE => 'Status',
+            );
+            my $date = RT::Date->new( RT->SystemUser );
+            $date->Set( Value => $args{ Time } );
+            $txns->Limit(
+                FIELD    => 'Created',
+                OPERATOR => '>=',
+                VALUE    => $date->ISO( Timezone => 'UTC' ),
+            );
+
+            my $last_time = $args{ Time };
+            while ( my $txn = $txns->Next ) {
+                if ( grep( { $txn->OldValue eq $_ } @{ $agreement->{ IgnoreOnStatuses } } ) ) {
+                    if ( !grep( { $txn->NewValue eq $_ } @{ $agreement->{ IgnoreOnStatuses } } ) ) {
+                        if ( defined $agreement->{ 'BusinessMinutes' } ) {
+
+                            # re-init $bhours to make sure we don't have a cached start/end,
+                            # so the time here is not outside the calculated business hours
+
+                            my $bhours = $self->BusinessHours( $agreement->{ 'BusinessHours' } );
+                            my $time = $bhours->between( $last_time, $txn->CreatedObj->Unix );
+                            if ( $time > 0 ) {
+                                $res = $bhours->add_seconds( $res, $time );
+                            }
+                        }
+                        else {
+                            my $time = $txn->CreatedObj->Unix - $last_time;
+                            $res += $time;
+                        }
+                        $last_time = $txn->CreatedObj->Unix;
+                    }
+                }
+                else {
+                    $last_time = $txn->CreatedObj->Unix;
+                }
+            }
+        }
+
+        if ( defined $agreement->{'BusinessMinutes'} ) {
+            if ( $agreement->{'BusinessMinutes'} ) {
+                $res = $bhours->add_seconds(
+                    $res, 60 * $agreement->{'BusinessMinutes'},
+                );
+            }
+            else {
+                $res = $bhours->first_after( $res );
+            }
+        }
+        $res += 60 * $agreement->{'RealMinutes'}
+            if defined $agreement->{'RealMinutes'};
+        1;
+    };
+
+    POSIX::tzset() if $agreement->{'Timezone'}
+        && $agreement->{'Timezone'} ne ($ENV{'TZ'}||'');
+    die $@ unless $ok;
+
+    return $res;
+}
+
+sub GetCustomField {
+    my $self = shift;
+    my %args = (Ticket => undef, CustomField => 'SLA', @_);
+    unless ( $args{'Ticket'} ) {
+        $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
+    }
+    unless ( $args{'Ticket'} ) {
+        return RT::CustomField->new( $RT::SystemUser );
+    }
+    my $cfs = $args{'Ticket'}->QueueObj->TicketCustomFields;
+    $cfs->Limit( FIELD => 'Name', VALUE => $args{'CustomField'}, CASESENSITIVE => 0 );
+    return $cfs->First || RT::CustomField->new( $RT::SystemUser );
+}
+
+sub GetDefaultServiceLevel {
+    my $self = shift;
+    my %args = (Ticket => undef, Queue => undef, @_);
+    unless ( $args{'Queue'} || $args{'Ticket'} ) {
+        $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
+    }
+    if ( !$args{'Queue'} && $args{'Ticket'} ) {
+        $args{'Queue'} = $args{'Ticket'}->QueueObj;
+    }
+    if ( $args{'Queue'} ) {
+        return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
+
+        if ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+            return $info unless ref $info;
+            return $info->{'Level'} || $RT::ServiceAgreements{'Default'};
+        }
+    }
+    return $RT::ServiceAgreements{'Default'};
+}
+
+RT::Base->_ImportOverlays();
+
+1;

commit 25a0ac4674fe9f20fab97c48df00066ea25830de
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Sun Aug 2 22:05:14 2015 +0800

    use SLA column in Tickets table instead

diff --git a/etc/initialdata b/etc/initialdata
index 21650a6..94a2a52 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -14,17 +14,6 @@
 @Groups = (
 );
 
- at CustomFields = (
-    {
-        Name        => 'SLA',
-        Queue       => 0,
-        Type        => 'SelectSingle',
-        Disabled    => 0,
-        Description => 'Service Level Agreement',
-        Values      => [ ],
-    },
-);
-
 @Queues = ({ Name              => 'General',
              Description       => 'The default queue',
              CorrespondAddress => "",
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index cc623e8..3ceae2e 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -270,6 +270,7 @@ CREATE TABLE Tickets (
         TimeEstimated           NUMBER(11,0) DEFAULT 0 NOT NULL,
         TimeWorked              NUMBER(11,0) DEFAULT 0 NOT NULL,
         Status                  VARCHAR2(64),           
+        SLA                     VARCHAR2(64),
         TimeLeft                NUMBER(11,0) DEFAULT 0 NOT NULL,
         Told                    DATE,
         Starts                  DATE,
diff --git a/etc/schema.Pg b/etc/schema.Pg
index cd526e2..81ebbf8 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -423,6 +423,7 @@ CREATE TABLE Tickets (
   TimeEstimated integer NOT NULL DEFAULT 0  ,
   TimeWorked integer NOT NULL DEFAULT 0  ,
   Status varchar(64) NULL  ,
+  SLA varchar(64) NULL  ,
   TimeLeft integer NOT NULL DEFAULT 0  ,
   Told TIMESTAMP NULL  ,
   Starts TIMESTAMP NULL  ,
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index 5b2bb0f..93c8909 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -300,6 +300,7 @@ CREATE TABLE Tickets (
   TimeEstimated integer NULL DEFAULT 0 ,
   TimeWorked integer NULL DEFAULT 0 ,
   Status varchar(64) collate NOCASE NULL  ,
+  SLA varchar(64) collate NOCASE NULL  ,
   TimeLeft integer NULL DEFAULT 0 ,
   Told DATETIME NULL  ,
   Starts DATETIME NULL  ,
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 032c4d4..93fd164 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -281,6 +281,7 @@ CREATE TABLE Tickets (
   TimeEstimated integer NOT NULL DEFAULT 0  ,
   TimeWorked integer NOT NULL DEFAULT 0  ,
   Status varchar(64) NULL  ,
+  SLA varchar(64) NULL  ,
   TimeLeft integer NOT NULL DEFAULT 0  ,
   Told DATETIME NULL  ,
   Starts DATETIME NULL  ,
diff --git a/etc/upgrade/4.3.7/content b/etc/upgrade/4.3.8/content
similarity index 89%
rename from etc/upgrade/4.3.7/content
rename to etc/upgrade/4.3.8/content
index 2c24760..492cabb 100644
--- a/etc/upgrade/4.3.7/content
+++ b/etc/upgrade/4.3.8/content
@@ -1,17 +1,6 @@
 use strict;
 use warnings;
 
-our @CustomFields = (
-    {
-        Name        => 'SLA',
-        Queue       => 0,
-        Type        => 'SelectSingle',
-        Disabled    => 0,
-        Description => 'Service Level Agreement',
-        Values      => [ ],
-    },
-);
-
 our @ScripConditions = (
     {  Name        => '[SLA] Require default', # loc
        Description => 'Detect a situation when we should set default service level' , # loc
diff --git a/etc/upgrade/4.3.8/schema.Oracle b/etc/upgrade/4.3.8/schema.Oracle
new file mode 100644
index 0000000..fb41770
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.Oracle
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD SLA VARCHAR2(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.Pg b/etc/upgrade/4.3.8/schema.Pg
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.Pg
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.SQLite b/etc/upgrade/4.3.8/schema.SQLite
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.SQLite
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/etc/upgrade/4.3.8/schema.mysql b/etc/upgrade/4.3.8/schema.mysql
new file mode 100644
index 0000000..21b2af6
--- /dev/null
+++ b/etc/upgrade/4.3.8/schema.mysql
@@ -0,0 +1 @@
+ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
diff --git a/lib/RT/Action/SLA_SetDefault.pm b/lib/RT/Action/SLA_SetDefault.pm
index 1f06a19..ea918a2 100644
--- a/lib/RT/Action/SLA_SetDefault.pm
+++ b/lib/RT/Action/SLA_SetDefault.pm
@@ -75,12 +75,6 @@ sub Prepare { return 1 }
 sub Commit {
     my $self = shift;
 
-    my $cf = $self->GetCustomField;
-    unless ( $cf->id ) {
-        $RT::Logger->warning("SLA scrip applied to a queue that has no SLA CF");
-        return 1;
-    }
-
     my $level = $self->GetDefaultServiceLevel;
     unless ( $level ) {
         $RT::Logger->info(
@@ -89,10 +83,7 @@ sub Commit {
         return 1;
     }
 
-    my ($status, $msg) = $self->TicketObj->AddCustomFieldValue(
-        Field => $cf->id,
-        Value => $level,
-    );
+    my ($status, $msg) = $self->TicketObj->SetSLA($level);
     unless ( $status ) {
         $RT::Logger->error("Couldn't set service level: $msg");
         return 0;
diff --git a/lib/RT/Action/SLA_SetDue.pm b/lib/RT/Action/SLA_SetDue.pm
index c790df8..75eaadd 100644
--- a/lib/RT/Action/SLA_SetDue.pm
+++ b/lib/RT/Action/SLA_SetDue.pm
@@ -62,7 +62,7 @@ Checks if the ticket has service level defined.
 sub Prepare {
     my $self = shift;
 
-    unless ( $self->TicketObj->FirstCustomFieldValue('SLA') ) {
+    unless ( $self->TicketObj->SLA ) {
         $RT::Logger->error('SLA::SetDue scrip has been applied to ticket #'
             . $self->TicketObj->id . ' that has no SLA defined');
         return 0;
@@ -82,7 +82,7 @@ sub Commit {
 
     my $ticket = $self->TicketObj;
     my $txn = $self->TransactionObj;
-    my $level = $ticket->FirstCustomFieldValue('SLA');
+    my $level = $ticket->SLA;
 
     my ($last_reply, $is_outside) = $self->LastEffectiveAct;
     $RT::Logger->debug(
diff --git a/lib/RT/Action/SLA_SetStarts.pm b/lib/RT/Action/SLA_SetStarts.pm
index 59b2fe1..f64b63f 100644
--- a/lib/RT/Action/SLA_SetStarts.pm
+++ b/lib/RT/Action/SLA_SetStarts.pm
@@ -74,7 +74,7 @@ sub Commit {
 
     my $ticket = $self->TicketObj;
 
-    my $level = $ticket->FirstCustomFieldValue('SLA');
+    my $level = $ticket->SLA;
     unless ( $level ) {
         $RT::Logger->debug('Ticket #'. $ticket->id .' has no service level defined, skip setting Starts');
         return 1;
diff --git a/lib/RT/Condition/SLA.pm b/lib/RT/Condition/SLA.pm
index b164fae..60a4a56 100644
--- a/lib/RT/Condition/SLA.pm
+++ b/lib/RT/Condition/SLA.pm
@@ -58,25 +58,4 @@ use base qw(RT::SLA RT::Condition);
 
 sub SLAIsApplied { return 1 }
 
-=head1 IsCustomFieldChange
-
-=cut
-
-sub IsCustomFieldChange {
-    my $self = shift;
-    my $cf_name = shift;
-
-    my $txn = $self->TransactionObj;
-    
-    return 0 unless $txn->Type eq 'CustomField';
-
-    my $cf = $self->GetCustomField( CustomField => $cf_name );
-    unless ( $cf->id ) {
-        $RT::Logger->debug("Custom field '$cf_name' is not applied to ticket #". $self->TicketObj->id);
-        return 0;
-    }
-    return 0 unless $cf->id == $txn->Field;
-    return 1;
-}
-
 1;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
index bc49970..bf9792b 100644
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -65,8 +65,7 @@ sub IsApplicable {
     return 0 unless $self->TransactionObj->Type eq 'Create';
     my $ticket = $self->TicketObj;
     return 0 unless lc($ticket->Type) eq 'ticket';
-    return 0 if $ticket->FirstCustomFieldValue('SLA');
-    return 0 unless $self->SLAIsApplied;
+    return 0 if $ticket->SLA;
     return 1;
 }
 
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
index d436c27..618bf0a 100644
--- a/lib/RT/Condition/SLA_RequireDueSet.pm
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -69,14 +69,16 @@ sub IsApplicable {
 
     my $type = $self->TransactionObj->Type;
     if ( $type eq 'Create' || $type eq 'Correspond' ) {
-        return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+        return 1 if $self->TicketObj->SLA;
         return 0;
     }
     elsif ( $type eq 'Status' || ($type eq 'Set' && $self->TransactionObj->Field eq 'Status') ) {
-        return 1 if $self->TicketObj->FirstCustomFieldValue('SLA');
+        return 1 if $self->TicketObj->SLA;
         return 0;
     }
-    return 1 if $self->IsCustomFieldChange('SLA');
+    elsif ( $type eq 'SLA' || ($type eq 'Set' && $self->TransactionObj->Field eq 'SLA') ) {
+        return 1;
+    }
     return 0;
 }
 
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
index 895ccaa..3959a2e 100644
--- a/lib/RT/Condition/SLA_RequireStartsSet.pm
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -65,7 +65,7 @@ Applies if Starts date is not set for the ticket.
 sub IsApplicable {
     my $self = shift;
     return 0 if $self->TicketObj->StartsObj->Unix > 0;
-    return 0 unless $self->TicketObj->FirstCustomFieldValue('SLA');
+    return 0 unless $self->TicketObj->SLA;
     return 1;
 }
 
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 193b2c2..14bfd8f 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -2140,6 +2140,7 @@ sub CreateTicket {
         Type => $ARGS{'Type'} || 'ticket',
         Queue => $ARGS{'Queue'},
         Owner => $ARGS{'Owner'},
+        SLA => $ARGS{'SLA'},
 
         # note: name change
         Requestor       => $ARGS{'Requestors'},
@@ -2861,6 +2862,7 @@ sub ProcessTicketBasics {
         Type
         Status
         Queue
+        SLA
     );
 
     # Canonicalize Queue and Owner to their IDs if they aren't numeric
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index 817e5e6..aa9ab4f 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -377,17 +377,6 @@ Response SLA has only been met when the owner or AdminCc reply.
         ...
     };
 
-=head2 Access control
-
-You can totally hide SLA custom field from users and use per queue
-defaults, just revoke SeeCustomField and ModifyCustomField.
-
-If you want people to see the current service level ticket is assigned
-to then grant SeeCustomField right.
-
-You may want to allow customers or managers to escalate thier tickets.
-Just grant them ModifyCustomField right.
-
 =cut
 
 sub BusinessHours {
@@ -556,20 +545,6 @@ sub CalculateTime {
     return $res;
 }
 
-sub GetCustomField {
-    my $self = shift;
-    my %args = (Ticket => undef, CustomField => 'SLA', @_);
-    unless ( $args{'Ticket'} ) {
-        $args{'Ticket'} = $self->TicketObj if $self->can('TicketObj');
-    }
-    unless ( $args{'Ticket'} ) {
-        return RT::CustomField->new( $RT::SystemUser );
-    }
-    my $cfs = $args{'Ticket'}->QueueObj->TicketCustomFields;
-    $cfs->Limit( FIELD => 'Name', VALUE => $args{'CustomField'}, CASESENSITIVE => 0 );
-    return $cfs->First || RT::CustomField->new( $RT::SystemUser );
-}
-
 sub GetDefaultServiceLevel {
     my $self = shift;
     my %args = (Ticket => undef, Queue => undef, @_);
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index 742c562..dd63eee 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -90,6 +90,7 @@ use RT::Transactions;
 use RT::Reminders;
 use RT::URI::fsck_com_rt;
 use RT::URI;
+use RT::SLA;
 use MIME::Entity;
 use Devel::GlobalDestruction;
 
@@ -235,6 +236,7 @@ sub Create {
         Starts             => undef,
         Started            => undef,
         Resolved           => undef,
+        SLA                => undef,
         MIMEObj            => undef,
         _RecordTransaction => 1,
         @_
@@ -387,7 +389,8 @@ sub Create {
         Starts          => $Starts->ISO,
         Started         => $Started->ISO,
         Resolved        => $Resolved->ISO,
-        Due             => $Due->ISO
+        Due             => $Due->ISO,
+        SLA             => $args{SLA},
     );
 
 # Parameters passed in during an import that we probably don't want to touch, otherwise
@@ -3461,6 +3464,8 @@ sub _CoreAccessible {
                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Status =>
                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
+        SLA =>
+                {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
         TimeLeft =>
                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Told =>
diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm
index c641cd2..47027eb 100644
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@ -104,6 +104,7 @@ __PACKAGE__->RegisterCustomFieldJoin(@$_) for
 
 our %FIELD_METADATA = (
     Status          => [ 'STRING', ], #loc_left_pair
+    SLA             => [ 'STRING', ], #loc_left_pair
     Queue           => [ 'ENUM' => 'Queue', ], #loc_left_pair
     Type            => [ 'ENUM', ], #loc_left_pair
     Creator         => [ 'ENUM' => 'User', ], #loc_left_pair
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Elements/SelectSLA
similarity index 53%
copy from share/html/Ticket/Elements/ShowBasics
copy to share/html/Elements/SelectSLA
index 546f581..c708ea5 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Elements/SelectSLA
@@ -45,53 +45,25 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<table>
-  <tr class="id">
-    <td class="label"><&|/l&>Id</&>:</td>
-    <td class="value"><%$Ticket->Id %></td>
-  </tr>
-  <tr class="status">
-    <td class="label"><&|/l&>Status</&>:</td>
-    <td class="value"><% loc($Ticket->Status) %></td>
-  </tr>
-% if ($Ticket->TimeEstimated) {
-  <tr class="time estimated">
-    <td class="label"><&|/l&>Estimated</&>:</td>
-    <td class="value"><& ShowTime, minutes => $Ticket->TimeEstimated &></td>
-  </tr>
+<select name="<%$Name%>" id="<%$Name%>">
+% if ($DefaultValue) {
+<option value=""<% !$Default ? qq[ selected="selected"] : '' |n %>><%$DefaultLabel |n%></option>
 % }
-% $m->callback( %ARGS, CallbackName => 'AfterTimeEstimated', TicketObj => $Ticket );
-% if ($Ticket->TimeWorked) {
-  <tr class="time worked">
-    <td class="label"><&|/l&>Worked</&>:</td>
-    <td class="value"><& ShowTime, minutes => $Ticket->TimeWorked &></td>
-  </tr>
+% for my $sla ( sort keys %{$RT::ServiceAgreements{'Levels'}} )  {
+<option <% $sla eq $Default ? qq[ selected="selected"] : '' |n %> value="<% $sla %>" ><% $sla %></option>
 % }
-% $m->callback( %ARGS, CallbackName => 'AfterTimeWorked', TicketObj => $Ticket );
-% if ($Ticket->TimeLeft) {
-  <tr class="time left">
-    <td class="label"><&|/l&>Left</&>:</td>
-    <td class="value"><& ShowTime, minutes => $Ticket->TimeLeft &></td>
-  </tr>
-% }
-  <tr class="priority">
-    <td class="label"><&|/l&>Priority</&>:</td>
-    <td class="value"><& ShowPriority, Ticket => $Ticket &></td>
-  </tr>
-%# This will check SeeQueue at the ticket role level, queue level, and global level
-% if ($Ticket->CurrentUserHasRight('SeeQueue')) {
-  <tr class="queue">
-    <td class="label"><&|/l&>Queue</&>:</td>
-    <td class="value"><& ShowQueue, Ticket => $Ticket, QueueObj => $Ticket->QueueObj &></td>
-  </tr>
-% }
-  <& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket, Grouping => 'Basics', Table => 0 &>
-% if ($UngroupedCFs) {
-  <& /Ticket/Elements/ShowCustomFields, Ticket => $Ticket, Grouping => '', Table => 0 &>
-% }
-% $m->callback( %ARGS, CallbackName => 'EndOfList', TicketObj => $Ticket );
-</table>
+</select>
+
+<%INIT>
+$Default ||= $DECODED_ARGS->{SLA} if $DefaultFromArgs;
+$Default ||= $TicketObj->SLA if $TicketObj;
+</%INIT>
 <%ARGS>
-$Ticket => undef
-$UngroupedCFs => 0
+$DefaultFromArgs => 1
+$TicketObj => undef
+$QueueObj => undef
+$Name => 'SLA'
+$Default => undef
+$DefaultValue => 1
+$DefaultLabel => '-'
 </%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 21d2e6b..fbb2b38 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -94,6 +94,15 @@
                     DefaultValue    => 0,
                     QueueObj        => $QueueObj,
                 },
+            },
+            {   name => 'SLA',
+                comp => '/Elements/SelectSLA',
+                args => {
+                    Name            => "SLA",
+                    Default         => $ARGS{SLA} || RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj),
+                    DefaultValue    => RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj) ? 0 : 1,
+                    QueueObj        => $QueueObj,
+                },
             }
         ]
         &>
diff --git a/share/html/Ticket/Elements/EditBasics b/share/html/Ticket/Elements/EditBasics
index cfb446a..ae5e121 100644
--- a/share/html/Ticket/Elements/EditBasics
+++ b/share/html/Ticket/Elements/EditBasics
@@ -85,6 +85,15 @@ unless ( @fields ) {
                 DefaultValue => 0,
             }
         },
+        {   name => 'SLA',
+            comp => '/Elements/SelectSLA',
+            args => {
+                Name => 'SLA',
+                Default => $defaults{SLA},
+                DefaultFromArgs => 0,
+                TicketObj => $TicketObj,
+            },
+        },
         # Time Estimated, Worked, and Left
         (
             map {
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Ticket/Elements/ShowBasics
index 546f581..6690cff 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Ticket/Elements/ShowBasics
@@ -54,6 +54,10 @@
     <td class="label"><&|/l&>Status</&>:</td>
     <td class="value"><% loc($Ticket->Status) %></td>
   </tr>
+  <tr class="sla">
+    <td class="label"><&|/l&>SLA</&>:</td>
+    <td class="value"><% loc($Ticket->SLA) %></td>
+  </tr>
 % if ($Ticket->TimeEstimated) {
   <tr class="time estimated">
     <td class="label"><&|/l&>Estimated</&>:</td>

commit 6e301495892e8ec0b6fb1f1a395ee13bf26cf925
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 00:37:55 2015 +0800

    allow to disable SLA for some queues

diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 3ceae2e..b8d9093 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -33,6 +33,7 @@ CREATE TABLE Queues (
         Created                 DATE,
         LastUpdatedBy           NUMBER(11,0) DEFAULT 0 NOT NULL,
         LastUpdated             DATE,
+        SLADisabled             NUMBER(11,0) DEFAULT 0 NOT NULL,
         Disabled                NUMBER(11,0) DEFAULT 0 NOT NULL
 );
 CREATE UNIQUE INDEX Queues1 ON Queues (LOWER(Name));
diff --git a/etc/schema.Pg b/etc/schema.Pg
index 81ebbf8..3f7faeb 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -56,6 +56,7 @@ CREATE TABLE Queues (
   Created TIMESTAMP NULL  ,
   LastUpdatedBy integer NOT NULL DEFAULT 0  ,
   LastUpdated TIMESTAMP NULL  ,
+  SLADisabled integer NOT NULL DEFAULT 0 ,
   Disabled integer NOT NULL DEFAULT 0 ,
   PRIMARY KEY (id)
 
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index 93c8909..d5ce0ca 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -34,6 +34,7 @@ CREATE TABLE Queues (
   Created DATETIME NULL  ,
   LastUpdatedBy integer NULL DEFAULT 0 ,
   LastUpdated DATETIME NULL  ,
+  SLADisabled int2 NOT NULL DEFAULT 0,
   Disabled int2 NOT NULL DEFAULT 0 
  
 ) ;
diff --git a/etc/schema.mysql b/etc/schema.mysql
index 93fd164..616676d 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -30,6 +30,7 @@ CREATE TABLE Queues (
   Created DATETIME NULL  ,
   LastUpdatedBy integer NOT NULL DEFAULT 0  ,
   LastUpdated DATETIME NULL  ,
+  SLADisabled int2 NOT NULL DEFAULT 0 ,
   Disabled int2 NOT NULL DEFAULT 0 ,
   PRIMARY KEY (id)
 ) ENGINE=InnoDB CHARACTER SET utf8;
diff --git a/etc/upgrade/4.3.8/schema.Oracle b/etc/upgrade/4.3.8/schema.Oracle
index fb41770..1e78116 100644
--- a/etc/upgrade/4.3.8/schema.Oracle
+++ b/etc/upgrade/4.3.8/schema.Oracle
@@ -1 +1,2 @@
 ALTER TABLE Tickets ADD SLA VARCHAR2(64) NULL;
+ALTER TABLE Queues ADD SLADisabled NUMBER(11,0) DEFAULT 0 NOT NULL;
diff --git a/etc/upgrade/4.3.8/schema.Pg b/etc/upgrade/4.3.8/schema.Pg
index 21b2af6..7df4453 100644
--- a/etc/upgrade/4.3.8/schema.Pg
+++ b/etc/upgrade/4.3.8/schema.Pg
@@ -1 +1,2 @@
 ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled integer NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.3.8/schema.SQLite b/etc/upgrade/4.3.8/schema.SQLite
index 21b2af6..7d89190 100644
--- a/etc/upgrade/4.3.8/schema.SQLite
+++ b/etc/upgrade/4.3.8/schema.SQLite
@@ -1 +1,2 @@
 ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled int2 NOT NULL DEFAULT 0;
diff --git a/etc/upgrade/4.3.8/schema.mysql b/etc/upgrade/4.3.8/schema.mysql
index 21b2af6..7d89190 100644
--- a/etc/upgrade/4.3.8/schema.mysql
+++ b/etc/upgrade/4.3.8/schema.mysql
@@ -1 +1,2 @@
 ALTER TABLE Tickets ADD COLUMN SLA VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SLADisabled int2 NOT NULL DEFAULT 0;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
index bf9792b..5513c2b 100644
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ b/lib/RT/Condition/SLA_RequireDefault.pm
@@ -65,6 +65,7 @@ sub IsApplicable {
     return 0 unless $self->TransactionObj->Type eq 'Create';
     my $ticket = $self->TicketObj;
     return 0 unless lc($ticket->Type) eq 'ticket';
+    return 0 if $ticket->QueueObj->SLADisabled;
     return 0 if $ticket->SLA;
     return 1;
 }
diff --git a/lib/RT/Condition/SLA_RequireDueSet.pm b/lib/RT/Condition/SLA_RequireDueSet.pm
index 618bf0a..77583c8 100644
--- a/lib/RT/Condition/SLA_RequireDueSet.pm
+++ b/lib/RT/Condition/SLA_RequireDueSet.pm
@@ -66,6 +66,7 @@ a ticket and it has service level value or when we set service level.
 sub IsApplicable {
     my $self = shift;
     return 0 unless $self->SLAIsApplied;
+    return 0 if $self->TicketObj->QueueObj->SLADisabled;
 
     my $type = $self->TransactionObj->Type;
     if ( $type eq 'Create' || $type eq 'Correspond' ) {
diff --git a/lib/RT/Condition/SLA_RequireStartsSet.pm b/lib/RT/Condition/SLA_RequireStartsSet.pm
index 3959a2e..883f45a 100644
--- a/lib/RT/Condition/SLA_RequireStartsSet.pm
+++ b/lib/RT/Condition/SLA_RequireStartsSet.pm
@@ -65,6 +65,7 @@ Applies if Starts date is not set for the ticket.
 sub IsApplicable {
     my $self = shift;
     return 0 if $self->TicketObj->StartsObj->Unix > 0;
+    return 0 if $self->TicketObj->QueueObj->SLADisabled;
     return 0 unless $self->TicketObj->SLA;
     return 1;
 }
diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm
index ba9d023..d4064c2 100644
--- a/lib/RT/Queue.pm
+++ b/lib/RT/Queue.pm
@@ -1014,6 +1014,8 @@ sub _CoreAccessible {
         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         LastUpdated => 
         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+        SLADisabled => 
+        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
         Disabled => 
         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
 
diff --git a/share/html/Admin/Queues/Modify.html b/share/html/Admin/Queues/Modify.html
index 8545744..5a483be 100644
--- a/share/html/Admin/Queues/Modify.html
+++ b/share/html/Admin/Queues/Modify.html
@@ -92,6 +92,15 @@
 <br /><span><em><&|/l , RT->Config->Get('CommentAddress')&>(If left blank, will default to [_1])</&></em></span></td>
 </tr>
 
+<tr><td align="right"><input type="checkbox" class="checkbox" id="SLAEnabled" name="SLAEnabled" value="1" 
+% if ( $Create || !$QueueObj->SLADisabled ) {
+checked="checked"
+% }
+ /></td>
+<td colspan="3"><label for="SLAEnabled"><&|/l&>SLA Enabled (Unchecking this box disables SLA for this queue)</&></label><br />
+<input type="hidden" class="hidden" name="SetSLAEnabled" value="1" />
+</td></tr>
+
 % my $CFs = $QueueObj->CustomFields;
 % while (my $CF = $CFs->Next) {
 <tr valign="top"><td align="right">
@@ -176,12 +185,15 @@ unless ($Create) {
 if ( $QueueObj->Id ) {
     $title = loc('Configuration for queue [_1]', $QueueObj->Name );
     my @attribs= qw(Description CorrespondAddress CommentAddress Name
-        Sign SignAuto Encrypt Lifecycle SubjectTag Disabled);
+        Sign SignAuto Encrypt Lifecycle SubjectTag SLADisabled Disabled);
 
     # we're asking about enabled on the web page but really care about disabled
     if ( $SetEnabled ) {
         $Disabled = $ARGS{'Disabled'} = $Enabled? 0: 1;
     }
+    if ( $SetSLAEnabled ) {
+        $ARGS{'SLADisabled'} = $SLAEnabled? 0: 1;
+    }
     if ( $SetCrypt ) {
         $ARGS{$_} = 0 foreach grep !defined $ARGS{$_} || !length $ARGS{$_},
             qw(Sign SignAuto Encrypt);
@@ -244,7 +256,9 @@ $Create => undef
 $Description => undef
 $CorrespondAddress => undef
 $CommentAddress => undef
+$SetSLAEnabled => undef
 $SetEnabled => undef
 $SetCrypt => undef
+$SLAEnabled => undef
 $Enabled => undef
 </%ARGS>
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index fbb2b38..8dba39f 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -95,6 +95,7 @@
                     QueueObj        => $QueueObj,
                 },
             },
+            $QueueObj->SLADisabled ? () : (
             {   name => 'SLA',
                 comp => '/Elements/SelectSLA',
                 args => {
@@ -103,7 +104,7 @@
                     DefaultValue    => RT::SLA->GetDefaultServiceLevel(Queue => $QueueObj) ? 0 : 1,
                     QueueObj        => $QueueObj,
                 },
-            }
+            }),
         ]
         &>
 
diff --git a/share/html/Ticket/Elements/EditBasics b/share/html/Ticket/Elements/EditBasics
index ae5e121..7b5708b 100644
--- a/share/html/Ticket/Elements/EditBasics
+++ b/share/html/Ticket/Elements/EditBasics
@@ -85,6 +85,7 @@ unless ( @fields ) {
                 DefaultValue => 0,
             }
         },
+        $TicketObj->QueueObj->SLADisabled ? () : (
         {   name => 'SLA',
             comp => '/Elements/SelectSLA',
             args => {
@@ -93,7 +94,7 @@ unless ( @fields ) {
                 DefaultFromArgs => 0,
                 TicketObj => $TicketObj,
             },
-        },
+        }),
         # Time Estimated, Worked, and Left
         (
             map {
diff --git a/share/html/Ticket/Elements/ShowBasics b/share/html/Ticket/Elements/ShowBasics
index 6690cff..3d2c652 100644
--- a/share/html/Ticket/Elements/ShowBasics
+++ b/share/html/Ticket/Elements/ShowBasics
@@ -54,10 +54,12 @@
     <td class="label"><&|/l&>Status</&>:</td>
     <td class="value"><% loc($Ticket->Status) %></td>
   </tr>
+% if ( !$Ticket->QueueObj->SLADisabled ) {
   <tr class="sla">
     <td class="label"><&|/l&>SLA</&>:</td>
     <td class="value"><% loc($Ticket->SLA) %></td>
   </tr>
+% }
 % if ($Ticket->TimeEstimated) {
   <tr class="time estimated">
     <td class="label"><&|/l&>Estimated</&>:</td>

commit f1cd8c158a9fc958c95ea0ca8bf12cf34631ebe8
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 00:59:22 2015 +0800

    update doc/config and move it to RT_Config.pm

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 5deaade..76585a6 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2918,9 +2918,341 @@ Set(%Lifecycles,
     },
 );
 
+=head1 SLA
 
+=over 4
+
+=item C<%ServiceAgreements>
+
+There is no WebUI in the current version. Almost everything is controlled in
+the C<%ServiceAgreements> and C<%ServiceBusinessHours>. For example:
+
+    Set( %ServiceAgreements = (
+        Default => '4h',
+        QueueDefault => {
+            'Incident' => '2h',
+        },
+        Levels => {
+            '2h' => { Resolve => { RealMinutes => 60*2 } },
+            '4h' => { Resolve => { RealMinutes => 60*4 } },
+        },
+    ));
+
+In this example I<Incident> is the name of the queue, and I<2h> is the name of
+the SLA which will be applied to this queue by default.
+
+Each service level can be described using several options:
+L<Starts|/"Starts (interval, first business minute)">,
+L<Resolve|/"Resolve and Response (interval, no defaults)">,
+L<Response|/"Resolve and Response (interval, no defaults)">,
+L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
+L<OutOfHours|/"OutOfHours (struct, no default)">
+and L<ServiceBusinessHours|/"Configuring business hours">.
+
+=over 4
+
+=item Starts (interval, first business minute)
+
+By default when a ticket is created Starts date is set to
+first business minute after time of creation. In other
+words if a ticket is created during business hours then
+Starts will be equal to Created time, otherwise Starts will
+be beginning of the next business day.
+
+However, if you provide 24/7 support then you most
+probably would be interested in Starts to be always equal
+to Created time.
+
+Starts option can be used to adjust behaviour. Format
+of the option is the same as format for deadlines which
+described later in details. RealMinutes, BusinessMinutes
+options and OutOfHours modifiers can be used here like
+for any other deadline. For example:
+
+    'standard' => {
+        # give people 15 minutes
+        Starts   => { BusinessMinutes => 15  },
+    },
+
+You can still use old option StartImmediately to set
+Starts date equal to Created date.
+
+Example:
+
+    '24/7' => {
+        StartImmediately => 1,
+        Response => { RealMinutes => 30 },
+    },
+
+But it's the same as:
+
+    '24/7' => {
+        Starts => { RealMinutes => 0 },
+        Response => { RealMinutes => 30 },
+    },
+
+=item Resolve and Response (interval, no defaults)
+
+These two options define deadlines for resolve of a ticket
+and reply to customer(requestors) questions accordingly.
+
+You can define them using real time, business or both. Read more
+about the latter L<below|/"Using both Resolve and Response in the same level">.
+
+The Due date field is used to store calculated deadlines.
+
+=over 4
+
+=item Resolve
+
+Defines deadline when a ticket should be resolved. This option is
+quite simple and straightforward when used without L</Response>.
+
+Example:
+
+    # 8 business hours
+    'simple' => { Resolve => 60*8 },
+    ...
+    # one real week
+    'hard' => { Resolve => { RealMinutes => 60*24*7 } },
+
+=item Response
+
+In many companies providing support service(s) resolve time of a ticket
+is less important than time of response to requestors from staff
+members.
+
+You can use Response option to define such deadlines.  The Due date is
+set when a ticket is created, unset when a worker replies, and re-set
+when the requestor replies again -- until the ticket is closed, when the
+ticket's Due date is unset.
+
+B<NOTE> that this behaviour changes when Resolve and Response options
+are combined; see L</"Using both Resolve and Response in the same
+level">.
+
+Note that by default, only the requestors on the ticket are considered
+"outside actors" and thus require a Response due date; all other email
+addresses are treated as workers of the ticket, and thus count as
+meeting the SLA.  If you'd like to invert this logic, so that the Owner
+and AdminCcs are the only worker email addresses, and all others are
+external, see the L</AssumeOutsideActor> configuration.
+
+The owner is never treated as an outside actor; if they are also the
+requestor of the ticket, it will have no SLA.
+
+If an outside actor replies multiple times, their later replies are
+ignored; the deadline is awlways calculated from the oldest
+correspondence from the outside actor.
+
+
+=item Using both Resolve and Response in the same level
+
+Resolve and Response can be combined. In such case due date is set
+according to the earliest of two deadlines and never is dropped to
+'not set'.
+
+If a ticket met its Resolve deadline then due date stops "flipping",
+is freezed and the ticket becomes overdue. Before that moment when
+an inside actor replies to a ticket, due date is changed to Resolve
+deadline instead of 'Not Set', as well this happens when a ticket
+is closed. So all the time due date is defined.
+
+Example:
+
+    'standard delivery' => {
+        Response => { RealMinutes => 60*1  }, # one hour
+        Resolve  => { RealMinutes => 60*24 }, # 24 real hours
+    },
+
+A client orders goods and due date of the order is set to the next one
+hour, you have this hour to process the order and write a reply.
+As soon as goods are delivered you resolve tickets and usually meet
+Resolve deadline, but if you don't resolve or user replies then most
+probably there are problems with delivery of the goods. And if after
+a week you keep replying to the client and always meeting one hour
+response deadline that doesn't mean the ticket is not over due.
+Due date was frozen 24 hours after creation of the order.
+
+=item Using business and real time in one option
+
+It's quite rare situation when people need it, but we've decided
+that business is applied first and then real time when deadline
+described using both types of time. For example:
+
+    'delivery' => {
+        Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
+    },
+    'fast delivery' {
+        StartImmediately => 1,
+        Resolve => { RealMinutes => 60*8 },
+    },
+
+For delivery requests which come into the system during business
+hours these levels define the same deadlines, otherwise the first
+level set deadline to 8 real hours starting from the next business
+day, when tickets with the second level should be resolved in the
+next 8 hours after creation.
+
+=back
+
+=item Keep in loop (interval, no defaults)
+
+If response deadline is used then Due date is changed to repsonse
+deadline or to "Not Set" when staff replies to a ticket. In some
+cases you want to keep requestors in loop and keed them up to date
+every few hours. KeepInLoop option can be used to achieve this.
+
+    'incident' => {
+        Response   => { RealMinutes => 60*1  }, # one hour
+        KeepInLoop => { RealMinutes => 60*2 }, # two hours
+        Resolve    => { RealMinutes => 60*24 }, # 24 real hours
+    },
 
+In the above example Due is set to one hour after creation, reply
+of a inside actor moves Due date two hours forward, outside actors'
+replies move Due date to one hour and resolve deadine is 24 hours.
 
+=item Modifying Agreements
+
+=over 4
+
+=item OutOfHours (struct, no default)
+
+Out of hours modifier. Adds more real or business minutes to resolve
+and/or reply options if event happens out of business hours, read also
+</"Configuring business hours"> below.
+
+Example:
+
+    'level x' => {
+        OutOfHours => { Resolve => { RealMinutes => +60*24 } },
+        Resolve    => { RealMinutes => 60*24 },
+    },
+
+If a request comes into the system during night then supporters have two
+hours, otherwise only one.
+
+    'level x' => {
+        OutOfHours => { Response => { BusinessMinutes => +60*2 } },
+        Resolve    => { BusinessMinutes => 60 },
+    },
+
+Supporters have two additional hours in the morning to deal with bunch
+of requests that came into the system during the last night.
+
+=item IgnoreOnStatuses (array, no default)
+
+Allows you to ignore a deadline when ticket has certain status. Example:
+
+    'level x' => {
+        KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
+    },
+
+In above example KeepInLoop deadline is ignored if ticket is stalled.
+
+B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
+Due date is calculated from the last action (reply, SLA change, etc) which fits
+the SLA type (Response, Starts, KeepInLoop, etc).  This means if a ticket in
+the above example flips from stalled to open without a reply, the ticket will
+probably be overdue.  In most cases this shouldn't be a problem since moving
+out of stalled-like statuses is often the result of RT's auto-open on reply
+scrip, therefore ensuring there's a new reply to calculate Due from.  The
+overall effect is that ignored statuses don't let the Due date drift
+arbitrarily, which could wreak havoc on your SLA performance.
+C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
+overdue" issue by excluding the time spent on ignored statuses, e.g.
+
+    'level x' => {
+        KeepInLoop => {
+            BusinessMinutes => 60,
+            ExcludeTimeOnIgnoredStatuses => 1,
+            IgnoreOnStatuses => ['stalled'],
+        },
+    },
+
+=back
+
+=item Defining service levels per queue
+
+In the config you can set per queue defaults, using:
+
+    Set( %ServiceAgreements, (
+        Default => 'global default level of service',
+        QueueDefault => {
+            'queue name' => 'default value for this queue',
+            ...
+        },
+        ...
+    ));
+
+=item AssumeOutsideActor
+
+When using a L<Response|/"Resolve and Response (interval, no defaults)">
+configuration, the due date is unset when anyone who is not a requestor
+replies.  If it is common for non-requestors to reply to tickets, and
+this should I<not> satisfy the SLA, you may wish to set
+C<AssumeOutsideActor>.  This causes the extension to assume that the
+Response SLA has only been met when the owner or AdminCc reply.
+
+    Set ( %ServiceAgreements = (
+        AssumeOutsideActor => 1,
+        ...
+    );
+
+=back
+
+=cut
+
+Set( %ServiceAgreements, );
+
+=item C<%ServiceBusinessHours>
+
+In the config you can set one or more work schedules, e.g.
+
+    Set( %ServiceBusinessHours, (
+        'Default' => {
+            ... description ...
+        },
+        'Support' => {
+            ... description ...
+        },
+        'Sales' => {
+            ... description ...
+        },
+    );
+
+Read more about how to describe a schedule in L<Business::Hours>.
+
+=over 4
+
+=item Defining different business hours for service levels
+
+Each level supports BusinessHours option to specify your own business
+hours.
+
+    'level x' => {
+        BusinessHours => 'work just in Monday',
+        Resolve    => { BusinessMinutes => 60 },
+    },
+
+then L<%ServiceBusinessHours> should have the corresponding definition:
+
+    Set( %ServiceBusinessHours, (
+        'work just in Monday' => {
+            1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
+        },
+    );
+
+Default Business Hours setting is in $ServiceBusinessHours{'Default'}.
+
+=back
+
+=cut
+
+Set( %ServiceBusinessHours, );
+
+=back
 
 =head1 Administrative interface
 
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index aa9ab4f..e9a7abf 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -58,325 +58,6 @@ RT::SLA - Service Level Agreements for RT
 
 Automated due dates using service levels.
 
-=head1 CONFIGURATION
-
-Service level agreements of tickets is controlled by an SLA custom field (CF).
-This field is created during C<make initdb> step (above) and applied globally.
-This CF MUST be of C<select one value> type. Values of the CF define the
-service levels.
-
-It's possible to define different set of levels for different
-queues. You can create several CFs with the same name and
-different set of values. But if you move tickets between
-queues a lot then it's going to be a problem and it's preferred
-to use B<ONE> SLA custom field.
-
-There is no WebUI in the current version. Almost everything is
-controlled in the RT's config using option C<%RT::ServiceAgreements>
-and C<%RT::ServiceBusinessHours>. For example:
-
-    %RT::ServiceAgreements = (
-        Default => '4h',
-        QueueDefault => {
-            'Incident' => '2h',
-        },
-        Levels => {
-            '2h' => { Resolve => { RealMinutes => 60*2 } },
-            '4h' => { Resolve => { RealMinutes => 60*4 } },
-        },
-    );
-
-In this example I<Incident> is the name of the queue, and I<2h> is the name of
-the SLA which will be applied to this queue by default.
-
-Each service level can be described using several options:
-L<Starts|/"Starts (interval, first business minute)">,
-L<Resolve|/"Resolve and Response (interval, no defaults)">,
-L<Response|/"Resolve and Response (interval, no defaults)">,
-L<KeepInLoop|/"Keep in loop (interval, no defaults)">,
-L<OutOfHours|/"OutOfHours (struct, no default)">
-and L<ServiceBusinessHours|/"Configuring business hours">.
-
-=head2 Starts (interval, first business minute)
-
-By default when a ticket is created Starts date is set to
-first business minute after time of creation. In other
-words if a ticket is created during business hours then
-Starts will be equal to Created time, otherwise Starts will
-be beginning of the next business day.
-
-However, if you provide 24/7 support then you most
-probably would be interested in Starts to be always equal
-to Created time.
-
-Starts option can be used to adjust behaviour. Format
-of the option is the same as format for deadlines which
-described later in details. RealMinutes, BusinessMinutes
-options and OutOfHours modifiers can be used here like
-for any other deadline. For example:
-
-    'standard' => {
-        # give people 15 minutes
-        Starts   => { BusinessMinutes => 15  },
-    },
-
-You can still use old option StartImmediately to set
-Starts date equal to Created date.
-
-Example:
-
-    '24/7' => {
-        StartImmediately => 1,
-        Response => { RealMinutes => 30 },
-    },
-
-But it's the same as:
-
-    '24/7' => {
-        Starts => { RealMinutes => 0 },
-        Response => { RealMinutes => 30 },
-    },
-
-=head2 Resolve and Response (interval, no defaults)
-
-These two options define deadlines for resolve of a ticket
-and reply to customer(requestors) questions accordingly.
-
-You can define them using real time, business or both. Read more
-about the latter L<below|/"Using both Resolve and Response in the same level">.
-
-The Due date field is used to store calculated deadlines.
-
-=head3 Resolve
-
-Defines deadline when a ticket should be resolved. This option is
-quite simple and straightforward when used without L</Response>.
-
-Example:
-
-    # 8 business hours
-    'simple' => { Resolve => 60*8 },
-    ...
-    # one real week
-    'hard' => { Resolve => { RealMinutes => 60*24*7 } },
-
-=head3 Response
-
-In many companies providing support service(s) resolve time of a ticket
-is less important than time of response to requestors from staff
-members.
-
-You can use Response option to define such deadlines.  The Due date is
-set when a ticket is created, unset when a worker replies, and re-set
-when the requestor replies again -- until the ticket is closed, when the
-ticket's Due date is unset.
-
-B<NOTE> that this behaviour changes when Resolve and Response options
-are combined; see L</"Using both Resolve and Response in the same
-level">.
-
-Note that by default, only the requestors on the ticket are considered
-"outside actors" and thus require a Response due date; all other email
-addresses are treated as workers of the ticket, and thus count as
-meeting the SLA.  If you'd like to invert this logic, so that the Owner
-and AdminCcs are the only worker email addresses, and all others are
-external, see the L</AssumeOutsideActor> configuration.
-
-The owner is never treated as an outside actor; if they are also the
-requestor of the ticket, it will have no SLA.
-
-If an outside actor replies multiple times, their later replies are
-ignored; the deadline is awlways calculated from the oldest
-correspondence from the outside actor.
-
-
-=head3 Using both Resolve and Response in the same level
-
-Resolve and Response can be combined. In such case due date is set
-according to the earliest of two deadlines and never is dropped to
-'not set'.
-
-If a ticket met its Resolve deadline then due date stops "flipping",
-is freezed and the ticket becomes overdue. Before that moment when
-an inside actor replies to a ticket, due date is changed to Resolve
-deadline instead of 'Not Set', as well this happens when a ticket
-is closed. So all the time due date is defined.
-
-Example:
-
-    'standard delivery' => {
-        Response => { RealMinutes => 60*1  }, # one hour
-        Resolve  => { RealMinutes => 60*24 }, # 24 real hours
-    },
-
-A client orders goods and due date of the order is set to the next one
-hour, you have this hour to process the order and write a reply.
-As soon as goods are delivered you resolve tickets and usually meet
-Resolve deadline, but if you don't resolve or user replies then most
-probably there are problems with delivery of the goods. And if after
-a week you keep replying to the client and always meeting one hour
-response deadline that doesn't mean the ticket is not over due.
-Due date was frozen 24 hours after creation of the order.
-
-=head3 Using business and real time in one option
-
-It's quite rare situation when people need it, but we've decided
-that business is applied first and then real time when deadline
-described using both types of time. For example:
-
-    'delivery' => {
-        Resolve => { BusinessMinutes => 0, RealMinutes => 60*8 },
-    },
-    'fast delivery' {
-        StartImmediately => 1,
-        Resolve => { RealMinutes => 60*8 },
-    },
-
-For delivery requests which come into the system during business
-hours these levels define the same deadlines, otherwise the first
-level set deadline to 8 real hours starting from the next business
-day, when tickets with the second level should be resolved in the
-next 8 hours after creation.
-
-=head2 Keep in loop (interval, no defaults)
-
-If response deadline is used then Due date is changed to repsonse
-deadline or to "Not Set" when staff replies to a ticket. In some
-cases you want to keep requestors in loop and keed them up to date
-every few hours. KeepInLoop option can be used to achieve this.
-
-    'incident' => {
-        Response   => { RealMinutes => 60*1  }, # one hour
-        KeepInLoop => { RealMinutes => 60*2 }, # two hours
-        Resolve    => { RealMinutes => 60*24 }, # 24 real hours
-    },
-
-In the above example Due is set to one hour after creation, reply
-of a inside actor moves Due date two hours forward, outside actors'
-replies move Due date to one hour and resolve deadine is 24 hours.
-
-=head2 Modifying Agreements
-
-=head3 OutOfHours (struct, no default)
-
-Out of hours modifier. Adds more real or business minutes to resolve
-and/or reply options if event happens out of business hours, read also
-</"Configuring business hours"> below.
-
-Example:
-
-    'level x' => {
-        OutOfHours => { Resolve => { RealMinutes => +60*24 } },
-        Resolve    => { RealMinutes => 60*24 },
-    },
-
-If a request comes into the system during night then supporters have two
-hours, otherwise only one.
-
-    'level x' => {
-        OutOfHours => { Response => { BusinessMinutes => +60*2 } },
-        Resolve    => { BusinessMinutes => 60 },
-    },
-
-Supporters have two additional hours in the morning to deal with bunch
-of requests that came into the system during the last night.
-
-=head3 IgnoreOnStatuses (array, no default)
-
-Allows you to ignore a deadline when ticket has certain status. Example:
-
-    'level x' => {
-        KeepInLoop => { BusinessMinutes => 60, IgnoreOnStatuses => ['stalled'] },
-    },
-
-In above example KeepInLoop deadline is ignored if ticket is stalled.
-
-B<NOTE>: When a ticket goes from an ignored status to a normal status, the new
-Due date is calculated from the last action (reply, SLA change, etc) which fits
-the SLA type (Response, Starts, KeepInLoop, etc).  This means if a ticket in
-the above example flips from stalled to open without a reply, the ticket will
-probably be overdue.  In most cases this shouldn't be a problem since moving
-out of stalled-like statuses is often the result of RT's auto-open on reply
-scrip, therefore ensuring there's a new reply to calculate Due from.  The
-overall effect is that ignored statuses don't let the Due date drift
-arbitrarily, which could wreak havoc on your SLA performance.
-C<ExcludeTimeOnIgnoredStatuses> option could get around the "probably be
-overdue" issue by excluding the time spent on ignored statuses.
-
-        'level x' => {
-            KeepInLoop => {
-                BusinessMinutes => 60,
-                ExcludeTimeOnIgnoredStatuses => 1,
-                IgnoreOnStatuses => ['stalled'],
-            },
-        },
-
-=head2 Configuring business hours
-
-In the config you can set one or more work schedules. Use the following
-format:
-
-    %RT::ServiceBusinessHours = (
-        'Default' => {
-            ... description ...
-        },
-        'Support' => {
-            ... description ...
-        },
-        'Sales' => {
-            ... description ...
-        },
-    );
-
-Read more about how to describe a schedule in L<Business::Hours>.
-
-=head3 Defining different business hours for service levels
-
-Each level supports BusinessHours option to specify your own business
-hours.
-
-    'level x' => {
-        BusinessHours => 'work just in Monday',
-        Resolve    => { BusinessMinutes => 60 },
-    },
-
-then %RT::ServiceBusinessHours should have the corresponding definition:
-
-    %RT::ServiceBusinessHours = (
-        'work just in Monday' => {
-            1 => { Name => 'Monday', Start => '9:00', End => '18:00' },
-        },
-    );
-
-Default Business Hours setting is in $RT::ServiceBusinessHours{'Default'}.
-
-=head2 Defining service levels per queue
-
-In the config you can set per queue defaults, using:
-
-    %RT::ServiceAgreements = (
-        Default => 'global default level of service',
-        QueueDefault => {
-            'queue name' => 'default value for this queue',
-            ...
-        },
-        ...
-    };
-
-=head2 AssumeOutsideActor
-
-When using a L<Response|/"Resolve and Response (interval, no defaults)">
-configuration, the due date is unset when anyone who is not a requestor
-replies.  If it is common for non-requestors to reply to tickets, and
-this should I<not> satisfy the SLA, you may wish to set
-C<AssumeOutsideActor>.  This causes the extension to assume that the
-Response SLA has only been met when the owner or AdminCc reply.
-
-    %RT::ServiceAgreements = (
-        AssumeOutsideActor => 1,
-        ...
-    };
-
 =cut
 
 sub BusinessHours {

commit 8be9a47d80869d91629b37a25d7c3009d9d3b94e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 01:52:58 2015 +0800

    upgrade-sla script to migrate SLA values from custom fields

diff --git a/.gitignore b/.gitignore
index 2156779..54bde7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
 /etc/upgrade/switch-templates-to
 /etc/upgrade/time-worked-history
 /etc/upgrade/upgrade-articles
+/etc/upgrade/upgrade-sla
 /etc/upgrade/vulnerable-passwords
 /lib/RT/Generated.pm
 /Makefile
diff --git a/configure.ac b/configure.ac
index 9c57eda..064274e 100755
--- a/configure.ac
+++ b/configure.ac
@@ -437,6 +437,7 @@ AC_CONFIG_FILES([
                  etc/upgrade/time-worked-history
                  etc/upgrade/upgrade-articles
                  etc/upgrade/vulnerable-passwords
+                 etc/upgrade/upgrade-sla
                  sbin/rt-attributes-viewer
                  sbin/rt-preferences-viewer
                  sbin/rt-session-viewer
diff --git a/etc/upgrade/upgrade-sla.in b/etc/upgrade/upgrade-sla.in
new file mode 100644
index 0000000..9cc1f28
--- /dev/null
+++ b/etc/upgrade/upgrade-sla.in
@@ -0,0 +1,91 @@
+#!@PERL@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use 5.10.1;
+use strict;
+use warnings;
+
+use lib "@LOCAL_LIB_PATH@";
+use lib "@RT_LIB_PATH@";
+
+use RT::Interface::CLI qw(Init);
+Init();
+
+my $tickets = RT::Tickets->new(RT->SystemUser);
+$tickets->FromSQL('CF.SLA IS NOT NULL AND SLA IS NULL');
+while ( my $ticket = $tickets->Next ) {
+    my ($ret, $msg) = $ticket->SetSLA($ticket->FirstCustomFieldValue('SLA'));
+    unless ( $ret ) {
+        RT->Logger->error("Failed to upgrade SLA for ticket #" . $ticket->id . ": $msg");
+    }
+}
+
+my $queues = RT::Queues->new(RT->SystemUser);
+$queues->UnLimit;
+
+my %cfs_to_disable;
+while ( my $queue = $queues->Next ) {
+    my $cfs = $queue->TicketCustomFields;
+    $cfs->Limit(FIELD => 'Name', VALUE => 'SLA', CASESENSITIVE => 0 );
+    if ( my $cf = $cfs->First ) {
+        $cfs_to_disable{$cf->id} ||= $cf;
+    }
+    elsif ( !$queue->SLADisabled ) {
+        my ($ret, $msg) = $queue->SetSLADisabled(1);
+        unless ( $ret ) {
+            RT->Logger->error("Failed to disable SLA for queue #" . $queue->id . ": $msg");
+        }
+    }
+}
+
+for my $cf ( values %cfs_to_disable ) {
+    my ($ret, $msg) = $cf->SetDisabled(1);
+    unless ( $ret ) {
+        RT->Logger->error("Failed to disable custom field SLA #" . $cf->id . ": $msg");
+    }
+}

commit 6eaad0416162dad8d1b4540825f8a2ee480a7f1f
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 02:07:26 2015 +0800

    upgrading note for core sla

diff --git a/docs/UPGRADING-4.4 b/docs/UPGRADING-4.4
index 78c2f85..3827cf4 100644
--- a/docs/UPGRADING-4.4
+++ b/docs/UPGRADING-4.4
@@ -37,6 +37,17 @@ Homepage component "Quicksearch" has been renamed to "QueueList" to reflect
 what it actually is. Please update C<$HomepageComponents> accordingly if you
 customized it in site config.
 
+=item *
+
+SLA is in core now, so C<SLA> became a core field. if you installed
+C<RT::Extension::SLA> before, you need to remove it from your plugins, adjust
+configs accordingly and run F<etc/upgrade/upgrade-sla>. see also the SLA
+section in F<RT_Config.pm>.
+
+Note that with core SLA, you can't define different set of levels for
+different queues. i.e. all the queues share the same set of levels defined in
+C<%ServiceAgreements>.
+
 =back
 
 =cut

commit 8bfb984257cd45a4ef9811a4ba10607b4e3b570a
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 02:24:08 2015 +0800

    fix uninitialized warnings to make tests happy

diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index e9a7abf..55cf793 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -237,8 +237,8 @@ sub GetDefaultServiceLevel {
     }
     if ( $args{'Queue'} ) {
         return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
-
-        if ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } ) {
+        if ( $RT::ServiceAgreements{'QueueDefault'} &&
+            ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } )) {
             return $info unless ref $info;
             return $info->{'Level'} || $RT::ServiceAgreements{'Default'};
         }

-----------------------------------------------------------------------


More information about the rt-commit mailing list