[freeside-commits] branch master updated. 1c59bba12621e154765a8255534e94a041dfd200

Mark Wells mark at 420.am
Tue Apr 17 15:52:34 PDT 2012


The branch, master has been updated
       via  1c59bba12621e154765a8255534e94a041dfd200 (commit)
      from  71cbdde5012550846390e9f0ebafdb48e06da5e8 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 1c59bba12621e154765a8255534e94a041dfd200
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Apr 17 15:52:14 2012 -0700

    link tickets to services, #17067

diff --git a/FS/FS/TicketSystem/RT_External.pm b/FS/FS/TicketSystem/RT_External.pm
index f976ac0..22d2472 100644
--- a/FS/FS/TicketSystem/RT_External.pm
+++ b/FS/FS/TicketSystem/RT_External.pm
@@ -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,
   );
 
diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm
index d96e5f0..ffa8c7c 100644
--- a/FS/FS/TicketSystem/RT_Internal.pm
+++ b/FS/FS/TicketSystem/RT_Internal.pm
@@ -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;
 }
 
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index fc6e605..a527913 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -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
diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm
index 118748e..b395ea6 100644
--- a/FS/FS/svc_phone.pm
+++ b/FS/FS/svc_phone.pm
@@ -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
index 0000000..362e823
--- /dev/null
+++ b/httemplate/elements/form-create_ticket.html
@@ -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/view/cust_main/tickets.html b/httemplate/elements/table-tickets.html
similarity index 60%
copy from httemplate/view/cust_main/tickets.html
copy to httemplate/elements/table-tickets.html
index 194e907..6d1a45a 100644
--- a/httemplate/view/cust_main/tickets.html
+++ b/httemplate/elements/table-tickets.html
@@ -1,39 +1,9 @@
-<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> | 
+<& /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>
+ <BR>
 
 <& /elements/table-grid.html &>
 % my $bgcolor1 = '#eeeeee';
