multiple payment options, RT#23741
[freeside.git] / FS / FS / cust_main / Search.pm
index 9ec40cd..097f2fb 100644 (file)
@@ -9,6 +9,7 @@ use FS::Record qw( qsearch );
 use FS::cust_main;
 use FS::cust_main_invoice;
 use FS::svc_acct;
 use FS::cust_main;
 use FS::cust_main_invoice;
 use FS::svc_acct;
+use FS::payinfo_Mixin;
 
 @EXPORT_OK = qw( smart_search );
 
 
 @EXPORT_OK = qw( smart_search );
 
@@ -18,8 +19,12 @@ use FS::svc_acct;
 $DEBUG = 0;
 $me = '[FS::cust_main::Search]';
 
 $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_main.ship_company', # if you're using it
+  'cust_location.address1',
+  'contact.first',   'contact.last',
+);
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -50,8 +55,12 @@ FS::cust_main::Search - Customer searching
 
 Accepts the following options: I<search>, the string to search for.  The string
 will be searched for as a customer number, phone number, name or company name,
 
 Accepts the following options: I<search>, the string to search for.  The string
 will be searched for as a customer number, phone number, name or company name,
-as an exact, or, in some cases, a substring or fuzzy match (see the source code
-for the exact heuristics used); I<no_fuzzy_on_exact>, causes smart_search to
+address (if address1-search is on), invoicing email address, or credit card
+number.
+
+Searches match as an exact, or, in some cases, a substring or fuzzy match (see
+the source code for the exact heuristics used); I<no_fuzzy_on_exact>, causes
+smart_search to
 skip fuzzy matching when an exact match is found.
 
 Any additional options are treated as an additional qualifier on the search
 skip fuzzy matching when an exact match is found.
 
 Any additional options are treated as an additional qualifier on the search
@@ -67,6 +76,7 @@ sub smart_search {
   #here is the agent virtualization
   my $agentnums_sql = 
     $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main');
   #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 = ();
 
 
   my @cust_main = ();
 
@@ -80,6 +90,10 @@ sub smart_search {
     my $phonen = "$1-$2-$3";
     $phonen .= " x$4" if $4;
 
     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 },
     push @cust_main, qsearch( {
       'table'   => 'cust_main',
       'hashref' => { %options },
@@ -92,6 +106,16 @@ sub smart_search {
                      " AND $agentnums_sql", #agent virtualization
     } );
 
                      " 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
 
     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
       #try looking for matches with extensions unless one was specified
 
@@ -109,19 +133,34 @@ sub smart_search {
 
     }
 
 
     }
 
-  # custnum search (also try agent_custid), with some tweaking options if your
-  # legacy cust "numbers" have letters
   } 
   
   
   } 
   
   
-  if ( $search =~ /@/ ) {
+  if ( $search =~ /@/ ) { #email address
+
+      # invoicing email address
       push @cust_main,
       push @cust_main,
+        grep $agentnums_href->{$_->agentnum}, #agent virt
          map $_->cust_main,
              qsearch( {
                         'table'     => 'cust_main_invoice',
                         'hashref'   => { 'dest' => $search },
                       }
                     );
          map $_->cust_main,
              qsearch( {
                         'table'     => 'cust_main_invoice',
                         'hashref'   => { 'dest' => $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*$/
          || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
               && $search =~ /^\s*(\w\w?\d+)\s*$/
   } elsif ( $search =~ /^\s*(\d+)\s*$/
          || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
               && $search =~ /^\s*(\w\w?\d+)\s*$/
@@ -153,7 +192,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
     # 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)) ) {
       my $p = $conf->config('cust_main-custnum-display_prefix', $agentnum);
       next if !$p;
       if ( $p eq substr($num, 0, length($p)) ) {
@@ -210,10 +249,12 @@ sub smart_search {
           $agentnums_sql,
         ),
       } ),
           $agentnums_sql,
         ),
       } ),
+
     #contacts?
     #contacts?
+    # probably not necessary for the "something a browser remembered" case
 
   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
 
   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
-                                              # try (ship_){last,company}
+                                              # try {first,last,company}
 
     my $value = lc($1);
 
 
     my $value = lc($1);
 
@@ -250,12 +291,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 )";
 
       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
       } );
       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)
 
 
       # or it just be something that was typed in... (try that in a sec)
 
