[Rt-commit] rt branch, 4.4/sla, created. rt-4.2.11-144-g5d441eb

? sunnavy sunnavy at bestpractical.com
Thu Aug 13 12:13:18 EDT 2015


The branch, 4.4/sla has been created
        at  5d441ebff4ff685ffc4c17e8e11fa2d8518a5cb4 (commit)

- Log -----------------------------------------------------------------
commit df41846df9562a7386627db03730fbd35190b0aa
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..a5cd3cf
--- /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 ( $RT::ServiceAgreements{'QueueDefault'} &&
+            ( 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;
diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index 021682f..b16bae3 100644
--- a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -190,6 +190,7 @@ sub set_dep {
 
 $deps{'CORE'} = [ text_to_hash( << '.') ];
 Apache::Session 1.53
+Business::Hours
 CGI 3.38
 CGI::Cookie 1.20
 CGI::Emulate::PSGI

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

    use SLA column in Tickets table instead of a custom field

diff --git a/etc/initialdata b/etc/initialdata
index 21650a6..77a56d5 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 => "",
@@ -249,12 +238,12 @@
     },
     {  Name        => '[SLA] Require Starts set', # loc
        Description => 'Detect a situation when we should set Starts date' , # loc
-       ApplicableTransTypes => 'Create,CustomField',
+       ApplicableTransTypes => 'Create,Set',
        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',
+       ApplicableTransTypes => 'Create,Correspond,Set,Status',
        ExecModule => 'SLA_RequireDueSet',
     },
 
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 83%
rename from etc/upgrade/4.3.7/content
rename to etc/upgrade/4.3.8/content
index 2c24760..27513fb 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
@@ -20,12 +9,12 @@ our @ScripConditions = (
     },
     {  Name        => '[SLA] Require Starts set', # loc
        Description => 'Detect a situation when we should set Starts date' , # loc
-       ApplicableTransTypes => 'Create,CustomField',
+       ApplicableTransTypes => 'Create,Set',
        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',
+       ApplicableTransTypes => 'Create,Correspond,Set,Status',
        ExecModule => 'SLA_RequireDueSet',
     },
 );
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 a5cd3cf..c3b443f 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 a07510707e6f8db00684127c0f13c33528794f11
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 633e4389159c26e10b995854fda304737c67972e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 12 01:15:15 2015 +0800

    set default SLA in ticket create stage directly
    
    no need to use a scrip to do it any more.

diff --git a/etc/initialdata b/etc/initialdata
index 77a56d5..5787fe6 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -111,10 +111,6 @@
     { 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',
@@ -231,11 +227,6 @@
        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,Set',
@@ -819,10 +810,6 @@ 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',
diff --git a/etc/upgrade/4.3.8/content b/etc/upgrade/4.3.8/content
index 27513fb..ac336ff 100644
--- a/etc/upgrade/4.3.8/content
+++ b/etc/upgrade/4.3.8/content
@@ -2,11 +2,6 @@ use strict;
 use warnings;
 
 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,Set',
@@ -20,10 +15,6 @@ our @ScripConditions = (
 );
 
 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',
@@ -35,10 +26,6 @@ our @ScripActions = (
 );
 
 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',
@@ -48,4 +35,3 @@ our @Scrips = (
        ScripAction       => '[SLA] Set due date',
        Template          => 'Blank' },
 );
-
diff --git a/lib/RT/Action/SLA_SetDefault.pm b/lib/RT/Action/SLA_SetDefault.pm
deleted file mode 100644
index ea918a2..0000000
--- a/lib/RT/Action/SLA_SetDefault.pm
+++ /dev/null
@@ -1,95 +0,0 @@
-# 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 $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->SetSLA($level);
-    unless ( $status ) {
-        $RT::Logger->error("Couldn't set service level: $msg");
-        return 0;
-    }
-
-    return 1;
-};
-
-1;
diff --git a/lib/RT/Condition/SLA_RequireDefault.pm b/lib/RT/Condition/SLA_RequireDefault.pm
deleted file mode 100644
index 5513c2b..0000000
--- a/lib/RT/Condition/SLA_RequireDefault.pm
+++ /dev/null
@@ -1,74 +0,0 @@
-# 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->QueueObj->SLADisabled;
-    return 0 if $ticket->SLA;
-    return 1;
-}
-
-1;
-
diff --git a/lib/RT/SLA.pm b/lib/RT/SLA.pm
index c3b443f..215a87a 100644
--- a/lib/RT/SLA.pm
+++ b/lib/RT/SLA.pm
@@ -555,6 +555,7 @@ sub GetDefaultServiceLevel {
         $args{'Queue'} = $args{'Ticket'}->QueueObj;
     }
     if ( $args{'Queue'} ) {
+        return undef if $args{Queue}->SLADisabled;
         return $args{'Queue'}->SLA if $args{'Queue'}->SLA;
         if ( $RT::ServiceAgreements{'QueueDefault'} &&
             ( my $info = $RT::ServiceAgreements{'QueueDefault'}{ $args{'Queue'}->Name } )) {
diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm
index dd63eee..e8325f7 100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@ -390,7 +390,9 @@ sub Create {
         Started         => $Started->ISO,
         Resolved        => $Resolved->ISO,
         Due             => $Due->ISO,
-        SLA             => $args{SLA},
+        $args{ 'Type' } eq 'ticket'
+          ? ( SLA => $args{ SLA } || RT::SLA->GetDefaultServiceLevel( Queue => $QueueObj ), )
+          : (),
     );
 
 # Parameters passed in during an import that we probably don't want to touch, otherwise

commit 61faa65ab6d8cf5a85d4edf87bd77b5795d5923e
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..28eaf6d 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2918,9 +2918,338 @@ Set(%Lifecycles,
     },
 );
 
+=head1 SLA
 
+=over 4
+
+=item C<%ServiceAgreements>
+
+    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 215a87a..c50d696 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 52f5f62b88fcb3f1323d3dec61de16793b5a1871
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Mon Aug 3 01:52:58 2015 +0800

    upgrade-sla script to migrate old data used in SLA extension

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..d885b65
--- /dev/null
+++ b/etc/upgrade/upgrade-sla.in
@@ -0,0 +1,142 @@
+#!@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);
+        if ( $ret ) {
+            RT->Logger->info("Disabled SLA for queue #" . $queue->id . " because it doesn't have custom field SLA applied");
+        }
+        else {
+            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);
+    if ( $ret ) {
+        RT->Logger->info("Disabled custom field SLA #" . $cf->id);
+    }
+    else {
+        RT->Logger->error("Failed to disable custom field SLA #" . $cf->id . ": $msg");
+    }
+}
+
+my @old_scrips = ( '[SLA] Set default service level if needed', '[SLA] Set starts date if needed', '[SLA] Set due date if needed' );
+for my $item ( @old_scrips ) {
+    my $scrip = RT::Scrip->new(RT->SystemUser);
+    $scrip->LoadByCols( Description => $item );
+    if ( $scrip->id ) {
+        my ($ret, $msg) = $scrip->RT::Record::Delete();
+        if ( $ret ) {
+            RT->Logger->info(qq{Deleted scrip "$item"});
+        }
+        else {
+            RT->Logger->error(qq{Failed to delete scrip "$item": $msg});
+        }
+    }
+}
+
+my @old_conditions = ( '[SLA] Require default', '[SLA] Require Starts set', '[SLA] Require Due set' );
+for my $item ( @old_conditions ) {
+    my $condition = RT::ScripCondition->new(RT->SystemUser);
+    $condition->Load($item);
+    if ( $condition->id ) {
+        my ($ret, $msg) = $condition->RT::Record::Delete();
+        if ( $ret ) {
+            RT->Logger->info(qq{Deleted condition "$item"});
+        }
+        else {
+            RT->Logger->error(qq{Failed to delete condition "$item": $msg});
+        }
+    }
+}
+
+my @old_actions = ('[SLA] Set default service level', '[SLA] Set starts date', '[SLA] Set due date' );
+for my $item ( @old_actions ) {
+    my $action = RT::ScripAction->new(RT->SystemUser);
+    $action->Load($item);
+    if ( $action->id ) {
+        my ($ret, $msg) = $action->RT::Record::Delete();
+        if ( $ret ) {
+            RT->Logger->info(qq{Failed to delete action "$item"});
+        }
+        else {
+            RT->Logger->error(qq{Failed to delete action "$item": $msg});
+        }
+    }
+}