@@ -52,6 +22,9 @@ View
 % 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 ) {
@@ -104,6 +77,15 @@ View
       <% $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>
 
@@ -112,25 +94,38 @@ View
 </TABLE>
 
 <%init>
-use Date::Format 'time2str';
-use Date::Parse 'str2time';
+use Date::Parse qw(str2time);
+use Date::Format qw(time2str);
 
-my( $conf ) = new FS::Conf;
-my( $cust_main ) = @_;
-my( @tickets )  = $cust_main->tickets;
+my %opt = @_;
+my $conf = new FS::Conf;
 
-my $open_link = FS::TicketSystem->href_customer_tickets($cust_main->custnum);
-my $openlabel = join('/', FS::TicketSystem->statuses );
+my $object = $opt{'object'};
+$object = $object->cust_svc if $object->isa('FS::svc_Common');
+my( @tickets )  = $object->tickets;
 
-my $res_link  = FS::TicketSystem->href_customer_tickets(
-                  $cust_main->custnum,
-                  { 'statuses' => [ 'resolved' ] }
-                );
+my ($openlabel, $open_link, $res_link, $thing);
+$openlabel = join('/', FS::TicketSystem->statuses );
 
-my( $new_base, %new_param ) = 
-  FS::TicketSystem->href_params_new_ticket( $cust_main );
+# 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);
 
-my $new_link = FS::TicketSystem->href_new_ticket( $cust_main );
+  $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 ) {
diff --git a/httemplate/view/cust_main/tickets.html b/httemplate/view/cust_main/tickets.html
index 194e907..f076fcc 100644
--- a/httemplate/view/cust_main/tickets.html
+++ b/httemplate/view/cust_main/tickets.html
@@ -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 &>
diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html
index 3130c73..04d2b29 100644
--- a/httemplate/view/elements/svc_Common.html
+++ b/httemplate/view/elements/svc_Common.html
@@ -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') %>
diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi
index 441e494..de97a07 100755
--- a/httemplate/view/svc_acct.cgi
+++ b/httemplate/view/svc_acct.cgi
@@ -75,6 +75,10 @@
 
 <& 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 &>
diff --git a/httemplate/view/svc_domain.cgi b/httemplate/view/svc_domain.cgi
index 3938a34..fcccd74 100755
--- a/httemplate/view/svc_domain.cgi
+++ b/httemplate/view/svc_domain.cgi
@@ -36,6 +36,10 @@
 
 <% 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') %>
diff --git a/httemplate/view/svc_external.cgi b/httemplate/view/svc_external.cgi
index 77679d8..72e5535 100644
--- a/httemplate/view/svc_external.cgi
+++ b/httemplate/view/svc_external.cgi
@@ -23,6 +23,11 @@
 
 
 </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') %>
diff --git a/httemplate/view/svc_forward.cgi b/httemplate/view/svc_forward.cgi
index 15b5ae5..2cb78eb 100755
--- a/httemplate/view/svc_forward.cgi
+++ b/httemplate/view/svc_forward.cgi
@@ -53,6 +53,10 @@
 
 <% 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') %>
diff --git a/httemplate/view/svc_www.cgi b/httemplate/view/svc_www.cgi
index 935d139..fbb02a0 100644
--- a/httemplate/view/svc_www.cgi
+++ b/httemplate/view/svc_www.cgi
@@ -49,6 +49,10 @@
 </TABLE>
 
 <BR>
+% if ( $conf->config('ticket_system') ) {
+<& /elements/table-tickets.html, object => $cust_svc &>
+% }
+
 <% joblisting({'svcnum'=>$svcnum}, 1) %>
 
 <% include('/elements/footer.html') %>
diff --git a/rt/lib/RT/Interface/Web_Vendor.pm b/rt/lib/RT/Interface/Web_Vendor.pm
index ee8c34b..ae7f089 100644
--- a/rt/lib/RT/Interface/Web_Vendor.pm
+++ b/rt/lib/RT/Interface/Web_Vendor.pm
@@ -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 }
diff --git a/rt/lib/RT/Record.pm b/rt/lib/RT/Record.pm
index 121c086..41e60bd 100755
--- a/rt/lib/RT/Record.pm
+++ b/rt/lib/RT/Record.pm
@@ -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]
diff --git a/rt/lib/RT/Tickets_Overlay.pm b/rt/lib/RT/Tickets_Overlay.pm
index a5d37a3..0d482cd 100644
--- a/rt/lib/RT/Tickets_Overlay.pm
+++ b/rt/lib/RT/Tickets_Overlay.pm
@@ -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
diff --git a/rt/lib/RT/URI/freeside/Internal.pm b/rt/lib/RT/URI/freeside/Internal.pm
index 5656a51..b5e56ee 100644
--- a/rt/lib/RT/URI/freeside/Internal.pm
+++ b/rt/lib/RT/URI/freeside/Internal.pm
@@ -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;
diff --git a/rt/share/html/Elements/CustomerFields b/rt/share/html/Elements/CustomerFields
index 553a349..d5419d2 100644
--- a/rt/share/html/Elements/CustomerFields
+++ b/rt/share/html/Elements/CustomerFields
@@ -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"; }
diff --git a/rt/share/html/Elements/RT__Ticket/ColumnMap b/rt/share/html/Elements/RT__Ticket/ColumnMap
index 9e6466c..35c0aad 100644
--- a/rt/share/html/Elements/RT__Ticket/ColumnMap
+++ b/rt/share/html/Elements/RT__Ticket/ColumnMap
@@ -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
index 0000000..9c9a248
--- /dev/null
+++ b/rt/share/html/Elements/ServiceFields
@@ -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>
diff --git a/rt/share/html/Search/Elements/BuildFormatString b/rt/share/html/Search/Elements/BuildFormatString
index 96e6a28..57c7679 100644
--- a/rt/share/html/Search/Elements/BuildFormatString
+++ b/rt/share/html/Search/Elements/BuildFormatString
@@ -76,6 +76,7 @@ my @fields = (
   ),
     
     $m->comp('/Elements/CustomerFields', 'Names'), #freeside