@@ -265,18 +319,29 @@ sub smart_search {
 
     #exact
     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
 
     #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
+                OR LOWER(cust_main.ship_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');
       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',
 
     push @cust_main, qsearch( {
       'table'     => 'cust_main',
@@ -293,27 +358,30 @@ sub smart_search {
 
       #substring
 
 
       #substring
 
-      my @hashrefs = (
+      my @company_hashrefs = (
         { 'company'      => { op=>'ILIKE', value=>"%$value%" }, },
         { 'company'      => { op=>'ILIKE', value=>"%$value%" }, },
+        { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
       );
 
       );
 
+      my @hashrefs = ();
+
       if ( $first && $last ) {
       if ( $first && $last ) {
-        #contacts? ship_first/ship_last are gone
 
 
-        push @hashrefs,
+        @hashrefs = (
           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
             'last'         => { op=>'ILIKE', value=>"%$last%" },
           },
           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
             'last'         => { op=>'ILIKE', value=>"%$last%" },
           },
-        ;
+        );
 
       } else {
 
 
       } else {
 
-        push @hashrefs,
+        @hashrefs = (
+          { 'first'        => { op=>'ILIKE', value=>"%$value%" }, },
           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
-        ;
+        );
       }
 
       }
 
-      foreach my $hashref ( @hashrefs ) {
+      foreach my $hashref ( @company_hashrefs, @hashrefs ) {
 
         push @cust_main, qsearch( {
           'table'     => 'cust_main',
 
         push @cust_main, qsearch( {
           'table'     => 'cust_main',
@@ -328,14 +396,33 @@ sub smart_search {
       if ( $conf->exists('address1-search') ) {
 
         push @cust_main, qsearch( {
       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
+
+      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,
       #fuzzy
       my %fuzopts = (
         'hashref'   => \%options,
@@ -349,21 +436,59 @@ sub smart_search {
             'first'  => $first }, #
           %fuzopts
         );
             'first'  => $first }, #
           %fuzopts
         );
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { 'contact.last'   => $last,    #fuzzy hashref
+            'contact.first'  => $first }, #
+          %fuzopts
+        );
+     }
+      foreach my $field ( 'first', 'last', 'company', 'ship_company' ) {
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { $field => $value },
+          %fuzopts
+        );
       }
       }
-      foreach my $field ( 'last', 'company' ) {
-        push @cust_main,
-          FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts );
+      foreach my $field ( 'first', 'last' ) {
+        push @cust_main, FS::cust_main::Search->fuzzy_search(
+          { "contact.$field" => $value },
+          %fuzopts
+        );
       }
       if ( $conf->exists('address1-search') ) {
         push @cust_main,
           FS::cust_main::Search->fuzzy_search(
       }
       if ( $conf->exists('address1-search') ) {
         push @cust_main,
           FS::cust_main::Search->fuzzy_search(
-            { 'cust_location.address1' => $value }, %fuzopts );
+            { 'cust_location.address1' => $value },
+            %fuzopts
+        );
       }
 
     }
 
   }
 
       }
 
     }
 
   }
 
+  ( my $nospace_search = $search ) =~ s/\s//g;
+  ( my $card_search = $nospace_search ) =~ s/\-//g;
+  $card_search =~ s/[x\*\.\_]/x/gi;
+  
+  if ( $card_search =~ /^[\dx]{15,16}$/i ) { #credit card search
+
+    ( my $like_search = $card_search ) =~ s/x/_/g;
+    my $mask_search = FS::payinfo_Mixin->mask_payinfo('CARD', $card_search);
+
+    push @cust_main, qsearch({
+      'table'     => 'cust_main',
+      'addl_from' => ' JOIN cust_payby USING (custnum)',
+      'hashref'   => {},
+      'extra_sql' => " WHERE (    cust_payby.payinfo LIKE '$like_search'
+                               OR cust_payby.paymask =    '$mask_search'
+                             ) ".
+                     " AND cust_payby.payby IN ('CARD','DCRD') ".
+                     " AND $agentnums_sql", #agent virtulization
+    });
+
+  }
+  
+
   #eliminate duplicates
   my %saw = ();
   @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
   #eliminate duplicates
   my %saw = ();
   @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
@@ -483,14 +608,6 @@ listref of start date, end date
 
 listref of start date, end date
 
 
 listref of start date, end date
 
-=item payby
-
-listref
-
-=item paydate_year
-
-=item paydate_month
-
 =item current_balance
 
 listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
 =item current_balance
 
 listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
@@ -521,7 +638,6 @@ sub search {
     'status'        => '',
     'address'       => '',
     'zip'           => '',
     'status'        => '',
     'address'       => '',
     'zip'           => '',
-    'paydate_year'  => '',
     'invoice_terms' => '',
     'custbatch'     => '',
     %$params
     'invoice_terms' => '',
     'custbatch'     => '',
     %$params
@@ -551,7 +667,16 @@ sub search {
   }
 
   ##
   }
 
   ##
-  # do the same for user
+  # parse sales person
+  ##
+
+  if ( $params->{'salesnum'} =~ /^(\d+)$/ ) {
+    push @where, ($1 > 0 ) ? "cust_main.salesnum = $1"
+                           : 'cust_main.salesnum IS NULL';
+  }
+
+  ##
+  # parse usernum
   ##
 
   if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) {
   ##
 
   if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) {
@@ -573,13 +698,59 @@ sub search {
   ##
   # address
   ##
   ##
   # address
   ##
-  if ( $params->{'address'} =~ /\S/ ) {
-    my $address = dbh->quote('%'. lc($params->{'address'}). '%');
+  if ( $params->{'address'} ) {
+    # allow this to be an arrayref
+    my @values = ($params->{'address'});
+    @values = @{$values[0]} if ref($values[0]);
+    my @orwhere;
+    foreach (grep /\S/, @values) {
+      my $address = dbh->quote('%'. lc($_). '%');
+      push @orwhere,
+        "LOWER(cust_location.address1) LIKE $address",
+        "LOWER(cust_location.address2) LIKE $address";
+    }
+    if (@orwhere) {
+      push @where, "EXISTS(
+        SELECT 1 FROM cust_location 
+        WHERE cust_location.custnum = cust_main.custnum
+          AND (".join(' OR ',@orwhere).")
+        )";
+    }
+  }
+
+  ##
+  # city
+  ##
+  if ( $params->{'city'} =~ /\S/ ) {
+    my $city = dbh->quote($params->{'city'});
+    push @where, "EXISTS(
+      SELECT 1 FROM cust_location
+      WHERE cust_location.custnum = cust_main.custnum
+        AND cust_location.city = $city
+    )";
+  }
+
+  ##
+  # county
+  ##
+  if ( $params->{'county'} =~ /\S/ ) {
+    my $county = dbh->quote($params->{'county'});
+    push @where, "EXISTS(
+      SELECT 1 FROM cust_location
+      WHERE cust_location.custnum = cust_main.custnum
+        AND cust_location.county = $county
+    )";
+  }
+
+  ##
+  # state
+  ##
+  if ( $params->{'state'} =~ /\S/ ) {
+    my $state = dbh->quote($params->{'state'});
     push @where, "EXISTS(
     push @where, "EXISTS(
-      SELECT 1 FROM cust_location 
+      SELECT 1 FROM cust_location
       WHERE cust_location.custnum = cust_main.custnum
       WHERE cust_location.custnum = cust_main.custnum
-        AND (LOWER(cust_location.address1) LIKE $address OR
-             LOWER(cust_location.address2) LIKE $address)
+        AND cust_location.state = $state
     )";
   }
 
     )";
   }
 
