link tickets to services, #17067
authorMark Wells <mark@freeside.biz>
Tue, 17 Apr 2012 22:52:14 +0000 (15:52 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 17 Apr 2012 22:52:14 +0000 (15:52 -0700)
27 files changed:
FS/FS/TicketSystem/RT_External.pm
FS/FS/TicketSystem/RT_Internal.pm
FS/FS/cust_svc.pm
FS/FS/svc_phone.pm
httemplate/elements/form-create_ticket.html [new file with mode: 0644]
httemplate/elements/table-tickets.html [new file with mode: 0644]
httemplate/view/cust_main/tickets.html
httemplate/view/elements/svc_Common.html
httemplate/view/svc_acct.cgi
httemplate/view/svc_domain.cgi
httemplate/view/svc_external.cgi
httemplate/view/svc_forward.cgi
httemplate/view/svc_www.cgi
rt/lib/RT/Interface/Web_Vendor.pm
rt/lib/RT/Record.pm
rt/lib/RT/Tickets_Overlay.pm
rt/lib/RT/URI/freeside/Internal.pm
rt/share/html/Elements/CustomerFields
rt/share/html/Elements/RT__Ticket/ColumnMap
rt/share/html/Elements/ServiceFields [new file with mode: 0644]
rt/share/html/Search/Elements/BuildFormatString
rt/share/html/Search/Elements/PickBasics
rt/share/html/Search/Elements/PickCriteria
rt/share/html/Search/Elements/PickCustomerFields [new file with mode: 0644]
rt/share/html/Ticket/Elements/AddCustomers
rt/share/html/Ticket/Elements/EditCustomers
rt/share/html/Ticket/Elements/ShowCustomers

index f976ac0..22d2472 100644 (file)
@@ -97,6 +97,11 @@ sub customer_tickets {
 
 }
 
+sub service_tickets {
+  warn "service_tickets not available with RT_External.\n";
+  return;
+}
+
 sub comments_on_tickets {
   my ($self, $custnum, $limit, $time ) = @_;
   $limit ||= 0;
@@ -206,7 +211,20 @@ sub statuses {
 }
 
 sub href_customer_tickets {
-  my( $self, $custnum ) = ( shift, shift );
+  my($self, $custnum) = (shift, shift);
+  if ( $custnum =~ /^(\d+)$/ ) {
+    return $self->href_search_tickets("MemberOf = 'freeside://freeside/cust_main/$1'");
+  }
+  warn "bad custnum $custnum"; return '';
+}
+
+sub href_service_tickets {
+  warn "service_tickets not available with RT_External.\n";
+  '';
+}
+
+sub href_search_tickets {
+  my( $self, $where ) = ( shift, shift );
   my( $priority, @statuses);
   if ( ref($_[0]) ) {
     my $opt = shift;
@@ -225,8 +243,8 @@ sub href_customer_tickets {
   #$href .= 
   my $href = 
     "Search/Results.html?Order=ASC&".
-    "Query= MemberOf = 'freeside://freeside/cust_main/$custnum' ".
-    #" AND ( Status = 'open'  OR Status = 'new'  OR Status = 'stalled' )"
+    "Query= $where" .
+    #MemberOf = 'freeside://freeside/cust_main/$custnum' ".
     " AND ( ". join(' OR ', map "Status = '$_'", @statuses ). " ) "
   ;
 
@@ -274,15 +292,19 @@ sub href_customer_tickets {
 }
 
 sub href_params_new_ticket {
-  my( $self, $custnum_or_cust_main, $requestors ) = @_;
-
-  my( $custnum, $cust_main );
-  if ( ref($custnum_or_cust_main) ) {
-    $cust_main = $custnum_or_cust_main;
-    $custnum = $cust_main->custnum;
-  } else {
-    $custnum = $custnum_or_cust_main;
-    $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+  # my( $self, $custnum_or_cust_main, $requestors ) = @_;
+  # no longer takes $custnum--it must be an object
+  my ( $self, $object, $requestors ) = @_;
+  my $cust_main; # for default requestors
+  if ( $object->isa('FS::cust_main') ) {
+    $cust_main = $object;
+  }
+  elsif ( $object->isa('FS::svc_Common') ) {
+    $object = $object->cust_svc;
+    $cust_main = $object->cust_pkg->cust_main if ( $object->cust_pkg );
+  }
+  elsif ( $object->isa('FS::cust_svc') ) {
+    $cust_main = $object->cust_pkg->cust_main if ( $object->cust_pkg );
   }
 
   # explicit $requestors > config option > invoicing_list
@@ -291,9 +313,12 @@ sub href_params_new_ticket {
   $requestors = $cust_main->invoicing_list_emailonly_scalar
       if (!$requestors) and defined($cust_main);
 
+  my $subtype = $object->table;
+  my $pkey = $object->get($object->primary_key);
+
   my %param = (
     'Queue'       => ($cust_main->agent->ticketing_queueid || $default_queueid),
-    'new-MemberOf'=> "freeside://freeside/cust_main/$custnum",
+    'new-MemberOf'=> "freeside://freeside/$subtype/$pkey",
     'Requestors'  => $requestors,
   );
 
index d96e5f0..ffa8c7c 100644 (file)
@@ -107,10 +107,13 @@ properly.
 
 =cut
 
-sub _customer_tickets_search {
-  my ( $self, $custnum, $limit, $priority ) = @_;
+# create an RT::Tickets object for a specified custnum or svcnum
 
-  $custnum =~ /^\d+$/ or die "invalid custnum: $custnum";
+sub _tickets_search {
+  my ( $self, $type, $number, $limit, $priority ) = @_;
+
+  $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 +122,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');
@@ -144,8 +148,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,8 +189,30 @@ 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 {
@@ -200,6 +243,12 @@ sub _ticket_info {
   if ( $ss_priority ) {
     $ticket_info{'_selfservice_priority'} = $ticket_info{"CF.{$ss_priority}"};
   }
+  my $svcnums = [ 
+    map { $_->Target =~ /cust_svc\/(\d+)/; $1 } 
+        @{ $t->Services->ItemsArrayRef }
+  ];
+  $ticket_info{'svcnums'} = $svcnums;
+
   return \%ticket_info;
 }
 
index fc6e605..a527913 100644 (file)
@@ -756,8 +756,94 @@ sub get_session_history {
 
 }
 
+=item tickets
+
+Returns an array of hashes representing the tickets linked to this service.
+
+=cut
+
+sub tickets {
+  my $self = shift;
+
+  my $conf = FS::Conf->new;
+  my $num = $conf->config('cust_main-max_tickets') || 10;
+  my @tickets = ();
+
+  if ( $conf->config('ticket_system') ) {
+    unless ( $conf->config('ticket_system-custom_priority_field') ) {
+
+      @tickets = @{ FS::TicketSystem->service_tickets($self->svcnum, $num) };
+
+    } else {
+
+      foreach my $priority (
+        $conf->config('ticket_system-custom_priority_field-values'), ''
+      ) {
+        last if scalar(@tickets) >= $num;
+        push @tickets,
+        @{ FS::TicketSystem->service_tickets( $self->svcnum,
+            $num - scalar(@tickets),
+            $priority,
+          )
+        };
+      }
+    }
+  }
+  (@tickets);
+}
+
+
 =back
 
+=head1 SUBROUTINES
+
+=over 4
+
+=item smart_search OPTION => VALUE ...
+
+Accepts the option I<search>, the string to search for.  The string will 
+be searched for as a username, email address, IP address, MAC address, 
+phone number, and hardware serial number.  Unlike the I<smart_search> on 
+customers, this always requires an exact match.
+
+=cut
+
+# though perhaps it should be fuzzy in some cases?
+sub smart_search {
+  my %opt = @_;
+  # some false laziness w/ search/cust_svc.html
+  my $string = $opt{'search'};
+  $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
+
+  my @extra_sql = ' ( '. join(' OR ',
+    map { my $table = $_;
+      my $search_sql = "FS::$table"->search_sql($string);
+      " ( svcdb = '$table'
+      AND 0 < ( SELECT COUNT(*) FROM $table
+      WHERE $table.svcnum = cust_svc.svcnum
+      AND $search_sql
+      )
+      ) ";
+    }
+    FS::part_svc->svc_tables
+  ). ' ) ';
+  push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
+    'null_right' => 'View/link unlinked services'
+  );
+  my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
+  #for agentnum
+  my $addl_from = ' LEFT JOIN cust_pkg  USING ( pkgnum  )'.
+                  ' LEFT JOIN cust_main USING ( custnum )'.
+                  ' LEFT JOIN part_svc  USING ( svcpart )';
+
+  qsearch({
+      'table'     => 'cust_svc',
+      'addl_from' => $addl_from,
+      'hashref'   => {},
+      'extra_sql' => $extra_sql,
+  });
+}
+
 =head1 BUGS
 
 Behaviour of changing the svcpart of cust_svc records is undefined and should
index 118748e..b395ea6 100644 (file)
@@ -218,13 +218,14 @@ Class method which returns an SQL fragment to search for the given string.
 sub search_sql {
   my( $class, $string ) = @_;
 
+  my $conf = new FS::Conf;
+
   if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
     $string =~ s/\W//g;
   } else {
     $string =~ s/\D//g;
   }
 
-  my $conf = new FS::Conf;
   my $ccode = (    $conf->exists('default_phone_countrycode')
                 && $conf->config('default_phone_countrycode')
               )
diff --git a/httemplate/elements/form-create_ticket.html b/httemplate/elements/form-create_ticket.html
new file mode 100644 (file)
index 0000000..362e823
--- /dev/null
@@ -0,0 +1,38 @@
+<FORM METHOD="GET" NAME="CreateTicketForm" STYLE="display:inline">
+<SCRIPT TYPE="text/javascript">
+function updateTicketLink() {
+  var link = document.getElementById('CreateTicketLink');
+  var selector = document.getElementById('Queue')
+  link.href = "<% $new_base.'?'.
+    join(';', map(
+      { ($_ eq 'Queue') ? () : "$_=$new_param{$_}"}
+    keys %new_param),'Queue=') %>" + selector.options[selector.selectedIndex].value;
+}
+</SCRIPT>
+<A NAME="tickets"><FONT CLASS="fsinnerbox-title">Tickets</FONT></A>
+<A id="CreateTicketLink" HREF="<% $new_link %>"><% mt('Create new ticket') |h %></A>
+ <% mt('in queue') |h %>
+%# fetch list of queues in which the user can create tickets
+% my %queues = FS::TicketSystem->queues('', 'CreateTicket');
+% if( $conf->exists('ticket_system-force_default_queueid') ) {
+<B><% $queues{$new_param{'Queue'}} %></B>
+<INPUT TYPE="hidden" NAME="Queue" VALUE="<% $new_param{'Queue'} %>">
+% }
+% else {
+<SELECT NAME="Queue" id="Queue" onchange="updateTicketLink()">
+% foreach my $queueid ( sort { $queues{$a} cmp $queues{$b} } keys %queues ) {
+    <OPTION VALUE="<% $queueid %>"
+            <% $queueid == $new_param{'Queue'} ? 'SELECTED' : '' %>
+    ><% $queues{$queueid} |h %>
+% }
+</SELECT>
+<SCRIPT DEFER TYPE="text/javascript">updateTicketLink();</SCRIPT>
+% }
+</FORM>
+<%init>
+my %opt = @_;
+my $conf = new FS::Conf;
+my $object = $opt{'object'}; # must be a cust_main, cust_svc, or svc_...
+my ($new_base, %new_param) = FS::TicketSystem->href_params_new_ticket($object);
+my $new_link = FS::TicketSystem->href_new_ticket($object);
+</%init>
diff --git a/httemplate/elements/table-tickets.html b/httemplate/elements/table-tickets.html
new file mode 100644 (file)
index 0000000..6d1a45a
--- /dev/null
@@ -0,0 +1,159 @@
+<& /elements/form-create_ticket.html, object => $object &>
+ |
+View
+<A HREF="<% $open_link %>"><% mt($openlabel) |h %></A> |
+<A HREF="<% $res_link  %>"><% mt('resolved') |h %></A>
+ <BR>
+
+<& /elements/table-grid.html &>
+% my $bgcolor1 = '#eeeeee';
+%   my $bgcolor2 = '#ffffff';
+%   my $bgcolor = '';
+
+<TR>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('#') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Subject') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Queue') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Owner') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Due') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Estimated Time') |h %></TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Priority') |h %></TH>
+% if ( $ss_priority ) {
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Customer Priority') |h %></TH>
+% }
+% if ( $object->isa('FS::cust_main') ) {
+  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Service') |h %></TH>
+% }
+</TR>
+
+% foreach my $ticket ( @tickets ) {
+%     my $href = FS::TicketSystem->href_ticket($ticket->{id});
+%     if ( $bgcolor eq $bgcolor1 ) {
+%       $bgcolor = $bgcolor2;
+%     } else {
+%       $bgcolor = $bgcolor1;
+%     }
+
+  <TR>
+  
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <A HREF=<%$href%>><% $ticket->{id} %></A>
+    </TD>
+  
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <A HREF=<%$href%>><% $ticket->{subject} %></A>
+    </TD>
+  
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{status} %>
+    </TD>
+  
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{queue} %>
+    </TD>
+  
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{owner} %>
+    </TD>
+
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $date_formatter->($ticket->{due}) %>
+    </TD>
+
+    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{timeestimated} %>
+    </TD>
+  
+    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{content}
+           ? $ticket->{content}.' ('.$ticket->{priority}.')'
+           : $ticket->{priority}
+      %>
+    </TD>
+
+%   if ( $ss_priority ) {
+    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $ticket->{"CF.{$ss_priority}"} %>
+    </TD>
+%   }
+%   if ( $object->isa('FS::cust_main') ) {
+    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><FONT SIZE=-1><B>
+%     foreach (@{ $ticket->{svcnums} }) {
+%       my $cust_svc = FS::cust_svc->by_key($_) or next;
+        <% FS::UI::Web::svc_label_link($m, $cust_svc->part_svc, $cust_svc) %>
+        <BR>
+%     }
+    </B></FONT></TD>
+%   }
+
+  </TR>
+
+% } 
+
+</TABLE>
+
+<%init>
+use Date::Parse qw(str2time);
+use Date::Format qw(time2str);
+
+my %opt = @_;
+my $conf = new FS::Conf;
+
+my $object = $opt{'object'};
+$object = $object->cust_svc if $object->isa('FS::svc_Common');
+my( @tickets )  = $object->tickets;
+
+my ($openlabel, $open_link, $res_link, $thing);
+$openlabel = join('/', FS::TicketSystem->statuses );
+
+# not the nicest way to do this--FS::has_tickets_Common?
+if ( $object->isa('FS::cust_main') ) {
+  $thing  = 'customer';
+  $open_link = FS::TicketSystem->href_customer_tickets($object->custnum);
+
+  $res_link  = FS::TicketSystem->href_customer_tickets(
+                    $object->custnum,
+                    { 'statuses' => [ 'resolved' ] }
+                  );
+}
+elsif ( $object->isa('FS::cust_svc') ) {
+  $thing = 'service';
+  $open_link = FS::TicketSystem->href_service_tickets($object->svcnum);
+
+  $res_link  = FS::TicketSystem->href_service_tickets(
+                    $object->svcnum,
+                    { 'statuses' => [ 'resolved' ] }
+                  );
+}
+
+my $ss_priority = FS::TicketSystem->selfservice_priority;
+if ( $ss_priority ) {
+  my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
+  use sort 'stable';
+  # sort in the following way:
+  @tickets = sort { 
+    # within a severity level...
+    ( $a->{'content'} eq $b->{'content'} ) ? (
+      # no-priority tickets sort last
+      (
+        ($a->{'_selfservice_priority'} eq '') <=> 
+        ($b->{'_selfservice_priority'} eq '')
+      ) ||
+      # otherwise obey ticket_system-priority_reverse
+      ( $dir * 
+        ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
+      )
+    ) : 0; # but don't rearrange between severity levels
+  } @tickets;
+}
+
+my $format = $conf->config('date_format') || '%Y-%m-%d';
+
+my $date_formatter = sub {
+  my $time = str2time($_[0], 'GMT');
+  # exclude times within 24 hours of zero
+  ($time > 86400) ? time2str($format, $time) : '';
+};
+
+</%init>
index 194e907..f076fcc 100644 (file)
@@ -1,164 +1,2 @@
-<FORM METHOD="GET" NAME="CreateTicketForm" STYLE="display:inline">
-<SCRIPT TYPE="text/javascript">
-function updateTicketLink() {
-  var link = document.getElementById('CreateTicketLink');
-  var selector = document.getElementById('Queue')
-  link.href = "<% $new_base.'?'.
-    join(';', map(
-      { ($_ eq 'Queue') ? () : "$_=$new_param{$_}"} 
-    keys %new_param),'Queue=') %>" + selector.options[selector.selectedIndex].value;
-}
-</SCRIPT>
-<A NAME="tickets"><FONT CLASS="fsinnerbox-title">Tickets</FONT></A>
-<A id="CreateTicketLink" HREF="<% $new_link %>"><% mt('Create new ticket') |h %></A>
- <% mt('in queue') |h %> 
-%# fetch list of queues in which the user can create tickets
-% my %queues = FS::TicketSystem->queues('', 'CreateTicket');
-% if( $conf->exists('ticket_system-force_default_queueid') ) {
-<B><% $queues{$new_param{'Queue'}} %></B>
-<INPUT TYPE="hidden" NAME="Queue" VALUE="<% $new_param{'Queue'} %>">
-% }
-% else {
-<SELECT NAME="Queue" id="Queue" onchange="updateTicketLink()">
-% foreach my $queueid ( sort { $queues{$a} cmp $queues{$b} } keys %queues ) {
-    <OPTION VALUE="<% $queueid %>"
-            <% $queueid == $new_param{'Queue'} ? 'SELECTED' : '' %>
-    ><% $queues{$queueid} |h %>
-% }
-</SELECT>
-<SCRIPT DEFER TYPE="text/javascript">updateTicketLink();</SCRIPT>
-% }
-</FORM>
- | 
-View 
-<A HREF="<% $open_link %>"><% mt($openlabel) |h %></A> | 
-<A HREF="<% $res_link  %>"><% mt('resolved') |h %></A>
-<BR>
-
-<& /elements/table-grid.html &>
-% my $bgcolor1 = '#eeeeee';
-%   my $bgcolor2 = '#ffffff';
-%   my $bgcolor = '';
-
-<TR>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('#') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Subject') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Status') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Queue') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Owner') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Due') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Estimated Time') |h %></TH>
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Priority') |h %></TH>
-% if ( $ss_priority ) {
-  <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Customer Priority') |h %></TH>
-% }
-</TR>
-
-% foreach my $ticket ( @tickets ) {
-%     my $href = FS::TicketSystem->href_ticket($ticket->{id});
-%     if ( $bgcolor eq $bgcolor1 ) {
-%       $bgcolor = $bgcolor2;
-%     } else {
-%       $bgcolor = $bgcolor1;
-%     }
-
-  <TR>
-  
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <A HREF=<%$href%>><% $ticket->{id} %></A>
-    </TD>
-  
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <A HREF=<%$href%>><% $ticket->{subject} %></A>
-    </TD>
-  
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{status} %>
-    </TD>
-  
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{queue} %>
-    </TD>
-  
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{owner} %>
-    </TD>
-
-    <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $date_formatter->($ticket->{due}) %>
-    </TD>
-
-    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{timeestimated} %>
-    </TD>
-  
-    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{content}
-           ? $ticket->{content}.' ('.$ticket->{priority}.')'
-           : $ticket->{priority}
-      %>
-    </TD>
-
-%   if ( $ss_priority ) {
-    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
-      <% $ticket->{"CF.{$ss_priority}"} %>
-    </TD>
-%   }
-
-  </TR>
-
-% } 
-
-</TABLE>
-
-<%init>
-use Date::Format 'time2str';
-use Date::Parse 'str2time';
-
-my( $conf ) = new FS::Conf;
-my( $cust_main ) = @_;
-my( @tickets )  = $cust_main->tickets;
-
-my $open_link = FS::TicketSystem->href_customer_tickets($cust_main->custnum);
-my $openlabel = join('/', FS::TicketSystem->statuses );
-
-my $res_link  = FS::TicketSystem->href_customer_tickets(
-                  $cust_main->custnum,
-                  { 'statuses' => [ 'resolved' ] }
-                );
-
-my( $new_base, %new_param ) = 
-  FS::TicketSystem->href_params_new_ticket( $cust_main );
-
-my $new_link = FS::TicketSystem->href_new_ticket( $cust_main );
-
-my $ss_priority = FS::TicketSystem->selfservice_priority;
-if ( $ss_priority ) {
-  my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
-  use sort 'stable';
-  # sort in the following way:
-  @tickets = sort { 
-    # within a severity level...
-    ( $a->{'content'} eq $b->{'content'} ) ? (
-      # no-priority tickets sort last
-      (
-        ($a->{'_selfservice_priority'} eq '') <=> 
-        ($b->{'_selfservice_priority'} eq '')
-      ) ||
-      # otherwise obey ticket_system-priority_reverse
-      ( $dir * 
-        ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
-      )
-    ) : 0; # but don't rearrange between severity levels
-  } @tickets;
-}
-
-my $format = $conf->config('date_format') || '%Y-%m-%d';
-
-my $date_formatter = sub {
-  my $time = str2time($_[0], 'GMT');
-  # exclude times within 24 hours of zero
-  ($time > 86400) ? time2str($format, $time) : '';
-};
-
-</%init>
+% my $cust_main = shift;
+<& /elements/table-tickets.html, object => $cust_main &>
index 3130c73..04d2b29 100644 (file)
@@ -114,6 +114,10 @@ function areyousure(href) {
 
 % }
 
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
index 441e494..de97a07 100755 (executable)
 
 <& elements/svc_export_settings.html, $svc_acct &>
 
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <& /elements/footer.html &>
index 3938a34..fcccd74 100755 (executable)
 
 <% include('elements/svc_export_settings.html', $svc_domain) %>
 
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
index 77679d8..72e5535 100644 (file)
 
 
 </TABLE></TD></TR></TABLE>
+
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <BR><% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
index 15b5ae5..2cb78eb 100755 (executable)
 
 <% include('elements/svc_export_settings.html', $svc_forward) %>
 
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
index 935d139..fbb02a0 100644 (file)
 </TABLE>
 
 <BR>
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
index ee8c34b..ae7f089 100644 (file)
@@ -76,12 +76,32 @@ sub ProcessTicketCustomers {
     ###
 
     ###
+    #find new services
+    ###
+    
+    my @svcnums = map  { /^Ticket-AddService-(\d+)$/; $1 }
+                  grep { /^Ticket-AddService-(\d+)$/ && $ARGSRef->{$_} }
+                  keys %$ARGSRef;
+
+    my @custnums;
+    foreach my $svcnum (@svcnums) {
+        my @link = ( 'Type'   => 'MemberOf',
+                     'Target' => "freeside://freeside/cust_svc/$svcnum",
+                   );
+
+        my( $val, $msg ) = $Ticket->AddLink(@link);
+        push @results, $msg;
+        next if !$val;
+
+    }
+
+    ###
     #find new customers
     ###
 
-    my @custnums = map  { /^Ticket-AddCustomer-(\d+)$/; $1 }
-                   grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
-                   keys %$ARGSRef;
+    push @custnums, map  { /^Ticket-AddCustomer-(\d+)$/; $1 }
+                    grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
+                    keys %$ARGSRef;
 
     #my @delete_custnums =
     #  map  { /^Ticket-AddCustomer-(\d+)$/; $1 }
index 121c086..41e60bd 100755 (executable)
@@ -1177,7 +1177,9 @@ sub DependsOn {
 
 =head2 Customers
 
-  This returns an RT::Links object which references all the customers that this object is a member of.
+  This returns an RT::Links object which references all the customers that 
+  this object is a member of.  This includes both explicitly linked customers
+  and links implied by services.
 
 =cut
 
@@ -1189,11 +1191,16 @@ sub Customers {
 
       $self->{'Customers'} = $self->MemberOf->Clone;
 
-      $self->{'Customers'}->Limit(
-                                   FIELD    => 'Target',
-                                   OPERATOR => 'STARTSWITH',
-                                   VALUE    => 'freeside://freeside/cust_main/',
-                                 );
+      for my $fstable (qw(cust_main cust_svc)) {
+
+        $self->{'Customers'}->Limit(
+                                     FIELD    => 'Target',
+                                     OPERATOR => 'STARTSWITH',
+                                     VALUE    => "freeside://freeside/$fstable",
+                                     ENTRYAGGREGATOR => 'OR',
+                                     SUBCLAUSE => 'customers',
+                                   );
+      }
     }
 
     warn "->Customers method called on $self; returning ".
@@ -1205,6 +1212,34 @@ sub Customers {
 
 # }}}
 
+# {{{ Services
+
+=head2 Services
+
+  This returns an RT::Links object which references all the services this 
+  object is a member of.
+
+=cut
+
+sub Services {
+    my( $self, %opt ) = @_;
+
+    unless ( $self->{'Services'} ) {
+
+      $self->{'Services'} = $self->MemberOf->Clone;
+
+      $self->{'Services'}->Limit(
+                                   FIELD    => 'Target',
+                                   OPERATOR => 'STARTSWITH',
+                                   VALUE    => "freeside://freeside/cust_svc",
+                                 );
+    }
+
+    return $self->{'Services'};
+}
+
+# }}}
+
 # {{{ sub _Links 
 
 =head2 Links DIRECTION [TYPE]
index a5d37a3..0d482cd 100644 (file)
@@ -146,11 +146,8 @@ our %FIELD_METADATA = (
     HasAttribute     => [ 'HASATTRIBUTE', 1 ],
     HasNoAttribute     => [ 'HASATTRIBUTE', 0 ],
     #freeside
-    Customer         => [ 'FREESIDEFIELD', ],
-#    Agentnum         => [ 'FREESIDEFIELD', ],
-#    Classnum         => [ 'FREESIDEFIELD', ],
-#    Refnum           => [ 'FREESIDEFIELD', ],
-#    Tagnum           => [ 'FREESIDEFIELD', 'cust_tag' ],
+    Customer         => [ 'FREESIDEFIELD' => 'Customer' ],
+    Service          => [ 'FREESIDEFIELD' => 'Service' ],
     WillResolve      => [ 'DATE'            => 'WillResolve', ], #loc_left_pair
 );
 
@@ -1823,6 +1820,15 @@ sub OrderByCols {
            }
            push @res, { %$row, ALIAS => $custalias, FIELD => $cust_field };
 
+      } elsif ( $field eq 'Service' ) {
+          
+          my $svcalias = $self->JoinToService;
+          my $svc_field = lc($subkey);
+          if ( !$svc_field or $svc_field eq 'number' ) {
+              $svc_field = 'svcnum';
+          }
+          push @res, { %$row, ALIAS => $svcalias, FIELD => $svc_field };
+
        } #Freeside
 
        else {
@@ -1842,7 +1848,7 @@ sub JoinToCustLinks {
     # and an sql expression to retrieve the custnum.
     my $self = shift;
     # only join once for each RT::Tickets object
-    my $linkalias = $self->{cust_linkalias};
+    my $linkalias = $self->{cust_main_linkalias};
     if (!$linkalias) {
         $linkalias = $self->Join(
             TYPE   => 'LEFT',
@@ -1864,7 +1870,7 @@ sub JoinToCustLinks {
             OPERATOR => 'STARTSWITH',
             VALUE    => 'freeside://freeside/cust_main/',
         );
-        $self->{cust_linkalias} = $linkalias;
+        $self->{cust_main_linkalias} = $linkalias;
     }
     my $custnum_sql = "CAST(SUBSTR($linkalias.Target,31) AS ";
     if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
@@ -1890,9 +1896,79 @@ sub JoinToCustomer {
     return $custalias;
 }
 
+sub JoinToSvcLinks {
+    my $self = shift;
+    my $linkalias = $self->{cust_svc_linkalias};
+    if (!$linkalias) {
+        $linkalias = $self->Join(
+            TYPE   => 'LEFT',
+            ALIAS1 => 'main',
+            FIELD1 => 'id',
+            TABLE2 => 'Links',
+            FIELD2 => 'LocalBase',
+        );
+
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => 'MemberOf',
+        );
+        $self->SUPER::Limit(
+            LEFTJOIN => $linkalias,
+            FIELD    => 'Target',
+            OPERATOR => 'STARTSWITH',
+            VALUE    => 'freeside://freeside/cust_svc/',
+        );
+        $self->{cust_svc_linkalias} = $linkalias;
+    }
+    my $svcnum_sql = "CAST(SUBSTR($linkalias.Target,30) AS ";
+    if ( RT->Config->Get('DatabaseType') eq 'mysql' ) {
+        $svcnum_sql .= 'SIGNED INTEGER)';
+    }
+    else {
+        $svcnum_sql .= 'INTEGER)';
+    }
+    return ($linkalias, $svcnum_sql);
+}
+
+sub JoinToService {
+    my $self = shift;
+    my ($linkalias, $svcnum_sql) = $self->JoinToSvcLinks;
+    $self->Join(
+        TYPE       => 'LEFT',
+        EXPRESSION => $svcnum_sql,
+        TABLE2     => 'cust_svc',
+        FIELD2     => 'svcnum',
+    );
+}
+
+# This creates an alternate left join path to cust_main via cust_svc.
+# _FreesideFieldLimit needs to add this as a separate, independent join
+# and include all tickets that have a matching cust_main record via 
+# either path.
+sub JoinToCustomerViaService {
+    my $self = shift;
+    my $svcalias = $self->JoinToService;
+    my $cust_pkg = $self->Join(
+        TYPE      => 'LEFT',
+        ALIAS1    => $svcalias,
+        FIELD1    => 'pkgnum',
+        TABLE2    => 'cust_pkg',
+        FIELD2    => 'pkgnum',
+    );
+    my $cust_main = $self->Join(
+        TYPE      => 'LEFT',
+        ALIAS1    => $cust_pkg,
+        FIELD1    => 'custnum',
+        TABLE2    => 'cust_main',
+        FIELD2    => 'custnum',
+    );
+    $cust_main;
+}
+
 sub _FreesideFieldLimit {
     my ( $self, $field, $op, $value, %rest ) = @_;
-    my $alias = $self->JoinToCustomer;
     my $is_negative = 0;
     if ( $op eq '!=' || $op =~ /\bNOT\b/i ) {
         # if the op is negative, do the join as though
@@ -1903,40 +1979,70 @@ sub _FreesideFieldLimit {
         $op =~ s/\bNOT\b//;
     }
 
-    my $cust_field = $rest{SUBKEY} || 'custnum';
+    my (@alias, $table2, $subfield, $pkey);
+    if ( $field eq 'Customer' ) {
+      push @alias, $self->JoinToCustomer;
+      push @alias, $self->JoinToCustomerViaService;
+      $pkey = 'custnum';
+    }
+    elsif ( $field eq 'Service' ) {
+      push @alias, $self->JoinToService;
+      $pkey = 'svcnum';
+    }
+    else {
+      die "malformed Freeside query: $field";
+    }
+
+    $subfield = $rest{SUBKEY} || $pkey;
     my $table2;
     # compound subkey: separate into table name and field in that table
     # (must be linked by custnum)
-    ($table2, $cust_field) = ($1, $2) if $cust_field =~ /^(\w+)?\.(\w+)$/;
-
-    $cust_field = lc($cust_field);
-    $cust_field = 'custnum' if !$cust_field or $cust_field eq 'number';
-
-    if ( $table2 ) {
-        $alias = $self->Join(
-            TYPE        => 'LEFT',
-            ALIAS1      => $alias,
-            FIELD1      => 'custnum',
-            TABLE2      => $table2,
-            FIELD2      => 'custnum',
-        );
+    $subfield = lc($subfield);
+    ($table2, $subfield) = ($1, $2) if $subfield =~ /^(\w+)?\.(\w+)$/;
+    $subfield = $pkey if $subfield eq 'number';
+
+    # if it's compound, create a join from cust_main or cust_svc to that 
+    # table, using custnum or svcnum, and Limit on that table instead.
+    foreach my $a (@alias) {
+      if ( $table2 ) {
+          $a = $self->Join(
+              TYPE        => 'LEFT',
+              ALIAS1      => $a,
+              FIELD1      => $pkey,
+              TABLE2      => $table2,
+              FIELD2      => $pkey,
+          );
+      }
+
+      # do the actual Limit
+      $self->SUPER::Limit(
+          LEFTJOIN        => $a,
+          FIELD           => $subfield,
+          OPERATOR        => $op,
+          VALUE           => $value,
+          ENTRYAGGREGATOR => 'AND',
+          # no SUBCLAUSE needed, limits on different aliases across left joins
+          # are inherently independent
+      );
+
+      # then, since it's a left join, exclude tickets for which there is now 
+      # no matching record in the table we just limited on.  (Or where there 
+      # is a matching record, if $is_negative.)
+      # For a cust_main query (where there are two different aliases), this 
+      # will produce a subclause: "cust_main_1.custnum IS NOT NULL OR 
+      # cust_main_2.custnum IS NOT NULL" (or "IS NULL AND..." for a negative
+      # query).
+      $self->_SQLLimit(
+          %rest,
+          ALIAS           => $a,
+          FIELD           => $pkey,
+          OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
+          VALUE           => 'NULL',
+          QUOTEVALUE      => 0,
+          ENTRYAGGREGATOR => $is_negative ? 'AND' : 'OR',
+          SUBCLAUSE       => 'fs_limit',
+      );
     }
-
-    $self->SUPER::Limit(
-        LEFTJOIN        => $alias,
-        FIELD           => $cust_field,
-        OPERATOR        => $op,
-        VALUE           => $value,
-        ENTRYAGGREGATOR => 'AND',
-    );
-    $self->_SQLLimit(
-        %rest,
-        ALIAS           => $alias,
-        FIELD           => 'custnum',
-        OPERATOR        => $is_negative ? 'IS' : 'IS NOT',
-        VALUE           => 'NULL',
-        QUOTEVALUE      => 0,
-    );
 }
 
 #Freeside
index 5656a51..b5e56ee 100644 (file)
@@ -38,8 +38,14 @@ use FS::Conf;
 use FS::Record qw(qsearchs qsearch dbdef);
 use FS::cust_main;
 use FS::cust_svc;
+use FS::part_svc;
 use FS::payby;
 
+#can I do this?
+FS::UID->install_callback(
+  sub { @RT::URI::freeside::svc_tables = FS::part_svc->svc_tables() }
+);
+
 =head1 NAME
 
 RT::URI::freeside::Internal
@@ -105,7 +111,23 @@ sub FreesideGetConfig {
 
 sub smart_search { #Subroutine
 
-  return map { { $_->hash } } &FS::cust_main::Search::smart_search(@_);
+    return map { { $_->hash } } &FS::cust_main::Search::smart_search(@_);
+
+}
+
+sub service_search {
+
+    return map {
+      my $cust_pkg = $_->cust_pkg;
+      my $custnum = $cust_pkg->custnum if $cust_pkg;
+      my $label = join(': ',($_->label)[0, 1]);
+      my %hash = (
+        $_->hash,
+        'label' => $label,
+        'custnum' => $custnum, # so that it's smart_searchable...
+      );
+      \%hash
+    } &FS::cust_svc::smart_search(@_);
 
 }
 
@@ -130,10 +152,22 @@ sub _FreesideURILabelLong {
   if ( $table eq 'cust_main' ) {
 
     my $rec = $self->_FreesideGetRecord();
-    return small_custview( $rec->{'_object'},
+    return '<A HREF="' . $self->HREF . '">' .
+                        small_custview( $rec->{'_object'},
                            scalar(FS::Conf->new->config('countrydefault')),
-                           1 #nobalance
-                         );
+                           1, #nobalance
+                        ) . '</A>';
+
+  } elsif ( $table eq 'cust_svc' ) {
+
+    my $string = '';
+    my $cust = $self->CustomerResolver;
+    if ( $cust ) {
+      $string = $cust->AsStringLong;
+    }
+    $string .= '<B><A HREF="' . $self->HREF . '">' . 
+        $self->AsString . '</A></B>';
+    return $string;
 
   } else {
 
@@ -143,18 +177,36 @@ sub _FreesideURILabelLong {
 
 }
 
-# no need to have a separate wrapper method for every one of these things
+sub CustomerResolver {
+  my $self = shift;
+  if ( $self->{fstable} eq 'cust_main' ) {
+    return $self;
+  }
+  elsif ( $self->{fstable} eq 'cust_svc' ) {
+    my $rec = $self->_FreesideGetRecord();
+    return if !$rec;
+    my $cust_pkg = $rec->{'_object'}->cust_pkg;
+    if ( $cust_pkg ) {
+      my $URI = RT::URI->new($self->CurrentUser);
+      $URI->FromURI('freeside://freeside/cust_main/'.$cust_pkg->custnum);
+      return $URI->Resolver;
+    }
+  }
+  return;
+}
+
 sub CustomerInfo {
   my $self = shift;
+  $self = $self->CustomerResolver or return;
   my $rec = $self->_FreesideGetRecord() or return;
-  my $cust_main = $rec->{'_object'};
+  my $cust_main = delete $rec->{_object};
   my $agent = $cust_main->agent;
   my $class = $cust_main->cust_class;
   my $referral = qsearchs('part_referral', { refnum => $cust_main->refnum });
   my @part_tags = $cust_main->part_tag;
 
   return $self->{CustomerInfo} ||= {
-    $cust_main->hash,
+    %$rec,
 
     AgentName     => ($agent ? ($agent->agentnum.': '.$agent->agent) : ''),
     CustomerClass => ($class ? $class->classname : ''),
@@ -170,4 +222,19 @@ sub CustomerInfo {
   }
 }
 
+sub ServiceInfo {
+  my $self = shift;
+  $self->{fstable} eq 'cust_svc' or return;
+  my $rec = $self->_FreesideGetRecord() or return;
+  my $cust_svc = $rec->{'_object'};
+  my $svc_x = $cust_svc->svc_x;
+  my $part_svc = $cust_svc->part_svc;
+  return $self->{ServiceInfo} ||= {
+    $cust_svc->hash,
+    $svc_x->hash,
+    ServiceType => $part_svc->svc,
+    Label => $self->AsString,
+  }
+}
+
 1;
index 553a349..d5419d2 100644 (file)
@@ -16,7 +16,6 @@ About the keys:
   name to sort by.
 </%doc>
 <%once>
-return unless $RT::URI::freeside::IntegrationType eq 'Internal';
 
 my @customer_fields = ( # ordered
   {
@@ -158,8 +157,12 @@ sub select_table {
 
 sub ticket_cust_resolvers {
     my $Ticket = shift;
-    my @Customers = @{ $Ticket->Customers->ItemsArrayRef };
-    return map $_->TargetURI->Resolver, @Customers;
+    my @Customers = map { $_->TargetURI->Resolver->CustomerResolver }
+                      @{ $Ticket->Customers->ItemsArrayRef };
+    # this can contain cust_svc links, careful
+    # uniq
+    my %seen = map { $_->URI => $_ } @Customers;
+    values %seen;
 }
 
 sub cust_info_attribute { # the simple case of $resolver->CustomerInfo->{foo}
@@ -177,7 +180,6 @@ sub cust_info_attribute { # the simple case of $resolver->CustomerInfo->{foo}
 
 </%once>
 <%init>
-return unless $RT::URI::freeside::IntegrationType eq 'Internal';
 
 my $arg = shift;
 if ( $arg eq 'Names' ) {
@@ -198,9 +200,11 @@ elsif ( $arg eq 'ColumnMap' ) {
   grep { exists $_->{Display} }
   @customer_fields;
 }
-elsif ( $arg eq 'PickBasics' ) {
+elsif ( $arg eq 'Criteria' ) {
   return map {
     my $f = $_;
+    # argument to Search/Elements/ConditionRow
+    $f->{Condition} ||
     {
       Name  => ($f->{QueryName} || $f->{Name}),
       Field => ($f->{QueryLabel} || $f->{Label}),
@@ -208,7 +212,7 @@ elsif ( $arg eq 'PickBasics' ) {
       Value => $f->{Value},
     }
   } #map
-  grep { exists $_->{Value} }
+  grep { exists $_->{Condition} || exists $_->{Value} }
   @customer_fields;
 }
 else { die "unknown CustomerFields mode '$arg'\n"; }
index 9e6466c..35c0aad 100644 (file)
@@ -321,6 +321,7 @@ $COLUMN_MAP = {
     
     #freeside
     $m->comp('/Elements/CustomerFields', 'ColumnMap'),
+    $m->comp('/Elements/ServiceFields', 'ColumnMap'),
 };
 
 # if no GPG support, then KeyOwnerName and KeyRequestors fall back to the regular
diff --git a/rt/share/html/Elements/ServiceFields b/rt/share/html/Elements/ServiceFields
new file mode 100644 (file)
index 0000000..9c9a248
--- /dev/null
@@ -0,0 +1,161 @@
+<%doc>
+Accessible Freeside svc_x fields go in here.  RT::URI::freeside::Internal
+pulls all fields from cust_svc and the svc_x tables into ServiceInfo().
+RT::Tickets_Overlay resolves "Service.foo" as "cust_svc.foo", and 
+"Service.svc_acct.bar" as "JOIN svc_acct USING (svcnum) ... svc_acct.bar".
+
+See /Elements/CustomerFields for notes on this data structure.
+</%doc>
+<%once>
+
+my @service_fields = ( # ordered
+  {
+    # svcnum
+    Name    => 'Service',
+    Label   => 'Service',
+    Display => sub {
+                my $Ticket = shift;
+                my @return = ();
+                foreach my $s (ticket_svc_resolvers($Ticket)) {
+                    push @return, \'<A HREF="', $s->HREF, \'">',
+                                  $s->AsString,
+                                  \'</A>',
+                                  \'<BR>';
+                }
+                pop @return;
+                @return;
+              },
+    OrderBy => 'Service.Number',
+  },
+  {
+    #Column name (format string)
+    Name    => 'ServiceType',
+    # Column heading/query builder name
+    Label   => 'Service Type',
+    # Column value (coderef, cust_svc/svc_x field, or ServiceInfo key)
+    Display => 'ServiceType',
+    # Query builder options
+    # RT-SQL field, defaults to Name
+    QueryName => 'Service.svcpart',
+    Op      => equals_notequals,
+    Value   => select_table('part_svc', 'svcpart', 'svc'),
+    # RT-SQL sort key (if any)
+    OrderBy => 'Service.svcpart',
+  },
+  {
+    Name    => 'ServiceKey', # loosely corresponds to smartsearch/label field
+    Label   => '',
+    # not displayable
+    QueryLabel  => {
+      Type      => 'select',
+      Options   => [
+        'Service.svc_acct.username'       => loc('Username'),
+        'Service.svc_phone.phonenum'      => loc('Phone Number'),
+        'Service.svc_broadband.ip_addr'   => loc('IP Address'),
+        'Service.svc_broadband.mac_addr'  => loc('MAC Address'),
+      ],
+    },
+    Op      => matches_notmatches,
+    Value   => { Type => 'text', Size => 20 },
+  },
+  {
+    Name    => 'Router',
+    Label   => 'Router',
+    QueryName => 'Service.svc_broadband.routernum',
+    # not displayable
+    Op      => equals_notequals,
+    Value   => select_table('router', 'routernum', 'routername'),
+    OrderBy => 'Service.svc_broadband.routernum',
+  },
+
+);
+#helper subs
+#Op      
+sub equals_notequals {
+  {
+      Type => 'component',
+      Path => '/Elements/SelectBoolean',
+      Arguments => { TrueVal=> '=', FalseVal=> '!=' },
+  }
+}
+sub matches_notmatches {
+    {
+        Type => 'component',
+        Path => '/Elements/SelectMatch',
+    },
+}
+
+#Value
+sub select_table {
+  my ($table, $value_col, $name_col, $hashref) = @_;
+  $hashref ||= { disabled => '' }; # common case
+  return {
+    Type => 'select',
+    Options => [
+      '' => '-',
+      map { $_->$value_col, $_->$name_col }
+      qsearch($table, $hashref)
+    ],
+  }
+}
+
+sub ticket_svc_resolvers {
+    my $Ticket = shift;
+    my @Services = @{ $Ticket->Services->ItemsArrayRef };
+    return map $_->TargetURI->Resolver, @Services;
+}
+
+sub svc_info_attribute {
+    my $attribute = shift;
+    sub {
+        my $Ticket = shift;
+        my @return;
+        foreach my $s (ticket_svc_resolvers($Ticket)) {
+            push @return, $s->ServiceInfo->{$attribute}, '<BR>';
+        }
+        pop @return; #trailing <BR>
+        @return;
+    };
+}
+
+</%once>
+<%init>
+use Data::Dumper;
+#warn Dumper(\@service_fields);
+
+my $arg = shift;
+if ( $arg eq 'Names' ) {
+  return map { $_->{Name} } 
+  grep { exists $_->{Display} }
+  @service_fields;
+}
+elsif ( $arg eq 'ColumnMap' ) {
+  return map {
+    my $f = $_;
+    $f->{Name} => {
+        title     => $f->{Label},
+        attribute => $f->{OrderBy} || '',
+        value     => ref($f->{Display}) eq 'CODE' ? 
+                      $f->{Display} : 
+                      svc_info_attribute($f->{Display})
+    }
+  } #map
+  grep { exists $_->{Display} }
+  @service_fields;
+}
+elsif ( $arg eq 'Criteria' ) {
+  return map {
+    my $f = $_;
+    # argument to Search/Elements/ConditionRow
+    {
+      Name  => ($f->{QueryName} || $f->{Name}),
+      Field => ($f->{QueryLabel} || $f->{Label}),
+      Op    => $f->{Op},
+      Value => $f->{Value},
+    }
+  } #map
+  grep { exists($_->{Value}) }
+  @service_fields;
+}
+else { die "unknown ServiceFields mode '$arg'\n"; }
+</%init>
index 96e6a28..57c7679 100644 (file)
@@ -76,6 +76,7 @@ my @fields = (
   ),
     
     $m->comp('/Elements/CustomerFields', 'Names'), #freeside
+    $m->comp('/Elements/ServiceFields', 'Names'), #freeside
 
   qw(
     Status ExtendedStatus UpdateStatus
index ff30f7c..e953423 100644 (file)
@@ -210,7 +210,6 @@ my @lines = (
 );
 
 #freeside
-push @lines, $m->comp('/Elements/CustomerFields', 'PickBasics');
 
 $m->callback( Conditions => \@lines );
 
index 44e3b70..5eb112d 100644 (file)
@@ -52,6 +52,7 @@
 
 
 <& PickBasics &>
+<& PickCustomerFields &>
 <& PickCFs, cfqueues => \%cfqueues &>
 
 <tr class="separator"><td colspan="3"><hr /></td></tr>
diff --git a/rt/share/html/Search/Elements/PickCustomerFields b/rt/share/html/Search/Elements/PickCustomerFields
new file mode 100644 (file)
index 0000000..96d8e47
--- /dev/null
@@ -0,0 +1,8 @@
+% if ( $RT::URI::freeside::IntegrationType eq 'Internal' ) {
+%     my @lines;
+%     push @lines, $m->comp('/Elements/CustomerFields', 'Criteria');
+%     push @lines, $m->comp('/Elements/ServiceFields',  'Criteria');
+%     foreach( @lines ) {
+<& ConditionRow, Condition => $_ &>
+%     }
+% }
index 09acdfd..3c2c82a 100644 (file)
 <BR>
 <%$msg%><br>
 
-% if (@Customers) {
+% if (@Customers or @Services) {
 
-<br><i>(Check box to link)<i>
+<br><i>(Check box to link)</i>
 <table>
 % foreach my $customer (@Customers) {
 <tr>
   <td>
     <input type="checkbox" name="Ticket-AddCustomer-<% $customer->{'custnum'} %>" VALUE="1" <% scalar(@Customers) == 1 ? 'CHECKED' : '' %>>
-    <A HREF="<%$freeside_url%>/view/cust_main.cgi?<% $customer->{'custnum'} %>"><% &RT::URI::freeside::small_custview($customer->{'custnum'}, &RT::URI::freeside::FreesideGetConfig('countrydefault'), 1) |n %>
+    <& .small_custview, $customer &>
+  </td>
+</tr>
+% }
+%
+% foreach my $service (@Services) {
+<tr>
+  <td>
+    <input type="checkbox" name="Ticket-AddService-<% $service->{'svcnum'} %>" VALUE="1" <% scalar(@Services) == 1 ? 'CHECKED' : '' %>>
+    <& .small_custview, $service &>
+    <& .small_svcview,  $service &>
   </td>
 </tr>
 % }
 
 % }
 
+<%once>
+my $freeside_url = &RT::URI::freeside::FreesideURL();
+
+</%once>
+<%def .small_custview>
+% my $cust = shift;
+<A HREF="<%$freeside_url%>/view/cust_main.cgi?<% $cust->{'custnum'}%>"><% &RT::URI::freeside::small_custview($cust->{'custnum'}, &RT::URI::freeside::FreesideGetConfig, 1) |n %></A>
+</%def>
+<%def .small_svcview>
+% my $svc = shift;
+<A HREF="<%$freeside_url%>/view/cust_svc.cgi?<% $svc->{'svcnum'}%>"><B><% $svc->{'label'} %></B></A>
+</%def>
 <%INIT>
 my ($msg);
 
-my $freeside_url = &RT::URI::freeside::FreesideURL();
-
 my @Customers = ();
 if ( $CustomerString ) {
     @Customers = &RT::URI::freeside::smart_search(
@@ -43,8 +63,11 @@ if ( $CustomerString ) {
 }
 
 my @Services = ();
-if ($ServiceString) {
-    @Services = (); #service_search();
+if ( $ServiceString
+      and $RT::URI::freeside::IntegrationType eq 'Internal' ) {
+    @Services = RT::URI::freeside::service_search(
+        'search'  => $ServiceString,
+    );
 }
 
 </%INIT>
index 0ba6e44..96207f4 100644 (file)
@@ -12,7 +12,7 @@
 %# General Public License for more details.
 <TABLE width=100%>
   <TR>
-    <TD VALIGN=TOP WIDTH=50%>
+    <TD VALIGN=TOP WIDTH=50% ROWSPAN=3>
       <h3><&|/l&>Current Customers</&></h3>
 
 <table>
 
       <INPUT TYPE=CHECKBOX NAME="DeleteLink--<%$link->Type%>-<%$link->Target%>">
 %#        <& ShowLink, URI => $link->TargetURI &><br>
-        <A HREF="<% $link->TargetURI->Resolver->HREF %>"><% $link->TargetURI->Resolver->AsStringLong |n %></A>
-      <BR>
+        <% $link->TargetURI->Resolver->AsStringLong |n %></A>
+      <BR><BR>
 % }
     </td>
   </tr>
 </table>
-                           
+
 </TD>
 
-<TD VALIGN=TOP>
+<TD VALIGN=TOP WIDTH=50% COLSPAN=2>
 <h3><&|/l&>New Customer Links</&></h3>
-<&|/l&>Find customer</&><BR>
-<input name="CustomerString">
-<input type=submit name="OnlySearchForCustomers" value="<&|/l&>Go!</&>">
-<br><i>cust #, name, company or phone</i>
-<BR>
-%#<BR>
-%#<&|/l&>Find service</&><BR>
-%#<input name="ServiceString">
-%#<input type=submit name="OnlySearchForServices" value="<&|/l&>Go!</&>">
-%#<br><i>username, username@domain, domain, or IP address</i>
-%#<BR>
-
+</TD>
+</TR>
+<TR VALIGN="top">
+%# rowspan
+  <td width=25%>
+    <&|/l&>Find customer</&><br>
+    <input name="CustomerString">
+    <input type=submit name="OnlySearchForCustomers" value="<&|/l&>Go!</&>">
+    <br><i>cust #, name, company or phone</i>
+  </td>
+  <td width=25%>
+    <&|/l&>Find service</&><br>
+    <input name="ServiceString">
+    <input type=submit name="OnlySearchForServices" value="<&|/l&>Go!</&>">
+    <br><i>user, email, ip, mac, phone</i>
+  </td>
+</TR>
+<TR>
+%#rowspan...
+<TD COLSPAN=2>
 <& AddCustomers, Ticket         => $Ticket,
                  CustomerString => $CustomerString,
-                 ServiceString  => $ServiceString,  &>
-
+                 ServiceString  => $ServiceString &>
 </TD>
 </TR>
 </TABLE>
index 3acf92d..add5624 100644 (file)
 %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 %# General Public License for more details.
 <table>
-% my $cust = 0;
-% foreach my $custResolver ( map { $_->TargetURI->Resolver }
-%                                @{ $Ticket->Customers->ItemsArrayRef }
-%                          )
-% {
-%   $cust++;
-%   my $cust_main = '';
+% my @cust = map { $_->TargetURI->Resolver } 
+%                   @{ $Ticket->Customers->ItemsArrayRef };
+%
+% foreach my $custResolver ( @cust ) {
   <tr>
     <td class="value">
-      <A HREF="<% $custResolver->HREF %>"><% $custResolver->AsStringLong |n %></A>
+      <% $custResolver->AsStringLong |n %>
+%# includes service label and view/svc_ link for cust_svc links
     </td>
   </tr>
 % }
-% unless ( $cust ) {
+
+% unless ( @cust ) {
   <tr>
     <td class="labeltop">
       <i>(none)<i>