+    $m->comp('/Elements/ServiceFields', 'Names'), #freeside
 
   qw(
     Status ExtendedStatus UpdateStatus
diff --git a/rt/share/html/Search/Elements/PickBasics b/rt/share/html/Search/Elements/PickBasics
index ff30f7c..e953423 100644
--- a/rt/share/html/Search/Elements/PickBasics
+++ b/rt/share/html/Search/Elements/PickBasics
@@ -210,7 +210,6 @@ my @lines = (
 );
 
 #freeside
-push @lines, $m->comp('/Elements/CustomerFields', 'PickBasics');
 
 $m->callback( Conditions => \@lines );
 
diff --git a/rt/share/html/Search/Elements/PickCriteria b/rt/share/html/Search/Elements/PickCriteria
index 44e3b70..5eb112d 100644
--- a/rt/share/html/Search/Elements/PickCriteria
+++ b/rt/share/html/Search/Elements/PickCriteria
@@ -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
index 0000000..96d8e47
--- /dev/null
+++ b/rt/share/html/Search/Elements/PickCustomerFields
@@ -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 => $_ &>
+%     }
+% }
diff --git a/rt/share/html/Ticket/Elements/AddCustomers b/rt/share/html/Ticket/Elements/AddCustomers
index 09acdfd..3c2c82a 100644
--- a/rt/share/html/Ticket/Elements/AddCustomers
+++ b/rt/share/html/Ticket/Elements/AddCustomers
@@ -13,15 +13,25 @@
 <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>
 % }
@@ -29,11 +39,21 @@
 
 % }
 
+<%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>
diff --git a/rt/share/html/Ticket/Elements/EditCustomers b/rt/share/html/Ticket/Elements/EditCustomers
index 0ba6e44..96207f4 100644
--- a/rt/share/html/Ticket/Elements/EditCustomers
+++ b/rt/share/html/Ticket/Elements/EditCustomers
@@ -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>
@@ -25,33 +25,40 @@
 
       <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 at 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>
diff --git a/rt/share/html/Ticket/Elements/ShowCustomers b/rt/share/html/Ticket/Elements/ShowCustomers
index 3acf92d..add5624 100644
--- a/rt/share/html/Ticket/Elements/ShowCustomers
+++ b/rt/share/html/Ticket/Elements/ShowCustomers
@@ -10,20 +10,19 @@
 %# 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>

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

Summary of changes:
 FS/FS/TicketSystem/RT_External.pm                  |   51 ++++--
 FS/FS/TicketSystem/RT_Internal.pm                  |   63 ++++++-
 FS/FS/cust_svc.pm                                  |   86 +++++++++
 FS/FS/svc_phone.pm                                 |    3 +-
 httemplate/elements/form-create_ticket.html        |   38 ++++
 .../tickets.html => elements/table-tickets.html}   |   93 +++++------
 httemplate/view/cust_main/tickets.html             |  166 +------------------
 httemplate/view/elements/svc_Common.html           |    4 +
 httemplate/view/svc_acct.cgi                       |    4 +
 httemplate/view/svc_domain.cgi                     |    4 +
 httemplate/view/svc_external.cgi                   |    5 +
 httemplate/view/svc_forward.cgi                    |    4 +
 httemplate/view/svc_www.cgi                        |    4 +
 rt/lib/RT/Interface/Web_Vendor.pm                  |   26 +++-
 rt/lib/RT/Record.pm                                |   47 +++++-
 rt/lib/RT/Tickets_Overlay.pm                       |  182 ++++++++++++++++----
 rt/lib/RT/URI/freeside/Internal.pm                 |   81 ++++++++-
 rt/share/html/Elements/CustomerFields              |   16 +-
 rt/share/html/Elements/RT__Ticket/ColumnMap        |    1 +
 rt/share/html/Elements/ServiceFields               |  161 +++++++++++++++++
 rt/share/html/Search/Elements/BuildFormatString    |    1 +
 rt/share/html/Search/Elements/PickBasics           |    1 -
 rt/share/html/Search/Elements/PickCriteria         |    1 +
 rt/share/html/Search/Elements/PickCustomerFields   |    8 +
 rt/share/html/Ticket/Elements/AddCustomers         |   37 ++++-
 rt/share/html/Ticket/Elements/EditCustomers        |   45 +++--
 rt/share/html/Ticket/Elements/ShowCustomers        |   17 +-
 27 files changed, 819 insertions(+), 330 deletions(-)
 create mode 100644 httemplate/elements/form-create_ticket.html
 copy httemplate/{view/cust_main/tickets.html => elements/table-tickets.html} (60%)
 create mode 100644 rt/share/html/Elements/ServiceFields
 create mode 100644 rt/share/html/Search/Elements/PickCustomerFields




More information about the freeside-commits mailing list