@@ -595,6 +766,18 @@ sub search {
     )";
   }
 
     )";
   }
 
+  ##
+  # country
+  ##
+  if ( $params->{'country'} =~ /^(\w\w)$/ ) {
+    my $country = uc($1);
+    push @where, "EXISTS(
+      SELECT 1 FROM cust_location
+      WHERE cust_location.custnum = cust_main.custnum
+        AND cust_location.country = '$country'
+    )";
+  }
+
   ###
   # refnum
   ###
   ###
   # refnum
   ###
@@ -621,20 +804,6 @@ sub search {
     unless $params->{'cancelled_pkgs'};
 
   ##
     unless $params->{'cancelled_pkgs'};
 
   ##
-  # parse without census tract checkbox
-  ##
-
-  push @where, "(censustract = '' or censustract is null)"
-    if $params->{'no_censustract'};
-
-  ##
-  # parse with hardcoded tax location checkbox
-  ##
-
-  push @where, "geocode is not null"
-    if $params->{'with_geocode'};
-
-  ##
   # "with email address(es)" checkbox
   ##
 
   # "with email address(es)" checkbox
   ##
 
@@ -666,6 +835,18 @@ sub search {
     if $params->{'no_POST'};
 
   ##
     if $params->{'no_POST'};
 
   ##
+  # "tax exempt" checkbox
+  ##
+  push @where, "cust_main.tax = 'Y'"
+    if $params->{'tax'};
+
+  ##
+  # "not tax exempt" checkbox
+  ##
+  push @where, "(cust_main.tax = '' OR cust_main.tax IS NULL )"
+    if $params->{'no_tax'};
+
+  ##
   # dates
   ##
 
   # dates
   ##
 
@@ -721,40 +902,6 @@ sub search {
   }
 
   ###
   }
 
   ###
-  # payby
-  ###
-
-  if ( $params->{'payby'} ) {
-
-    my @payby = ref( $params->{'payby'} )
-                  ? @{ $params->{'payby'} }
-                  :  ( $params->{'payby'} );
-
-    @payby = grep /^([A-Z]{4})$/, @payby;
-
-    push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )'
-      if @payby;
-
-  }
-
-  ###
-  # paydate_year / paydate_month
-  ###
-
-  if ( $params->{'paydate_year'} =~ /^(\d{4})$/ ) {
-    my $year = $1;
-    $params->{'paydate_month'} =~ /^(\d\d?)$/
-      or die "paydate_year without paydate_month?";
-    my $month = $1;
-
-    push @where,
-      'paydate IS NOT NULL',
-      "paydate != ''",
-      "CAST(paydate AS timestamp) < CAST('$year-$month-01' AS timestamp )"
-;
-  }
-
-  ###
   # invoice terms
   ###
 
   # invoice terms
   ###
 
