RT#38973: Bill for time worked on ticket resolution [fully functional]
[freeside.git] / FS / FS / TicketSystem / RT_Internal.pm
index d96e5f0..99e7044 100644 (file)
@@ -3,6 +3,7 @@ package FS::TicketSystem::RT_Internal;
 use strict;
 use vars qw( @ISA $DEBUG $me );
 use Data::Dumper;
+use Date::Format qw( time2str );
 use MIME::Entity;
 use FS::UID qw(dbh);
 use FS::CGI qw(popurl);
@@ -50,7 +51,7 @@ sub access_right {
 sub session {
   my( $self, $session ) = @_;
 
-  if ( $session && $session->{'Current_User'} ) { # does this even work?
+  if ( $session && $session->{'CurrentUser'} ) { # does this even work?
     warn "$me session: using existing session and CurrentUser: \n".
          Dumper($session->{'CurrentUser'})
       if $DEBUG;
@@ -92,6 +93,7 @@ sub init {
   # this needs to be done on each fork
   warn "$me init: initializing RT\n" if $DEBUG;
   {
+    local $SIG{__WARN__};
     local $SIG{__DIE__};
     eval 'RT::Init("NoSignalHandlers"=>1);';
   }
@@ -100,17 +102,46 @@ sub init {
   warn "$me init: complete" if $DEBUG;
 }
 
-=item customer_tickets CUSTNUM [ LIMIT ] [ PRIORITYVALUE ]
+=item customer_tickets CUSTNUM [ PARAMS ]
 
 Replacement for the one in RT_External so that we can access custom fields 
-properly.
+properly.  Accepts a hashref with the following parameters:
+
+number - custnum/svcnum
+
+limit 
+
+priority 
+
+status
+
+queueid
+
+resolved - only return tickets resolved after this timestamp
 
 =cut
 
-sub _customer_tickets_search {
-  my ( $self, $custnum, $limit, $priority ) = @_;
+# create an RT::Tickets object for a specified custnum or svcnum
+
+sub _tickets_search {
+  my $self = shift;
+  my $type = shift;
+
+  my( $number, $limit, $priority, $status, $queueid, $opt );
+  if ( ref($_[0]) eq 'HASH' ) {
+    $opt = shift;
+    $number   = $$opt{'number'};
+    $limit    = $$opt{'limit'};
+    $priority = $$opt{'priority'};
+    $status   = $$opt{'status'};
+    $queueid  = $$opt{'queueid'};
+  } else {
+    ( $number, $limit, $priority, $status, $queueid ) = @_;
+    $opt = {};
+  }
 
-  $custnum =~ /^\d+$/ or die "invalid custnum: $custnum";
+  $type =~ /^Customer|Service$/ or die "invalid type: $type";
+  $number =~ /^\d+$/ or die "invalid custnum/svcnum: $number";
   $limit =~ /^\d+$/ or die "invalid limit: $limit";
 
   my $session = $self->session();
@@ -119,7 +150,8 @@ sub _customer_tickets_search {
 
   my $Tickets = RT::Tickets->new($CurrentUser);
 
-  my $rtql = "MemberOf = 'freeside://freeside/cust_main/$custnum'";
+  # "Customer.number" searches tickets linked via cust_svc also
+  my $rtql = "$type.number = $number";
 
   if ( defined( $priority ) ) {
     my $custom_priority = FS::Conf->new->config('ticket_system-custom_priority_field');
@@ -131,9 +163,34 @@ sub _customer_tickets_search {
     }
   }
 
-  $rtql .= ' AND ( ' .
-           join(' OR ', map { "Status = '$_'" } $self->statuses) .
-           ' )';
+  my @statuses;
+  if ( defined($status) && $status ) {
+    if ( ref($status) ) {
+      if ( ref($status) eq 'HASH' ) {
+        @statuses = grep $status->{$_}, keys %$status;
+      } elsif ( ref($status) eq 'ARRAY' ) {
+        @statuses = @$status;
+      } else {
+        #what should be the failure mode here?  die?  return no tickets?
+        die 'unknown status ref '. ref($status);
+      }
+    } else {
+      @statuses = ( $status );
+    }
+    @statuses = grep /^\w+$/, @statuses; #injection prevention
+  } else {
+    @statuses = $self->statuses;
+  }
+
+  $rtql .= ' AND ( '.
+                      join(' OR ', map { "Status = '$_'" } @statuses).
+               ' ) ';
+
+  $rtql .= " AND Queue = $queueid " if $queueid;
+
+  if ($$opt{'resolved'}) {
+    $rtql .= " AND Resolved >= " . dbh->quote(time2str('%Y-%m-%d %H:%M:%S',$$opt{'resolved'}));
+  }
 
   warn "$me _customer_tickets_search:\n$rtql\n" if $DEBUG;
   $Tickets->FromSQL($rtql);
@@ -144,8 +201,25 @@ sub _customer_tickets_search {
   return $Tickets;
 }
 
+sub href_customer_tickets {
+  my ($self, $custnum) = (shift, shift);
+  if ($custnum =~ /^(\d+)$/) {
+    return $self->href_search_tickets("Customer.number = $custnum", @_);
+  }
+  warn "bad custnum $custnum"; '';
+}
+
+sub href_service_tickets {
+  my ($self, $svcnum) = (shift, shift);
+  if ($svcnum =~ /^(\d+)$/ ) {
+    return $self->href_search_tickets("Service.number = $svcnum", @_);
+  }
+  warn "bad svcnum $svcnum"; '';
+}
+
 sub customer_tickets {
-  my $Tickets = _customer_tickets_search(@_);
+  my $self = shift;
+  my $Tickets = $self->_tickets_search('Customer', @_);
 
   my $conf = FS::Conf->new;
   my $priority_order =
@@ -168,14 +242,37 @@ sub customer_tickets {
 
 sub num_customer_tickets {
   my ( $self, $custnum, $priority ) = @_;
-  my $Tickets = $self->_customer_tickets_search($custnum, 0, $priority);
-  return $Tickets->CountAll;
+  $self->_tickets_search('Customer', $custnum, 0, $priority)->CountAll;
+}
+
+sub service_tickets  {
+  my $self = shift;
+  my $Tickets = $self->_tickets_search('Service', @_);
+
+  my $conf = FS::Conf->new;
+  my $priority_order =
+    $conf->exists('ticket_system-priority_reverse') ? 'ASC' : 'DESC';
+
+  my @order_by = (
+    { FIELD => 'Priority', ORDER => $priority_order },
+    { FIELD => 'Id',       ORDER => 'DESC' },
+  );
+
+  $Tickets->OrderByCols(@order_by);
+
+  my @tickets;
+  while ( my $t = $Tickets->Next ) {
+    push @tickets, _ticket_info($t);
+  }
+
+  return \@tickets;
 }
 
 sub _ticket_info {
   # Takes an RT::Ticket; returns a hashref of the ticket's fields, including 
   # custom fields.  Also returns custom and selfservice priority values as 
-  # _custom_priority and _selfservice_priority.
+  # _custom_priority and _selfservice_priority, and the IsUnreplied property
+  # as is_unreplied.
   my $t = shift;
 
   my $custom_priority = 
@@ -189,7 +286,10 @@ sub _ticket_info {
   }
   $ticket_info{'owner'} = $t->OwnerObj->Name;
   $ticket_info{'queue'} = $t->QueueObj->Name;
+  $ticket_info{'_cf_sort_order'} = {};
+  my $cf_sort = 0;
   foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
+    $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
     my $name = 'CF.{'.$CF->Name.'}';
     $ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
   }
@@ -200,6 +300,13 @@ sub _ticket_info {
   if ( $ss_priority ) {
     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
   }
+  $ticket_info{'is_unreplied'} = $t->IsUnreplied;
+  my $svcnums = [ 
+    map { $_->Target =~ /cust_svc\/(\d+)/; $1 } 
+        @{ $t->Services->ItemsArrayRef }
+  ];
+  $ticket_info{'svcnums'} = $svcnums;
+
   return \%ticket_info;
 }
 
@@ -385,23 +492,21 @@ sub get_ticket_object {
   my $self = shift;
   my ($session, %opt) = @_;
   $session = $self->session(shift);
-  my $Ticket = RT::Ticket->new($session->{CurrentUser});
-  $Ticket->Load($opt{'ticket_id'});
-  return if ( !$Ticket->id );
-  my $custnum = $opt{'custnum'};
-  if ( defined($custnum) && $custnum =~ /^\d+$/ ) {
-    # probably the most efficient way to check ticket ownership
-    my $Link = RT::Link->new($session->{CurrentUser});
-    $Link->LoadByCols( LocalBase => $opt{'ticket_id'},
-                       Type      => 'MemberOf',
-                       Target    => "freeside://freeside/cust_main/$custnum",
-                     );
-    return if ( !$Link->id );
+  # use a small search here so we can check ticket ownership
+  my $query;
+  if ( $opt{'ticket_id'} =~ /^(\d+)$/ ) {
+    $query = "id = $1";
+  } else {
+    return;
   }
-  return $Ticket;
+  if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
+    $query .= " AND Customer.number = $1"; # also checks ownership via services
+  }
+  my $Tickets = RT::Tickets->new($session->{CurrentUser});
+  $Tickets->FromSQL($query);
+  return $Tickets->First;
 }
 
-
 =item correspond_ticket SESSION_HASHREF, OPTION => VALUE ...
 
 Class method. Correspond on a ticket. If there is an error, returns the scalar
@@ -503,7 +608,7 @@ sub _web_external_auth {
 
           # now get user specific information, to better create our user.
           my $new_user_info
-              = RT::Interface::Web::WebExternalAutoInfo($user);
+              = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
 
           # set the attributes that have been defined.
           # FIXME: this is a horrible kludge. I'm sure there's something cleaner
@@ -539,7 +644,7 @@ sub _web_external_auth {
          # we failed to successfully create the user. abort abort abort.
           delete $session->{'CurrentUser'};
 
-          die "can't auto-create RT user"; #an error message would be nice :/
+          die "can't auto-create RT user: $msg"; #an error message would be nice :/
           #$m->abort() unless $RT::WebFallbackToInternalAuth;
           #$m->comp( '/Elements/Login', %ARGS,
           #    Error => loc( 'Cannot create user: [_1]', $msg ) );
@@ -578,5 +683,49 @@ sub selfservice_priority {
   }
 }
 
+=item custom_fields
+
+Returns a hash of custom field names and descriptions.
+
+Accepts the following options:
+
+lookuptype - limit results to this lookuptype
+
+valuetype - limit results to this valuetype
+
+Fields must be visible to CurrentUser.
+
+=cut
+
+sub custom_fields {
+  my $self = shift;
+  my %opt = @_;
+  my $lookuptype = $opt{lookuptype};
+  my $valuetype = $opt{valuetype};
+
+  my $CurrentUser = RT::CurrentUser->new();
+  $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
+  die "RT not configured" unless $CurrentUser->id;
+  my $CFs = RT::CustomFields->new($CurrentUser);
+
+  $CFs->UnLimit;
+
+  $CFs->Limit(FIELD => 'LookupType',
+              OPERATOR => 'ENDSWITH',
+              VALUE => $lookuptype)
+      if $lookuptype;
+
+  $CFs->Limit(FIELD => 'Type',
+              VALUE => $valuetype)
+      if $valuetype;
+
+  my @fields;
+  while (my $CF = $CFs->Next) {
+    push @fields, $CF->Name, ($CF->Description || $CF->Name);
+  }
+
+  return @fields;
+}
+
 1;