commit f255b26f907007a6cf7a1d94f5dbeb22f8089b1a
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 b645c2b1b4c7bbeb0d967daa850fb34c45b146f6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 12 00:35:40 2015 +0800

    warn if we still have SLA extension enabled
    
    we can't die here because it's possible that someone wrote his own version
    of SLA extension that's not related to ours at all.

diff --git a/lib/RT.pm b/lib/RT.pm
index 4147992..f0dbe37 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -722,11 +722,18 @@ their lib and L<HTML::Mason> component roots.
 
 =cut
 
+our %CORED_PLUGINS = (
+    'RT::Extension::SLA' => '4.4',
+);
+
 sub InitPlugins {
     my $self    = shift;
     my @plugins;
     require RT::Plugin;
     foreach my $plugin (grep $_, RT->Config->Get('Plugins')) {
+        if ( $CORED_PLUGINS{$plugin} ) {
+            RT->Logger->warning( "$plugin has been cored since RT $CORED_PLUGINS{$plugin}, please check the upgrade document for more details" );
+        }
         $plugin->require;
         die $UNIVERSAL::require::ERROR if ($UNIVERSAL::require::ERROR);
         push @plugins, RT::Plugin->new(name =>$plugin);

commit b109c8a8cf942663c1ad34e7a8257315152418e3
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Aug 13 19:45:00 2015 +0800

    better name of SLA conditions/actions/scrips

diff --git a/etc/initialdata b/etc/initialdata
index 5787fe6..21c8888 100644
--- a/etc/initialdata
+++ b/etc/initialdata
@@ -111,11 +111,11 @@
     { Name        => 'Send Forward',                 # loc
       Description => 'Send forwarded message',       # loc
       ExecModule  => 'SendForward', },
-    {  Name        => '[SLA] Set starts date', # loc
+    {  Name        => 'Set starts date according to SLA', # loc
        Description => 'Set the starts date according to an agreement' , # loc
        ExecModule  => 'SLA_SetStarts',
     },
-    {  Name        => '[SLA] Set due date', # loc
+    {  Name        => 'Set due date according to SLA', # loc
        Description => 'Set the due date according to an agreement' , # loc
        ExecModule  => 'SLA_SetDue',
     },
@@ -227,12 +227,12 @@
        ApplicableTransTypes => 'Status,Set',
        ExecModule           => 'ReopenTicket',
     },
-    {  Name        => '[SLA] Require Starts set', # loc
+    {  Name        => 'Require Starts set according to SLA', # loc
        Description => 'Detect a situation when we should set Starts date' , # loc
        ApplicableTransTypes => 'Create,Set',
        ExecModule => 'SLA_RequireStartsSet',
     },
-    {  Name        => '[SLA] Require Due set', # loc
+    {  Name        => 'Require Due set according to SLA', # loc
        Description => 'Detect a situation when we should set Due date' , # loc
        ApplicableTransTypes => 'Create,Correspond,Set,Status',
        ExecModule => 'SLA_RequireDueSet',
@@ -810,13 +810,13 @@ Hour:         { $SubscriptionObj->SubValue('Hour') }
        ScripCondition => 'On Forward Ticket',
        ScripAction    => 'Send Forward',
        Template       => 'Forward Ticket' },
-    {  Description       => "[SLA] Set starts date if needed",
-       ScripCondition    => '[SLA] Require starts set',
-       ScripAction       => '[SLA] Set starts date',
+    {  Description       => "Set starts date if needed according to SLA",
+       ScripCondition    => 'Require starts set according to SLA',
+       ScripAction       => 'Set starts date according to SLA',
        Template          => 'Blank' },
-    {  Description       => "[SLA] Set due date if needed",
-       ScripCondition    => '[SLA] Require due set',
-       ScripAction       => '[SLA] Set due date',
+    {  Description       => "Set due date if needed according to SLA",
+       ScripCondition    => 'Require due set according to SLA',
+       ScripAction       => 'Set due date according to SLA',
        Template          => 'Blank' },
 );
 
diff --git a/etc/upgrade/4.3.8/content b/etc/upgrade/4.3.8/content
index ac336ff..38e3a68 100644
--- a/etc/upgrade/4.3.8/content
+++ b/etc/upgrade/4.3.8/content
@@ -2,12 +2,12 @@ use strict;
 use warnings;
 
 our @ScripConditions = (
-    {  Name        => '[SLA] Require Starts set', # loc
+    {  Name        => 'Require Starts set according to SLA', # loc
        Description => 'Detect a situation when we should set Starts date' , # loc
        ApplicableTransTypes => 'Create,Set',
        ExecModule => 'SLA_RequireStartsSet',
     },
-    {  Name        => '[SLA] Require Due set', # loc
+    {  Name        => 'Require Due set according to SLA', # loc
        Description => 'Detect a situation when we should set Due date' , # loc
        ApplicableTransTypes => 'Create,Correspond,Set,Status',
        ExecModule => 'SLA_RequireDueSet',
@@ -15,23 +15,23 @@ our @ScripConditions = (
 );
 
 our @ScripActions = (
-    {  Name        => '[SLA] Set starts date', # loc
+    {  Name        => 'Set starts date according to SLA', # loc
        Description => 'Set the starts date according to an agreement' , # loc
        ExecModule  => 'SLA_SetStarts',
     },
-    {  Name        => '[SLA] Set due date', # loc
+    {  Name        => 'Set due date according to SLA', # loc
        Description => 'Set the due date according to an agreement' , # loc
        ExecModule  => 'SLA_SetDue',
     },
 );
 
 our @Scrips = (
-    {  Description       => "[SLA] Set starts date if needed",
-       ScripCondition    => '[SLA] Require starts set',
-       ScripAction       => '[SLA] Set starts date',
+    {  Description       => "Set starts date if needed according to SLA",
+       ScripCondition    => 'Require starts set according to SLA',
+       ScripAction       => 'Set starts date according to SLA',
        Template          => 'Blank' },
-    {  Description       => "[SLA] Set due date if needed",
-       ScripCondition    => '[SLA] Require due set',
-       ScripAction       => '[SLA] Set due date',
+    {  Description       => "Set due date if needed according to SLA",
+       ScripCondition    => 'Require due set according to SLA',
+       ScripAction       => 'Set due date according to SLA',
        Template          => 'Blank' },
 );

commit 5d441ebff4ff685ffc4c17e8e11fa2d8518a5cb4
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Wed Aug 12 01:51:43 2015 +0800

    SLA tests
    
    most tests are migrated from SLA extension.
    I also added more tests for web and ExcludeTimeOnIgnoredStatuses option

diff --git a/t/sla/business_hours.t b/t/sla/business_hours.t
new file mode 100644
index 0000000..510faeb
--- /dev/null
+++ b/t/sla/business_hours.t
@@ -0,0 +1,64 @@
+use strict;
+use warnings;
+
+use Test::MockTime qw( :all );
+use RT::Test tests => undef;
+
+# we assume the RT's Timezone is UTC now, need a smart way to get over that.
+$ENV{'TZ'} = 'GMT';
+RT->Config->Set( Timezone => 'GMT' );
+
+diag 'check business hours' if $ENV{'TEST_VERBOSE'};
+{
+
+    no warnings 'once';
+    %RT::ServiceAgreements = (
+        Default => 'Sunday',
+        Levels  => {
+            Sunday => {
+                Resolve       => { BusinessMinutes => 60 },
+                BusinessHours => 'Sunday',
+            },
+            Monday => {
+                Resolve       => { BusinessMinutes => 60 },
+            },
+        },
+    );
+
+    %RT::ServiceBusinessHours = (
+        Sunday => {
+            0 => {
+                Name  => 'Sunday',
+                Start => '9:00',
+                End   => '17:00'
+            }
+        },
+        Default => {
+            1 => {
+                Name  => 'Monday',
+                Start => '9:00',
+                End   => '17:00'
+            },
+        },
+    );
+
+    set_absolute_time('2007-01-01T00:00:00Z');
+
+    my $ticket = RT::Ticket->new($RT::SystemUser);
+    my ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx' );
+    ok( $id, "created ticket #$id" );
+
+    is( $ticket->SLA, 'Sunday', 'default sla' );
+
+    my $start = $ticket->StartsObj->Unix;
+    my $due = $ticket->DueObj->Unix;
+    is( $start, 1168160400, 'Start date is 2007-01-07T09:00:00Z' );
+    is( $due, 1168164000, 'Due date is 2007-01-07T10:00:00Z' );
+
+    $ticket->SetSLA( 'Monday' );
+    is( $ticket->SLA, 'Monday', 'new sla' );
+    $due = $ticket->DueObj->Unix;
+    is( $due, 1167645600, 'Due date is 2007-01-01T10:00:00Z' );
+}
+
+done_testing();
diff --git a/t/sla/due.t b/t/sla/due.t
new file mode 100644
index 0000000..7fb96c9
--- /dev/null
+++ b/t/sla/due.t
@@ -0,0 +1,396 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+diag 'check change of Due date when SLA for a ticket is changed' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Resolve => { RealMinutes => 60*2 } },
+            '4' => { Resolve => { RealMinutes => 60*4 } },
+        },
+    );
+
+    my $time = time;
+
+    my $ticket = RT::Ticket->new( $RT::SystemUser );
+    my ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx' );
+    ok $id, "created ticket #$id";
+
+    is $ticket->SLA, '2', 'default sla';
+
+    my $orig_due = $ticket->DueObj->Unix;
+    ok $orig_due > 0, 'Due date is set';
+    ok $orig_due > $time, 'Due date is in the future';
+
+    $ticket->SetSLA('4');
+    is $ticket->SLA, '4', 'new sla';
+
+    my $new_due = $ticket->DueObj->Unix;
+    ok $new_due > 0, 'Due date is set';
+    ok $new_due > $time, 'Due date is in the future';
+
+    is $new_due, $orig_due+2*60*60, 'difference is two hours';
+}
+
+diag 'when not requestor creates a ticket, we dont set due date' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    my $ticket = RT::Ticket->new( $RT::SystemUser );
+    my ($id) = $ticket->Create(
+        Queue => 'General',
+        Subject => 'xxx',
+        Requestor => 'user at example.com',
+    );
+    ok $id, "created ticket #$id";
+
+    is $ticket->SLA, '2', 'default sla';
+
+    my $due = $ticket->DueObj->Unix;
+    ok $due <= 0, 'Due date is not set';
+}
+
+diag 'check that reply to requestors unset due date' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates
+    my $id;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+        );
+        ok $id, "created ticket #$id";
+
+        is $ticket->SLA, '2', 'default sla';
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is set';
+    }
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due <= 0, 'Due date is not set';
+    }
+
+    # non-requestor reply again
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are still working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due <= 0, 'Due date is not set';
+    }
+
+    # requestor reply
+    my $last_unreplied_due;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        $ticket->Correspond( Content => 'what\'s going on with my ticket?' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is set again';
+
+        $last_unreplied_due = $due;
+    }
+
+    # sleep at least one second and requestor replies again
+    sleep 1;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        $ticket->Correspond( Content => 'HEY! Were is my answer?' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is still set';
+        is $due, $last_unreplied_due, 'due is unchanged';
+    }
+}
+
+diag 'check that reply to requestors dont unset due date with KeepInLoop' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => {
+                Response   => { RealMinutes => 60*2 },
+                KeepInLoop => { RealMinutes => 60*4 },
+            },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates
+    my $id;
+    my $due;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+        );
+        ok $id, "created ticket #$id";
+
+        is $ticket->SLA, '2', 'default sla';
+
+        $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is set';
+    }
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp > $due, "keep in loop is 4hours when response is 2hours";
+        $due = $tmp;
+    }
+
+    # non-requestor reply again
+    {
+        sleep 1;
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are still working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp > $due, "keep in loop sligtly moved";
+        $due = $tmp;
+    }
+
+    # requestor reply
+    my $last_unreplied_due;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        $ticket->Correspond( Content => 'what\'s going on with my ticket?' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp < $due, "response deadline is 2 hours earlier";
+        $due = $tmp;
+
+        $last_unreplied_due = $due;
+    }
+
+    # sleep at least one second and requestor replies again
+    sleep 1;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        $ticket->Correspond( Content => 'HEY! Were is my answer?' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        is $tmp, $last_unreplied_due, 'due is unchanged';
+        $due = $tmp;
+    }
+}
+
+diag 'check that replies dont affect resolve deadlines' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Resolve => { RealMinutes => 60*2 } },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates
+    my ($id, $orig_due);
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+        );
+        ok $id, "created ticket #$id";
+
+        is $ticket->SLA, '2', 'default sla';
+
+        $orig_due = $ticket->DueObj->Unix;
+        ok $orig_due > 0, 'Due date is set';
+    }
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is set';
+        is $due, $orig_due, 'due is not changed';
+    }
+
+    # requestor reply
+    {
+        my $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        $ticket->Correspond( Content => 'what\'s going on with my ticket?' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'Due date is set';
+        is $due, $orig_due, 'due is not changed';
+    }
+}
+
+diag 'check that owner is not treated as requestor' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates and he is owner
+    my $id;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+            Owner => $root->id,
+        );
+        ok $id, "created ticket #$id";
+
+        is $ticket->SLA, '2', 'default sla';
+        is $ticket->Owner, $root->id, 'correct owner';
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due <= 0, 'Due date is not set';
+    }
+}
+
+diag 'check that response deadline is left alone when there is no requestor' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => { Response => { RealMinutes => 60*2 } },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # create a ticket without requestor
+    my $id;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+        );
+        ok $id, "created ticket #$id";
+
+        is $ticket->SLA, '2', 'default sla';
+
+        my $due = $ticket->DueObj->Unix;
+        ok $due <= 0, 'Due date is not set';
+    }
+}
+
+done_testing();
diff --git a/t/sla/ignore-on-statuses.t b/t/sla/ignore-on-statuses.t
new file mode 100644
index 0000000..496f530
--- /dev/null
+++ b/t/sla/ignore-on-statuses.t
@@ -0,0 +1,261 @@
+use strict;
+use warnings;
+
+use RT::Test tests => undef;
+
+diag 'check that reply to requestors dont unset due date with KeepInLoop' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => {
+                KeepInLoop => { RealMinutes => 60*4, IgnoreOnStatuses => ['stalled'] },
+            },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates
+    my $id;
+    my $due;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+        );
+        ok $id, "created ticket #$id";
+        is $ticket->SLA, '2', 'default sla';
+        ok !$ticket->DueObj->Unix, 'no response deadline';
+        $due = 0;
+    }
+
+    # non-requestor reply
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are working on this.' );
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp > $due, "keep in loop due set";
+        $due = $tmp;
+    }
+
+    # stalling ticket
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        my ($status, $msg) = $ticket->SetStatus('stalled');
+        ok $status, 'stalled the ticket';
+
+        $ticket->Load( $id );
+        ok !$ticket->DueObj->Unix, 'keep in loop deadline ignored for stalled';
+    }
+
+    # non-requestor reply again
+    {
+        sleep 1;
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are still working on this.' );
+        $ticket->SetStatus('open');
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        is $ticket->Status, 'open', 'ticket was opened';
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp > $due, "keep in loop sligtly moved";
+        $due = $tmp;
+    }
+}
+
+diag 'Check that failing to reply to the requestors is not ignored' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => {
+                Response   => { RealMinutes => 60*2 },
+                KeepInLoop => { RealMinutes => 60*4, IgnoreOnStatuses => ['stalled'] },
+            },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    # requestor creates
+    my $id;
+    my $due;
+    {
+        my $ticket = RT::Ticket->new( $root );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $root->id,
+        );
+        ok $id, "created ticket #$id";
+        is $ticket->SLA, '2', 'default sla';
+        $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'response deadline';
+    }
+
+    # stalling ticket
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        my ($status, $msg) = $ticket->SetStatus('stalled');
+        ok $status, 'stalled the ticket';
+
+        $ticket->Load( $id );
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp, 'response deadline not unset';
+        is $tmp, $due, 'due not changed';
+    }
+
+    # non-requestor reply
+    {
+        sleep 1;
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'we are still working on this.' );
+        $ticket->SetStatus('open');
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        is $ticket->Status, 'open', 'ticket was opened';
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp > $due, "keep in loop is greater than response";
+        $due = $tmp;
+    }
+
+    # stalling ticket again
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        my ($status, $msg) = $ticket->SetStatus('stalled');
+        ok $status, 'stalled the ticket';
+
+        $ticket->Load( $id );
+        ok !$ticket->DueObj->Unix, 'keep in loop deadline unset for stalled';
+    }
+}
+
+diag 'check the ExcludeTimeOnIgnoredStatuses option' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => {
+                Response => { RealMinutes => 60*2, IgnoreOnStatuses => ['stalled'] },
+            },
+        },
+    );
+
+    my $root = RT::User->new( $RT::SystemUser );
+    $root->LoadByEmail('root at localhost');
+    ok $root->id, 'loaded root user';
+
+    my $bob =
+      RT::Test->load_or_create_user( Name => 'bob', EmailAddress => 'bob at example.com', Password => 'password' );
+    ok( $bob->Id, "Created test user bob" );
+    ok( RT::Test->add_rights( { Principal => 'Privileged', Right => [ qw(CreateTicket ShowTicket ModifyTicket SeeQueue) ] } ),
+        'Granted ticket management rights' );
+
+    # requestor creates
+    my $id;
+    my $due;
+    {
+        my $ticket = RT::Ticket->new( $bob );
+        ($id) = $ticket->Create(
+            Queue => 'General',
+            Subject => 'xxx',
+            Requestor => $bob->id,
+        );
+        ok $id, "created ticket #$id";
+        is $ticket->SLA, '2', 'default sla';
+        $due = $ticket->DueObj->Unix;
+        ok $due > 0, 'response deadline';
+    }
+
+    # stalling ticket
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        my ($status, $msg) = $ticket->SetStatus('stalled');
+        ok $status, 'stalled the ticket';
+
+        $ticket->Load( $id );
+        ok !$ticket->DueObj->Unix, 'deadline ignored for stalled';
+    }
+
+    # requestor reply again
+    {
+        sleep 1;
+        my $ticket = RT::Ticket->new( $bob );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+        $ticket->Correspond( Content => 'please reopen this ticket, we are good to continue' );
+        $ticket->SetStatus('open');
+
+        $ticket = RT::Ticket->new( $root );
+        $ticket->Load( $id );
+        ok $ticket->id, "loaded ticket #$id";
+
+        is $ticket->Status, 'open', 'ticket was opened';
+
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp == $due, "deadline not changed";
+    }
+
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels => {
+            '2' => {
+                Response => { RealMinutes => 60*2, IgnoreOnStatuses => ['stalled'], ExcludeTimeOnIgnoredStatuses => 1 },
+            },
+        },
+    );
+    {
+        my $ticket = RT::Ticket->new( $RT::SystemUser );
+        $ticket->Load( $id );
+        my ($status, $msg) = $ticket->SetStatus('stalled');
+        ok $status, 'stalled the ticket';
+        ok !$ticket->DueObj->Unix, 'deadline ignored for stalled';
+        sleep 1;
+        $ticket->SetStatus('open');
+        is $ticket->Status, 'open', 'ticket was opened';
+        my $tmp = $ticket->DueObj->Unix;
+        ok $tmp > 0, 'Due date is set';
+        ok $tmp >= $due+1, "deadline slighted moved";
+        ok $tmp <= $due+5, "deadline slighted moved but not much";
+    }
+}
+
+done_testing;
diff --git a/t/sla/queue.t b/t/sla/queue.t
new file mode 100644
index 0000000..622d21f
--- /dev/null
+++ b/t/sla/queue.t
@@ -0,0 +1,52 @@
+use strict;
+use warnings;
+
+use Test::MockTime qw( :all );
+use RT::Test tests => undef;
+
+my $queue = RT::Queue->new($RT::SystemUser);
+$queue->Load('General');
+
+my $queue_sla = RT::Attribute->new($RT::SystemUser);
+
+diag 'check set of Due date with Queue default SLA' if $ENV{'TEST_VERBOSE'};
+{
+
+    # add default SLA for 'General';
+    my ($id) = $queue_sla->Create(
+        Name        => 'SLA',
+        Description => 'Default Queue SLA',
+        Content     => '4',
+        Object      => $queue
+    );
+
+    ok( $id, 'Created SLA Attribute for General' );
+
+    no warnings 'once';
+    %RT::ServiceAgreements = (
+        Default => '2',
+        Levels  => {
+            '2' => { Resolve => { RealMinutes => 60 * 2 } },
+            '4' => { StartImmediately => 1, Resolve => { RealMinutes => 60 * 4 } },
+        },
+    );
+
+
+    set_absolute_time('2007-01-01T00:00:00Z');
+    my $time = time;
+    my $ticket = RT::Ticket->new($RT::SystemUser);
+    ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx' );
+    ok( $id, "created ticket #$id" );
+
+    is $ticket->SLA, '4', 'default sla';
+
+    my $start = $ticket->StartsObj->Unix;
+    my $due = $ticket->DueObj->Unix;
+    is( $start, $time, 'Start Date is right' );
+    is( $due, $time+3600*4, 'Due date is right');
+
+    my ( $status, $message ) = $queue->DeleteAttribute('SLA');
+    ok( $status, $message );
+}
+
+done_testing;
diff --git a/t/sla/starts.t b/t/sla/starts.t
new file mode 100644
index 0000000..4769c62
--- /dev/null
+++ b/t/sla/starts.t
@@ -0,0 +1,79 @@
+use strict;
+use warnings;
+
+use Test::MockTime qw( :all );
+use RT::Test tests => undef;
+
+# we assume the RT's Timezone is UTC now, need a smart way to get over that.
+$ENV{'TZ'} = 'GMT';
+RT->Config->Set( Timezone => 'GMT' );
+
+my $bhours = RT::SLA->BusinessHours;
+
+diag 'check Starts date' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => 'standard',
+        Levels  => {
+            'standard' => {
+                Response => 2 * 60,
+                Resolve  => 7 * 60 * 24,
+            },
+        },
+    );
+    %RT::ServiceBusinessHours = (
+        Default => {
+            1 => {
+                Name  => 'Monday',
+                Start => '09:00',
+                End   => '17:00'
+            },
+            2 => {
+                Name  => 'Tuesday',
+                Start => '09:00',
+                End   => '17:00'
+            },
+        }
+    );
+
+    my %time = (
+        '2007-01-01T13:15:00Z' => 1167657300,    # 2007-01-01T13:15:00Z
+        '2007-01-01T19:15:00Z' => 1167728400,    # 2007-01-02T09:00:00Z
+        '2007-01-06T13:15:00Z' => 1168246800,    # 2007-01-08T09:00:00Z
+    );
+
+    for my $time ( keys %time ) {
+        set_absolute_time($time);
+        my $ticket = RT::Ticket->new($RT::SystemUser);
+        my ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx' );
+        ok $id, "created ticket #$id";
+        is $ticket->StartsObj->Unix, $time{$time}, 'Starts date is right';
+    }
+
+    restore_time();
+}
+
+diag 'check Starts date with StartImmediately enabled' if $ENV{'TEST_VERBOSE'};
+{
+    %RT::ServiceAgreements = (
+        Default => 'start immediately',
+        Levels  => {
+            'start immediately' => {
+                StartImmediately => 1,
+                Response         => 2 * 60,
+                Resolve          => 7 * 60 * 24,
+            },
+        },
+    );
+    my $time = time;
+
+    my $ticket = RT::Ticket->new($RT::SystemUser);
+    my ($id) = $ticket->Create( Queue => 'General', Subject => 'xxx' );
+    ok $id, "created ticket #$id";
+
+    my $starts = $ticket->StartsObj->Unix;
+    ok $starts > 0, 'Starts date is set';
+    is $starts, $ticket->CreatedObj->Unix, 'Starts is correct';
+}
+
+done_testing;
diff --git a/t/sla/timezone.t b/t/sla/timezone.t
new file mode 100644
index 0000000..8edf894
--- /dev/null
+++ b/t/sla/timezone.t
@@ -0,0 +1,51 @@
+use strict;
+use warnings;
+
+use Test::MockTime qw( :all );
+use RT::Test tests => undef;
+
+my $ru_queue = RT::Test->load_or_create_queue( Name => 'RU' );
+ok $ru_queue && $ru_queue->id, 'created RU queue';
+
+my $us_queue = RT::Test->load_or_create_queue( Name => 'US' );
+ok $us_queue && $ru_queue->id, 'created US queue';
+
+no warnings 'once';
+%RT::ServiceAgreements = (
+    Default => 2,
+    QueueDefault => {
+        RU => { Timezone => 'Europe/Moscow' },
+        US => { Timezone => 'America/New_York' },
+    },
+    Levels  => {
+        '2' => { Resolve => { BusinessMinutes => 60 * 2 } },
+    },
+);
+
+set_absolute_time('2007-01-01T22:00:00Z');
+
+diag 'check dates in US queue' if $ENV{'TEST_VERBOSE'};
+{
+    my $ticket = RT::Ticket->new($RT::SystemUser);
+    my ($id) = $ticket->Create( Queue => 'US', Subject => 'xxx' );
+    ok( $id, "created ticket #$id" );
+
+    my $start = $ticket->StartsObj->ISO( Timezone => 'utc' );
+    is( $start, '2007-01-01 22:00:00', 'Start date is right' );
+    my $due = $ticket->DueObj->ISO( Timezone => 'utc' );
+    is( $due, '2007-01-02 15:00:00', 'Due date is right' );
+}
+
+diag 'check dates in RU queue' if $ENV{'TEST_VERBOSE'};
+{
+    my $ticket = RT::Ticket->new($RT::SystemUser);
+    my ($id) = $ticket->Create( Queue => 'RU', Subject => 'xxx' );
+    ok( $id, "created ticket #$id" );
+
+    my $start = $ticket->StartsObj->ISO( Timezone => 'utc' );
+    is( $start, '2007-01-02 06:00:00', 'Start date is right' );
+    my $due = $ticket->DueObj->ISO( Timezone => 'utc' );
+    is( $due, '2007-01-02 08:00:00', 'Due date is right' );
+}
+
+done_testing;
diff --git a/t/sla/web.t b/t/sla/web.t
new file mode 100644
index 0000000..d62d45b
--- /dev/null
+++ b/t/sla/web.t
@@ -0,0 +1,106 @@
+use strict;
+use warnings;
+
+my $now;
+
+BEGIN {
+    $now = time();
+    use Test::MockTime 'set_fixed_time';
+    set_fixed_time( $now );
+
+    use RT::Test tests => undef;
+}
+
+%RT::ServiceAgreements = (
+    Default => '2',
+    Levels  => {
+        '2' => {
+            StartImmediately => 1,
+            Response => { RealMinutes => 60 * 2 },
+        },
+        '4' => {
+            StartImmediately => 1,
+            Response => { RealMinutes => 60 * 4 },
+        },
+    },
+);
+
+my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+
+my $user = RT::Test->load_or_create_user(
+    Name         => 'user',
+    Password     => 'password',
+    EmailAddress => 'user at example.com',
+);
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+ok(
+    RT::Test->set_rights(
+        { Principal => $user, Right => [ qw(SeeQueue CreateTicket ShowTicket ModifyTicket ShowConfigTab AdminQueue) ] },
+    ),
+    'set rights'
+);
+
+ok $m->login( 'user', 'password' ), 'logged in as user';
+
+{
+
+    $m->goto_create_ticket( $queue->id );
+    my $form = $m->form_name( 'TicketCreate' );
+    my $sla  = $form->find_input( 'SLA' );
+    is_deeply( [$sla->possible_values], [ 2, 4 ], 'possible sla' );
+    $m->submit_form( fields => { Subject => 'ticket foo with default sla' } );
+
+    my $ticket = RT::Test->last_ticket;
+    ok( $ticket->id, 'ticket is created' );
+    my $id = $ticket->id;
+    is( $ticket->SLA,             2,                  'default SLA is 2' );
+    is( $ticket->StartsObj->Unix, $now,               'Starts' );
+    is( $ticket->DueObj->Unix,    $now + 60 * 60 * 2, 'Due' );
+}
+
+{
+    $m->goto_create_ticket( $queue->id );
+    my $form = $m->form_name( 'TicketCreate' );
+    $m->submit_form( fields => { Subject => 'ticket foo with default sla', SLA => 4 } );
+
+    my $ticket = RT::Test->last_ticket;
+    ok( $ticket->id, 'ticket is created' );
+    my $id = $ticket->id;
+    is( $ticket->SLA,             4,                  'SLA is set to 4' );
+    is( $ticket->StartsObj->Unix, $now,               'Starts' );
+    is( $ticket->DueObj->Unix,    $now + 60 * 60 * 4, 'Due' );
+    $m->follow_link_ok( { text => 'Basics' }, 'Ticket -> Basics' );
+    $m->submit_form(
+        form_name => 'TicketModify',
+        fields    => { SLA => 2 },
+    );
+    $ticket->Load( $id );
+    is( $ticket->SLA, 2, 'SLA is set to 2' );
+    is( $ticket->DueObj->Unix, $now + 60 * 60 * 2, 'Due is updated accordingly' );
+}
+
+{
+    $m->get_ok( $baseurl . '/Admin/Queues/Modify.html?id=' . $queue->id );
+    my $form = $m->form_name( 'ModifyQueue' );
+    $m->untick( 'SLAEnabled', 1 );
+    $m->submit;
+    $m->text_contains( q{SLADisabled changed from (no value) to "1"} );
+}
+
+{
+
+    $m->goto_create_ticket( $queue->id );
+    my $form = $m->form_name( 'TicketCreate' );
+    ok( !$form->find_input( 'SLA' ), 'no SLA input' );
+    $m->submit_form( fields => { Subject => 'ticket foo without sla' } );
+
+    my $ticket = RT::Test->last_ticket;
+    ok( $ticket->id,               'ticket is created' );
+    ok( !$ticket->SLA,             'no SLA' );
+    ok( !$ticket->StartsObj->Unix, 'no Starts' );
+    ok( !$ticket->DueObj->Unix,    'no Due' );
+}
+
+undef $m;
+done_testing();

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


More information about the rt-commit mailing list