@@ -820,6 +967,35 @@ sub search {
     }
   }
 
     }
   }
 
+  # pkg_classnum
+  #   all_pkg_classnums
+  #   any_pkg_status
+  if ( $params->{'pkg_classnum'} ) {
+    my @pkg_classnums = ref( $params->{'pkg_classnum'} ) ?
+                          @{ $params->{'pkg_classnum'} } :
+                             $params->{'pkg_classnum'};
+    @pkg_classnums = grep /^(\d+)$/, @pkg_classnums;
+
+    if ( @pkg_classnums ) {
+
+      my @pkg_where;
+      if ( $params->{'all_pkg_classnums'} ) {
+        push @pkg_where, "part_pkg.classnum = $_" foreach @pkg_classnums;
+      } else {
+        push @pkg_where,
+          'part_pkg.classnum IN('. join(',', @pkg_classnums).')';
+      }
+      foreach (@pkg_where) {
+        my $select_pkg = 
+          "SELECT 1 FROM cust_pkg JOIN part_pkg USING (pkgpart) WHERE ".
+          "cust_pkg.custnum = cust_main.custnum AND $_ ";
+        if ( not $params->{'any_pkg_status'} ) {
+          $select_pkg .= 'AND '.FS::cust_pkg->active_sql;
+        }
+        push @where, "EXISTS($select_pkg)";
+      }
+    }
+  }
 
   ##
   # setup queries, subs, etc. for the search
 
   ##
   # setup queries, subs, etc. for the search
