contact search, RT#25687 (also possibly #25583 and #22991)
[freeside.git] / FS / FS / cust_main / Search.pm
index 70d12c9..362a6aa 100644 (file)
@@ -19,8 +19,11 @@ use FS::payinfo_Mixin;
 $DEBUG = 0;
 $me = '[FS::cust_main::Search]';
 
-@fuzzyfields = ( 'cust_main.first', 'cust_main.last', 'cust_main.company', 
-  'cust_location.address1' );
+@fuzzyfields = (
+  'cust_main.first', 'cust_main.last', 'cust_main.company', 
+  'cust_location.address1',
+  'contact.first',   'contact.last',
+);
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -72,6 +75,7 @@ sub smart_search {
   #here is the agent virtualization
   my $agentnums_sql = 
     $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main');
+  my $agentnums_href = $FS::CurrentUser::CurrentUser->agentnums_href;
 
   my @cust_main = ();
 
@@ -85,6 +89,10 @@ sub smart_search {
     my $phonen = "$1-$2-$3";
     $phonen .= " x$4" if $4;
 
+    my $phonenum = "$1$2$3";
+    #my $extension = $4;
+
+    #cust_main phone numbers
     push @cust_main, qsearch( {
       'table'   => 'cust_main',
       'hashref' => { %options },
@@ -97,6 +105,16 @@ sub smart_search {
                      " AND $agentnums_sql", #agent virtualization
     } );
 
+    #contact phone numbers
+    push @cust_main,
+      grep $agentnums_href->{$_->agentnum}, #agent virt
+        grep $_, #skip contacts that don't have cust_main records
+          map $_->contact->cust_main,
+            qsearch({
+                      'table'   => 'contact_phone',
+                      'hashref' => { 'phonenum' => $phonenum },
+                   });
+
     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
       #try looking for matches with extensions unless one was specified
 
@@ -117,8 +135,11 @@ sub smart_search {
   } 
   
   
-  if ( $search =~ /@/ ) { #invoicing email address
+  if ( $search =~ /@/ ) { #email address
+
+      # invoicing email address
       push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
          map $_->cust_main,
              qsearch( {
                         'table'     => 'cust_main_invoice',
@@ -126,6 +147,17 @@ sub smart_search {
                       }
                     );
 
+      # contact email address
+      push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
+          grep $_, #skip contacts that don't have cust_main records
+           map $_->contact->cust_main,
+             qsearch( {
+                        'table'     => 'contact_email',
+                        'hashref'   => { 'emailaddress' => $search },
+                      }
+                    );
+
   # custnum search (also try agent_custid), with some tweaking options if your
   # legacy cust "numbers" have letters
   } elsif ( $search =~ /^\s*(\d+)\s*$/
@@ -159,7 +191,7 @@ sub smart_search {
     # for all agents this user can see, if any of them have custnum prefixes 
     # that match the search string, include customers that match the rest 
     # of the custnum and belong to that agent
-    foreach my $agentnum ( $FS::CurrentUser::CurrentUser->agentnums ) {
+    foreach my $agentnum ( keys %$agentnums_href ) {
       my $p = $conf->config('cust_main-custnum-display_prefix', $agentnum);
       next if !$p;
       if ( $p eq substr($num, 0, length($p)) ) {
@@ -216,10 +248,12 @@ sub smart_search {
           $agentnums_sql,
         ),
       } ),
+
     #contacts?
+    # probably not necessary for the "something a browser remembered" case
 
   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
-                                              # try (ship_){last,company}
+                                              # try {first,last,company}
 
     my $value = lc($1);
 
@@ -256,12 +290,25 @@ sub smart_search {
       my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
       $sql .= "( LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first )";
 
+      #cust_main
       push @cust_main, qsearch( {
         'table'     => 'cust_main',
         'hashref'   => \%options,
         'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
       } );
-      #contacts?
+
+      #contacts
+      push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
+          grep $_, #skip contacts that don't have cust_main records
+           map $_->cust_main,
+             qsearch( {
+                        'table'     => 'contact',
+                        'hashref'   => { 'first' => $first,
+                                          'last'  => $last,
+                                        }, 
+                      }
+                    );
 
       # or it just be something that was typed in... (try that in a sec)
 
@@ -271,18 +318,28 @@ sub smart_search {
 
     #exact
     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
-    $sql .= " (    LOWER(last)          = $q_value
-                OR LOWER(company)       = $q_value
+    $sql .= " (    LOWER(cust_main.first)         = $q_value
+                OR LOWER(cust_main.last)          = $q_value
+                OR LOWER(cust_main.company)       = $q_value
             ";
-    #yes, it's a kludge
-    $sql .= "   OR EXISTS( 
-                SELECT 1 FROM cust_location 
-                WHERE LOWER(cust_location.address1) = $q_value
-                  AND cust_location.custnum = cust_main.custnum
-            )
-            "
+
+    #address1 (yes, it's a kludge)
+    $sql .= "   OR EXISTS ( 
+                            SELECT 1 FROM cust_location 
+                              WHERE LOWER(cust_location.address1) = $q_value
+                                AND cust_location.custnum = cust_main.custnum
+                          )"
       if $conf->exists('address1-search');
-    $sql .= " )";
+
+    #contacts (look, another kludge)
+    $sql .= "   OR EXISTS ( SELECT 1 FROM contact
+                              WHERE (    LOWER(contact.first) = $q_value
+                                      OR LOWER(contact.last)  = $q_value
+                                    )
+                                AND contact.custnum IS NOT NULL
+                                AND contact.custnum = cust_main.custnum
+                          )
+              ) ";
 
     push @cust_main, qsearch( {
       'table'     => 'cust_main',
@@ -304,7 +361,6 @@ sub smart_search {
       );
 
       if ( $first && $last ) {
-        #contacts? ship_first/ship_last are gone
 
         push @hashrefs,
           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
@@ -315,6 +371,7 @@ sub smart_search {
       } else {
 
         push @hashrefs,
+          { 'first'        => { op=>'ILIKE', value=>"%$value%" }, },
           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
         ;
       }
@@ -334,14 +391,35 @@ sub smart_search {
       if ( $conf->exists('address1-search') ) {
 
         push @cust_main, qsearch( {
-          'table'     => 'cust_main',
-          'addl_from' => 'JOIN cust_location USING (custnum)',
-          'extra_sql' => 'WHERE cust_location.address1 ILIKE '.
-                          dbh->quote("%$value%"),
+          table     => 'cust_main',
+          addl_from => 'JOIN cust_location USING (custnum)',
+          extra_sql => 'WHERE '.
+                        ' cust_location.address1 ILIKE '.dbh->quote("%$value%").
+                        " AND $agentnums_sql", #agent virtualizaiton
         } );
 
       }
 
+      #contact substring
+
+      shift @hashrefs; #no company column in contact table
+     
+      foreach my $hashref ( @hashrefs ) {
+
+        push @cust_main,
+          grep $agentnums_href->{$_->agentnum}, #agent virt
+            grep $_, #skip contacts that don't have cust_main records
+             map $_->cust_main,
+                qsearch({
+                          'table'     => 'contact',
+                          'hashref'   => { %$hashref,
+                                           #%options,
+                                         },
+                          #'extra_sql' => " AND $agentnums_sql", #agent virt
+                       });
+
+      }
+
       #fuzzy
       my %fuzopts = (
         'hashref'   => \%options,
@@ -356,7 +434,7 @@ sub smart_search {
           %fuzopts
         );
       }
-      foreach my $field ( 'last', 'company' ) {
+      foreach my $field ( 'first', 'last', 'company' ) {
         push @cust_main,
           FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts );
       }
@@ -1004,8 +1082,10 @@ sub fuzzy_search {
     $extra_sql .= "$field $in_matches";
 
     my $addl_from = $fuzopts{addl_from};
-    if ( $field =~ /^cust_location/ ) {
+    if ( $field =~ /^cust_location\./ ) {
       $addl_from .= ' JOIN cust_location USING (custnum)';
+    } elsif ( $field =~ /^contact\./ ) {
+      $addl_from .= ' JOIN contact USING (custnum)';
     }
 
     push @cust_main, qsearch({
@@ -1035,7 +1115,14 @@ sub fuzzy_search {
 
 sub check_and_rebuild_fuzzyfiles {
   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields;
+  rebuild_fuzzyfiles()
+    if grep { ! -e "$dir/$_" }
+         map {
+               my ($field, $table) = reverse split('\.', $_);
+               $table ||= 'cust_main';
+               "$table.$field"
+             }
+           @fuzzyfields;
 }
 
 =item rebuild_fuzzyfiles
@@ -1088,34 +1175,47 @@ sub append_fuzzyfiles {
 
   check_and_rebuild_fuzzyfiles();
 
-  use Fcntl qw(:flock);
+  #foreach my $fuzzy (@fuzzyfields) {
+  foreach my $fuzzy ( 'cust_main.first', 'cust_main.last', 'cust_main.company', 
+                      'cust_location.address1',
+                    ) {
 
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+    append_fuzzyfiles_fuzzyfield($fuzzy, shift);
 
-  foreach my $fuzzy (@fuzzyfields) {
+  }
 
-    my ($field, $table) = reverse split('\.', $fuzzy);
-    $table ||= 'cust_main';
+  1;
+}
 
-    my $value = shift;
+=item append_fuzzyfiles_fuzzyfield COLUMN VALUE
 
-    if ( $value ) {
+=item append_fuzzyfiles_fuzzyfield TABLE.COLUMN VALUE
 
-      open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" )
-        or die "can't open $dir/$table.$field: $!";
-      flock(CACHE,LOCK_EX)
-        or die "can't lock $dir/$table.$field: $!";
+=cut
 
-      print CACHE "$value\n";
+use Fcntl qw(:flock);
+sub append_fuzzyfiles_fuzzyfield {
+  my( $fuzzyfield, $value ) = @_;
 
-      flock(CACHE,LOCK_UN)
-        or die "can't unlock $dir/$table.$field: $!";
-      close CACHE;
-    }
+  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
 
-  }
 
-  1;
+  my ($field, $table) = reverse split('\.', $fuzzyfield);
+  $table ||= 'cust_main';
+
+  return unless length($value);
+
+  open(CACHE, '>>:encoding(UTF-8)', "$dir/$table.$field" )
+    or die "can't open $dir/$table.$field: $!";
+  flock(CACHE,LOCK_EX)
+    or die "can't lock $dir/$table.$field: $!";
+
+  print CACHE "$value\n";
+
+  flock(CACHE,LOCK_UN)
+    or die "can't unlock $dir/$table.$field: $!";
+  close CACHE;
+
 }
 
 =item all_X