package FS::Cron::rt_tasks;
use strict;
-use vars qw( @ISA @EXPORT_OK $DEBUG );
+use vars qw( @ISA @EXPORT_OK $DEBUG $conf );
use Exporter;
use FS::UID qw( dbh driver_name );
use FS::Record qw(qsearch qsearchs);
use Date::Parse qw(str2time);
@ISA = qw( Exporter );
-@EXPORT_OK = qw ( rt_escalate );
+@EXPORT_OK = qw ( rt_daily );
$DEBUG = 0;
+FS::UID->install_callback( sub {
+ eval "use FS::Conf;";
+ die $@ if $@;
+ $conf = FS::Conf->new;
+});
+
+
my %void = ();
-sub rt_escalate {
+sub rt_daily {
my %opt = @_;
+ my @custnums = @ARGV; # ick
+
# RT_External installations should have their own cron scripts for this
my $system = $FS::TicketSystem::system;
return if $system ne 'RT_Internal';
- my $conf = new FS::Conf;
- return if !$conf->exists('ticket_system-escalation');
-
FS::TicketSystem->init;
$DEBUG = 1 if $opt{'v'};
RT::Config->Set( LogToScreen => 'debug' ) if $DEBUG;
-
- #we're at now now (and later).
- my $time = $opt{'d'} ? str2time($opt{'d'}) : $^T;
- $time += $opt{'y'} * 86400 if $opt{'y'};
- my $error = '';
+
+ # if -d or -y is in use, bail out. There's no reliable way to tell RT
+ # to use an alternate system time.
+ if ( $opt{'d'} or $opt{'y'} ) {
+ warn "Forced date options in use - RT daily tasks skipped.\n";
+ return;
+ }
my $session = FS::TicketSystem->session();
my $CurrentUser = $session->{'CurrentUser'}
or die "Failed to create RT session";
-
+
# load some modules that aren't handled in FS::TicketSystem
foreach (qw(
- Search::ActiveTicketsInQueue
+ Search::ActiveTicketsInQueue
Action::EscalatePriority
Action::EscalateQueue
+ Action::ScheduledResolve
)) {
eval "use RT::$_";
die $@ if $@;
}
# adapted from rt-crontool
- # Mechanics:
- # We're using EscalatePriority, so search in all queues that have a
- # priority range defined. Select all active tickets in those queues and
- # EscalatePriority, then EscalateQueue them.
# to make some actions work without complaining
%void = map { $_ => "RT::$_"->new($CurrentUser) }
(qw(Scrip ScripAction));
- # Most of this stuff is common to any condition -> action processing
- # we might want to do, but escalation is the only one we do now.
+ # compile actions to be run
+ my (@actions, @active_tickets);
my $queues = RT::Queues->new($CurrentUser);
$queues->UnLimit;
- my @actions = ();
- my @active_tickets = ();
while (my $queue = $queues->Next) {
- if ( $queue->InitialPriority == $queue->FinalPriority ) {
- warn "Queue '".$queue->Name."' (skipped)\n" if $DEBUG;
- next;
- }
warn "Queue '".$queue->Name."'\n" if $DEBUG;
+ my $CurrentUser = $queue->CurrentUser;
+ my %opt = @_;
my $tickets = RT::Tickets->new($CurrentUser);
my $search = RT::Search::ActiveTicketsInQueue->new(
TicketsObj => $tickets,
- Argument => $queue->Name,
+ Argument => $queue->Id,
CurrentUser => $CurrentUser,
);
$search->Prepare;
+ foreach my $custnum ( @custnums ) {
+ die "invalid custnum passed to rt_daily: $custnum"
+ if !$custnum =~ /^\d+$/;
+ $tickets->LimitMemberOf(
+ "freeside://freeside/cust_main/$custnum",
+ ENTRYAGGREGATOR => 'OR',
+ SUBCLAUSE => 'custnum'
+ );
+ }
while (my $ticket = $tickets->Next) {
warn 'Ticket #'.$ticket->Id()."\n" if $DEBUG;
- my @a = (
- action($ticket, 'EscalatePriority', "CurrentTime:$time"),
- action($ticket, 'EscalateQueue')
- );
- next if !@a;
+ my @a = task_actions($ticket);
push @actions, @a;
- push @active_tickets, $ticket; # avoid RT's overzealous garbage collector
+ push @active_tickets, $ticket if @a; # avoid garbage collection
}
}
+
+ # and then commit them all
foreach (grep {$_} @actions) {
my ($val, $msg) = $_->Commit;
if ( $DEBUG ) {
return;
}
+sub task_actions {
+ my $ticket = shift;
+ (
+ ### escalation ###
+ $conf->exists('ticket_system-escalation') ? (
+ action($ticket, 'EscalatePriority', "CurrentTime: $^T"),
+ action($ticket, 'EscalateQueue')
+ ) : (),
+
+ ### scheduled resolve ###
+ action($ticket, 'ScheduledResolve'),
+ );
+}
+
sub action {
my $ticket = shift;
my $CurrentUser = $ticket->CurrentUser;
use vars qw( $conf $system $AUTOLOAD );
use FS::Conf;
use FS::UID qw( dbh driver_name );
+use FS::Record qw( dbdef );
FS::UID->install_callback( sub {
$conf = new FS::Conf;
$self->$sub(@_);
}
+# Our schema changes
+my %columns = (
+ Tickets => {
+ WillResolve => { type => 'timestamp', null => 1, default => '', },
+ },
+ CustomFields => {
+ Required => { type => 'integer', default => 0, null => 0 },
+ },
+);
+
+sub _upgrade_schema {
+ my $system = FS::Conf->new->config('ticket_system');
+ return if !defined($system) || $system ne 'RT_Internal';
+ my ($class, %opts) = @_;
+
+ my $dbh = dbh;
+ my @sql;
+ my $case = driver_name eq 'mysql' ? sub {@_} : sub {map lc, @_};
+ foreach my $tablename (keys %columns) {
+ my $table = dbdef->table(&$case($tablename));
+ if ( !$table ) {
+ warn
+ "$tablename table does not exist. Your RT installation is incomplete.\n";
+ next;
+ }
+ foreach my $colname (keys %{ $columns{$tablename} }) {
+ if ( !$table->column(&$case($colname)) ) {
+ my $col = new DBIx::DBSchema::Column {
+ table_obj => $table,
+ name => &$case($colname),
+ %{ $columns{$tablename}->{$colname} }
+ };
+ $col->table_obj($table);
+ push @sql, $col->sql_add_column($dbh);
+ }
+ } #foreach $colname
+ } #foreach $tablename
+
+ return if !@sql;
+ warn "Upgrading RT schema:\n";
+ foreach my $statement (@sql) {
+ warn "$statement\n";
+ $dbh->do( $statement )
+ or die "Error: ". $dbh->errstr. "\n executing: $statement";
+ }
+ return;
+}
+
sub _upgrade_data {
return if !defined($system) || $system ne 'RT_Internal';
my ($class, %opts) = @_;
#fix classnum character(1)
'cust_bill_pkg_detail' => [],
+ #add necessary columns to RT schema
+ 'TicketSystem' => [],
;
backup();
#same
-use FS::Cron::rt_tasks qw(rt_escalate);
-rt_escalate(%opt);
+use FS::Cron::rt_tasks qw(rt_daily);
+rt_daily(%opt);
my $deldir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/";
unlink <${deldir}.invoice*>;
}
}
-# RT required field flag
-# for consistency with RT schema: mysql is in CamelCase,
-# pg is in lowercase, and they use different data types.
-my ($t, $creq, $cdis) =
- map { driver_name =~ /^mysql/i ? $_ : lc($_) }
- ('CustomFields','Required','Disabled');
-
-if ( dbdef->table($t) &&
- ! dbdef->table($t)->column($creq) ) {
- push @bugfix,
- "ALTER TABLE $t ADD COLUMN $creq ".
- dbdef->table($t)->column($cdis)->type .
- ' NOT NULL DEFAULT 0';
-}
-
if ( $DRY_RUN ) {
print
join(";\n", @bugfix ). ";\n";
ExecModule => 'SetPriority',
Argument => '',
},
+ { Name => 'Cancel Scheduled Resolve',
+ Description => 'Set ticket not to resolve in the future',
+ ExecModule => 'SetWillResolve',
+ Argument => '',
+ },
+ { Name => 'Scheduled Resolve',
+ Description => 'Resolve ticket if its WillResolve date has passed',
+ ExecModule => 'ScheduledResolve',
+ Argument => '',
+ },
);
@ScripConditions = (
ScripCondition => 'On Transaction',
ScripAction => 'Extract Subject Tag',
Template => 'Blank' },
+ { Description => 'On Correspond, cancel future resolve',
+ ScripCondition => 'On Correspond',
+ ScripAction => 'Cancel Scheduled Resolve',
+ Template => 'Blank' },
);
@ACL = (
Due TIMESTAMP NULL ,
Resolved TIMESTAMP NULL ,
+ WillResolve TIMESTAMP NULL ,
LastUpdatedBy integer NOT NULL DEFAULT 0 ,
LastUpdated TIMESTAMP NULL ,
Due DATETIME NULL ,
Resolved DATETIME NULL ,
+ WillResolve DATETIME NULL ,
LastUpdatedBy integer NOT NULL DEFAULT 0 ,
LastUpdated DATETIME NULL ,
--- /dev/null
+package RT::Action::ScheduledResolve;
+
+use strict;
+use warnings;
+
+use base qw(RT::Action);
+
+=head1 DESCRIPTION
+
+If the ticket's WillResolve date is in the past, set its status to resolved.
+
+=cut
+
+sub Prepare {
+ my $self = shift;
+
+ return undef if grep { $self->TicketObj->Status eq $_ } (
+ 'resolved',
+ 'rejected',
+ 'deleted'
+ ); # don't resolve from any of these states.
+ my $time = $self->TicketObj->WillResolveObj->Unix;
+ return ( $time > 0 and $time < time() );
+}
+
+sub Commit {
+ my $self = shift;
+
+ my $never = RT::Date->new($self->CurrentUser);
+ $never->Unix(-1);
+ $self->TicketObj->SetWillResolve($never->ISO);
+ $self->TicketObj->SetStatus('resolved');
+}
+
+RT::Base->_ImportOverlays();
+
+1;
--- /dev/null
+package RT::Action::SetWillResolve;
+use base 'RT::Action';
+
+use strict;
+
+sub Describe {
+ my $self = shift;
+ return (ref $self ." will set a ticket's future resolve date to the argument.");
+}
+
+sub Prepare {
+ return 1;
+}
+
+sub Commit {
+ my $self = shift;
+ my $DateObj = RT::Date->new( $self->CurrentUser );
+ $DateObj->Set(
+ Format => 'unknown',
+ Value => $self->Argument,
+ );
+ $self->TicketObj->SetWillResolve( $DateObj->ISO );
+}
+
+RT::Base->_ImportOverlays();
+
+1;
push( @results, $msg );
}
- # }}}
+ return (@results);
+}
+
+=head2 ProcessTicketDates (TicketObj => RT::Ticket, ARGSRef => {})
+
+Process updates to the Starts, Started, Told, Resolved, and WillResolve
+fields.
+
+=cut
+sub ProcessTicketDates {
+ my %args = (
+ TicketObj => undef,
+ ARGSRef => undef,
+ @_
+ );
+
+ my $Ticket = $args{'TicketObj'};
+ my $ARGSRef = $args{'ARGSRef'};
+
+ my (@results);
+
+ # {{{ Set date fields
+ my @date_fields = qw(
+ Told
+ Resolved
+ Starts
+ Started
+ Due
+ WillResolve
+ );
+
+ #Run through each field in this list. update the value if apropriate
+ foreach my $field (@date_fields) {
+ next unless exists $ARGSRef->{ $field . '_Date' };
+ next if $ARGSRef->{ $field . '_Date' } eq '';
+
+ my ( $code, $msg );
+
+ my $DateObj = RT::Date->new( $session{'CurrentUser'} );
+ $DateObj->Set(
+ Format => 'unknown',
+ Value => $ARGSRef->{ $field . '_Date' }
+ );
+
+ my $obj = $field . "Obj";
+ if ( ( defined $DateObj->Unix )
+ and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) )
+ {
+ my $method = "Set$field";
+ my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
+ push @results, "$msg";
+ }
+ }
+
+ # }}}
return (@results);
}
return @results;
}
+# Declare the 'WillResolve' field
+sub _VendorAccessible {
+ {
+ WillResolve =>
+ {read => 1, write => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
+ },
+};
+
+sub WillResolveObj {
+ my $self = shift;
+
+ my $time = new RT::Date( $self->CurrentUser );
+
+ if ( my $willresolve = $self->WillResolve ) {
+ $time->Set( Format => 'sql', Value => $willresolve );
+ }
+ else {
+ $time->Set( Format => 'unix', Value => -1 );
+ }
+
+ return $time;
+}
+
+sub WillResolveAsString {
+ my $self = shift;
+ return $self->WillResolveObj->AsString();
+}
+
+
1;
WatcherGroup => [ 'MEMBERSHIPFIELD', ], #loc_left_pair
HasAttribute => [ 'HASATTRIBUTE', 1 ],
HasNoAttribute => [ 'HASATTRIBUTE', 0 ],
+ #freeside
Agentnum => [ 'FREESIDEFIELD', ],
Classnum => [ 'FREESIDEFIELD', ],
Tagnum => [ 'FREESIDEFIELD', 'cust_tag' ],
+ WillResolve => [ 'DATE' => 'WillResolve', ], #loc_left_pair
);
our %SEARCHABLE_SUBFIELDS = (
--- /dev/null
+package RT::Transaction;
+use strict;
+use vars qw(%_BriefDescriptions);
+
+$_BriefDescriptions{'Set'} = sub {
+ my $self = shift;
+ if ( $self->Field eq 'Password' ) {
+ return $self->loc('Password changed');
+ }
+ elsif ( $self->Field eq 'Queue' ) {
+ my $q1 = new RT::Queue( $self->CurrentUser );
+ $q1->Load( $self->OldValue );
+ my $q2 = new RT::Queue( $self->CurrentUser );
+ $q2->Load( $self->NewValue );
+ return $self->loc("[_1] changed from [_2] to [_3]",
+ $self->loc($self->Field) , $q1->Name , $q2->Name);
+ }
+
+ # Write the date/time change at local time:
+ elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) {
+ my $t1 = new RT::Date($self->CurrentUser);
+ $t1->Set(Format => 'ISO', Value => $self->NewValue);
+ my $t2 = new RT::Date($self->CurrentUser);
+ $t2->Set(Format => 'ISO', Value => $self->OldValue);
+ return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
+ }
+ else {
+ return $self->loc( "[_1] changed from [_2] to [_3]",
+ $self->loc($self->Field),
+ ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
+ }
+};
+
+1;
+
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<select name="<%$Name%>">
+% my $onchange_attr = $onchange ? " onchange=\"$onchange\"" : '';
+<select id="<%$Name%>" name="<%$Name%>"<% $onchange_attr |n%>>
%if ($DefaultValue) {
<option value=""<% !$Default && qq[ selected="selected"] |n %>><%$DefaultLabel%></option>
%}
$SkipDeleted => 0
$DefaultValue => 1
$DefaultLabel => "-"
+$onchange => ''
</%ARGS>
<& /Elements/SelectDate, menu_prefix => 'Due', current => 0 &> (<% $TicketObj->DueObj->AsString %>)
</td>
</tr>
+ <tr>
+ <td class="label"><&|/l&>Close After</&>:</td>
+ <td class="entry">
+ <& /Elements/SelectDate, menu_prefix => 'WillResolve', current => 0 &> (<% $TicketObj->WillResolveObj->AsString %>)
+ </td>
+ </tr>
% $m->callback( %ARGS, CallbackName => 'EndOfList', Ticket => $TicketObj );
</table>
<%ARGS>
<td class="label date resolved"><&|/l&>Closed</&>:</td>
<td class="value date resolved"><% $Ticket->ResolvedObj->AsString %></td>
</tr>
+% my $willresolve = $Ticket->WillResolveObj;
+% if ( $willresolve && $willresolve->Unix > 0 ) {
+ <tr>
+ <td class="label date willresolve"><&|/l&>Will Resolve</&>:</td>
+ <td class="value date willresolve"><% $willresolve->AsString %></td>
+ </tr>
+% } # else don't display either of them
<tr>
<td class="label date updated"><&|/l&>Updated</&>:</td>
% my $UpdatedString = $Ticket->LastUpdated ? loc("[_1] by [_2]", $Ticket->LastUpdatedAsString, $m->scomp('/Elements/ShowUser', User => $Ticket->LastUpdatedByObj)) : loc("Never");
<tr><td valign="baseline" class="label"><&|/l&>Status</&>:</td>
<td valign="baseline">
-<& /Elements/SelectStatus, Name=>"Status", DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)), Default => $ARGS{'Status'} || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus)&>
+<script type="text/javascript">
+function changeStatus() {
+ var Status_select = document.getElementById('Status');
+ var x = Status_select.options[Status_select.selectedIndex].value;
+ var text = document.getElementById('WillResolve_Date');
+ var button = document.getElementById('WillResolve_Date_date_button');
+ if (x == 'resolved' || x == 'rejected' || x == 'deleted') {
+ text.disabled = true;
+ button.style.display = 'none';
+ }
+ else {
+ text.disabled = false;
+ button.style.display = 'inline';
+ }
+}
+</script>
+<& /Elements/SelectStatus,
+ Name=>"Status",
+ DefaultLabel => loc("[_1] (Unchanged)", loc($TicketObj->Status)),
+ Default => $ARGS{'Status'}
+ || ($TicketObj->Status eq $DefaultStatus ? undef : $DefaultStatus,
+ onchange => 'changeStatus()'
+)&>
<span class="label"><&|/l&>Owner</&>:</span>
<& /Elements/SelectOwner,
Name => "Owner",
DefaultLabel => loc("[_1] (Unchanged)", $m->scomp('/Elements/ShowUser', User => $TicketObj->OwnerObj)),
Default => $ARGS{'Owner'}
&>
+<span class="label"><&|/l&>Close this Ticket on</&>:</span>
+<& /Elements/SelectDate,
+ menu_prefix => 'WillResolve',
+ current => 0,
+ ShowTime => 0,
+&>
+% if ( $TicketObj->WillResolve ) {
+<span class="label"> (<% $TicketObj->WillResolveObj->AsString %>)</span>
+% }
+<script type="text/javascript">
+changeStatus();
+</script>
</td>
<td rowspan=4 valign="top">
<table style="float:right;">