@@ -841,10 +1017,11 @@ sub search {
       'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
   }
 
       'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
   }
 
-  my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
+  my $count_query = "SELECT COUNT(*) FROM cust_main $addl_from $extra_sql";
 
   my @select = (
                  'cust_main.custnum',
 
   my @select = (
                  'cust_main.custnum',
+                 'cust_main.salesnum',
                  # there's a good chance that we'll need these
                  'cust_main.bill_locationnum',
                  'cust_main.ship_locationnum',
                  # there's a good chance that we'll need these
                  'cust_main.bill_locationnum',
                  'cust_main.ship_locationnum',
@@ -901,19 +1078,6 @@ sub search {
 
   }
 
 
   }
 
-  if ( $params->{'with_geocode'} ) {
-
-    unshift @extra_headers, 'Tax location override', 'Calculated tax location';
-    unshift @extra_fields, sub { my $c = shift; $c->get('geocode'); },
-                           sub { my $c = shift;
-                                 $c->set('geocode', '');
-                                 $c->geocode('cch'); #XXX only cch right now
-                               };
-    push @select, 'geocode';
-    push @select, 'zip' unless grep { $_ eq 'zip' } @select;
-    push @select, 'ship_zip' unless grep { $_ eq 'ship_zip' } @select;
-  }
-
   my $select = join(', ', @select);
 
   my $sql_query = {
   my $select = join(', ', @select);
 
   my $sql_query = {
@@ -927,6 +1091,7 @@ sub search {
     'extra_headers' => \@extra_headers,
     'extra_fields'  => \@extra_fields,
   };
     'extra_headers' => \@extra_headers,
     'extra_fields'  => \@extra_fields,
   };
+  $sql_query;
 
 }
 
 
 }
 
@@ -954,6 +1119,11 @@ sub fuzzy_search {
 
   my @cust_main = ();
 
 
   my @cust_main = ();
 
+  my @fuzzy_mod = 'i';
+  my $conf = new FS::Conf;
+  my $fuzziness = $conf->config('fuzzy-fuzziness');
+  push @fuzzy_mod, $fuzziness if $fuzziness;
+
   check_and_rebuild_fuzzyfiles();
   foreach my $field ( keys %$fuzzy ) {
 
   check_and_rebuild_fuzzyfiles();
   foreach my $field ( keys %$fuzzy ) {
 
@@ -961,7 +1131,7 @@ sub fuzzy_search {
     next unless scalar(@$all);
 
     my %match = ();
     next unless scalar(@$all);
 
     my %match = ();
-    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
+    $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, \@fuzzy_mod, @$all ) );
     next if !keys(%match);
 
     my $in_matches = 'IN (' .
     next if !keys(%match);
 
     my $in_matches = 'IN (' .
@@ -977,8 +1147,10 @@ sub fuzzy_search {
     $extra_sql .= "$field $in_matches";
 
     my $addl_from = $fuzopts{addl_from};
     $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)';
       $addl_from .= ' JOIN cust_location USING (custnum)';
+    } elsif ( $field =~ /^contact\./ ) {
+      $addl_from .= ' JOIN contact USING (custnum)';
     }
 
     push @cust_main, qsearch({
     }
 
     push @cust_main, qsearch({
@@ -1008,7 +1180,14 @@ sub fuzzy_search {
 
 sub check_and_rebuild_fuzzyfiles {
   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
 
 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
 }
 
 =item rebuild_fuzzyfiles
@@ -1061,34 +1240,48 @@ sub append_fuzzyfiles {
 
   check_and_rebuild_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',
+                      'cust_main.ship_company',
+                    ) {
 
 
-  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 defined($value) && 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
 }
 
 =item all_X