From 9b05a3660eabd3215bdebf9429eb490b41570436 Mon Sep 17 00:00:00 2001 From: ivan Date: Fri, 17 Sep 2010 23:32:33 +0000 Subject: [PATCH] refactor giant cust_main.pm a little in preparation of adding API methods for maestro, RT#9967 --- FS/FS.pm | 2 + FS/FS/ClientAPI/Agent.pm | 27 +- FS/FS/Mason.pm | 3 +- FS/FS/cust_main.pm | 796 +--------------------------------- FS/FS/cust_main/Billing_Realtime.pm | 1 + FS/FS/cust_main/Search.pm | 828 ++++++++++++++++++++++++++++++++++++ FS/MANIFEST | 1 + httemplate/search/cust_main.cgi | 4 +- httemplate/search/cust_main.html | 2 +- rt/lib/RT/URI/freeside/Internal.pm | 4 +- 10 files changed, 886 insertions(+), 782 deletions(-) create mode 100644 FS/FS/cust_main/Search.pm diff --git a/FS/FS.pm b/FS/FS.pm index 40914cef9..4d994105c 100644 --- a/FS/FS.pm +++ b/FS/FS.pm @@ -61,6 +61,8 @@ L - ClientAPI session cache L - A pony +L - Customer searching + L - Batch customer importing =head2 Database record classes diff --git a/FS/FS/ClientAPI/Agent.pm b/FS/FS/ClientAPI/Agent.pm index daede59a8..e1624b930 100644 --- a/FS/FS/ClientAPI/Agent.pm +++ b/FS/FS/ClientAPI/Agent.pm @@ -9,7 +9,7 @@ use Digest::MD5 qw(md5_hex); use FS::Record qw(qsearchs); # qsearch dbdef dbh); use FS::ClientAPI_SessionCache; use FS::agent; -use FS::cust_main qw(smart_search); +use FS::cust_main::Search qw(smart_search); sub _cache { $cache ||= new FS::ClientAPI_SessionCache( { @@ -17,6 +17,31 @@ sub _cache { } ); } +sub new_agent { + my $p = shift; + + my $conf = new FS::Conf; + return { error=>'Disabled' } unless $conf->exists('selfservice-agent_signup'); + + #add a customer record and set agent_custnum? + + my $agent = new FS::agent { + 'typenum' => $conf->config('selfservice-agent_signup-agent_type'), + 'agent' => $p->{'agent'}, + 'username' => $p->{'username'}, + '_password' => $p->{'password'}, + # + }; + + my $error = $agent->insert; + + return { 'error' => $error } if $error; + + agent_login({ 'username' => $p->{'username'}, + 'password' => $p->{'password'}, + }); +} + sub agent_login { my $p = shift; diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index d769d8514..5c57cbefc 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -142,7 +142,8 @@ if ( -e $addl_handler_use_file ) { use FS::cust_bill_pay; use FS::cust_credit; use FS::cust_credit_bill; - use FS::cust_main qw(smart_search); + use FS::cust_main; + use FS::cust_main::Search qw(smart_search); use FS::cust_main::Import; use FS::cust_main_county; use FS::cust_location; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 21f66b92e..8764c88a4 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -6,7 +6,7 @@ use base qw( FS::cust_main::Billing FS::cust_main::Billing_Realtime FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin FS::Record ); -use vars qw( @EXPORT_OK $DEBUG $me $conf +use vars qw( $DEBUG $me $conf @encrypted_fields $import $ignore_expired_card $skip_fuzzyfiles @fuzzyfields @@ -14,7 +14,6 @@ use vars qw( @EXPORT_OK $DEBUG $me $conf ); use vars qw( $realtime_bop_decline_quiet ); #ugh use Carp; -use Exporter; use Scalar::Util qw( blessed ); use List::Util qw( min ); use Time::Local qw(timelocal); @@ -26,7 +25,6 @@ use Digest::MD5 qw(md5_base64); use Date::Format; #use Date::Manip; use File::Temp qw( tempfile ); -use String::Approx qw(amatch); use Business::CreditCard 0.28; use Locale::Country; use FS::UID qw( getotaker dbh driver_name ); @@ -67,8 +65,6 @@ use FS::agent_payment_gateway; use FS::banned_pay; use FS::TicketSystem; -@EXPORT_OK = qw( smart_search ); - $realtime_bop_decline_quiet = 0; #move to Billing_Realtime # 1 is mostly method/subroutine entry and options @@ -4714,6 +4710,22 @@ sub service_coordinates { scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : () } +=item masked FIELD + +Returns a masked version of the named field + +=cut + +sub masked { +my ($self,$field) = @_; + +# Show last four + +'x'x(length($self->getfield($field))-4). + substr($self->getfield($field), (length($self->getfield($field))-4)); + +} + =back =head1 CLASS METHODS @@ -5002,356 +5014,11 @@ sub _money_table_where { } -=item search HASHREF - -(Class method) - -Returns a qsearch hash expression to search for parameters specified in -HASHREF. Valid parameters are - -=over 4 - -=item agentnum - -=item status - -=item cancelled_pkgs - -bool - -=item signupdate - -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 cust_fields - -=item flattened_pkgs - -bool - -=back - -=cut - +#for dyanmic FS::$table->search in httemplate/misc/email_customers.html +use FS::cust_main::Search; sub search { - my ($class, $params) = @_; - - my $dbh = dbh; - - my @where = (); - my $orderby; - - ## - # parse agent - ## - - if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) { - push @where, - "cust_main.agentnum = $1"; - } - - ## - # do the same for user - ## - - if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) { - push @where, - "cust_main.usernum = $1"; - } - - ## - # parse status - ## - - #prospect ordered active inactive suspended cancelled - if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) { - my $method = $params->{'status'}. '_sql'; - #push @where, $class->$method(); - push @where, FS::cust_main->$method(); - } - - ## - # parse cancelled package checkbox - ## - - my $pkgwhere = ""; - - $pkgwhere .= "AND (cancel = 0 or cancel is null)" - unless $params->{'cancelled_pkgs'}; - - ## - # parse without census tract checkbox - ## - - push @where, "(censustract = '' or censustract is null)" - if $params->{'no_censustract'}; - - ## - # dates - ## - - foreach my $field (qw( signupdate )) { - - next unless exists($params->{$field}); - - my($beginning, $ending, $hour) = @{$params->{$field}}; - - push @where, - "cust_main.$field IS NOT NULL", - "cust_main.$field >= $beginning", - "cust_main.$field <= $ending"; - - # XXX: do this for mysql and/or pull it out of here - if(defined $hour) { - if ($dbh->{Driver}->{Name} eq 'Pg') { - push @where, "extract(hour from to_timestamp(cust_main.$field)) = $hour"; - } - else { - warn "search by time of day not supported on ".$dbh->{Driver}->{Name}." databases"; - } - } - - $orderby ||= "ORDER BY cust_main.$field"; - - } - - ### - # classnum - ### - - if ( $params->{'classnum'} ) { - - my @classnum = ref( $params->{'classnum'} ) - ? @{ $params->{'classnum'} } - : ( $params->{'classnum'} ); - - @classnum = grep /^(\d*)$/, @classnum; - - if ( @classnum ) { - push @where, '( '. join(' OR ', map { - $_ ? "cust_main.classnum = $_" - : "cust_main.classnum IS NULL" - } - @classnum - ). - ' )'; - } - - } - - ### - # 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 - ### - - if ( $params->{'invoice_terms'} =~ /^([\w ]+)$/ ) { - my $terms = $1; - if ( $1 eq 'NULL' ) { - push @where, - "( cust_main.invoice_terms IS NULL OR cust_main.invoice_terms = '' )"; - } else { - push @where, - "cust_main.invoice_terms IS NOT NULL", - "cust_main.invoice_terms = '$1'"; - } - } - - ## - # amounts - ## - - if ( $params->{'current_balance'} ) { - - #my $balance_sql = $class->balance_sql(); - my $balance_sql = FS::cust_main->balance_sql(); - - my @current_balance = - ref( $params->{'current_balance'} ) - ? @{ $params->{'current_balance'} } - : ( $params->{'current_balance'} ); - - push @where, map { s/current_balance/$balance_sql/; $_ } - @current_balance; - - } - - ## - # custbatch - ## - - if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) { - push @where, - "cust_main.custbatch = '$1'"; - } - - ## - # setup queries, subs, etc. for the search - ## - - $orderby ||= 'ORDER BY custnum'; - - # here is the agent virtualization - push @where, $FS::CurrentUser::CurrentUser->agentnums_sql; - - my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; - - my $addl_from = 'LEFT JOIN cust_pkg USING ( custnum ) '; - - my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql"; - - my $select = join(', ', - 'cust_main.custnum', - FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), - ); - - my(@extra_headers) = (); - my(@extra_fields) = (); - - if ($params->{'flattened_pkgs'}) { - - if ($dbh->{Driver}->{Name} eq 'Pg') { - - $select .= ", array_to_string(array(select pkg from cust_pkg left join part_pkg using ( pkgpart ) where cust_main.custnum = cust_pkg.custnum $pkgwhere),'|') as magic"; - - }elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) { - $select .= ", GROUP_CONCAT(pkg SEPARATOR '|') as magic"; - $addl_from .= " LEFT JOIN part_pkg using ( pkgpart )"; - }else{ - warn "warning: unknown database type ". $dbh->{Driver}->{Name}. - "omitting packing information from report."; - } - - my $header_query = "SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count FROM cust_main $addl_from $extra_sql $pkgwhere group by cust_main.custnum order by count desc limit 1"; - - my $sth = dbh->prepare($header_query) or die dbh->errstr; - $sth->execute() or die $sth->errstr; - my $headerrow = $sth->fetchrow_arrayref; - my $headercount = $headerrow ? $headerrow->[0] : 0; - while($headercount) { - unshift @extra_headers, "Package ". $headercount; - unshift @extra_fields, eval q!sub {my $c = shift; - my @a = split '\|', $c->magic; - my $p = $a[!.--$headercount. q!]; - $p; - };!; - } - - } - - my $sql_query = { - 'table' => 'cust_main', - 'select' => $select, - 'hashref' => {}, - 'extra_sql' => $extra_sql, - 'order_by' => $orderby, - 'count_query' => $count_query, - 'extra_headers' => \@extra_headers, - 'extra_fields' => \@extra_fields, - }; - -} - -=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ] - -Performs a fuzzy (approximate) search and returns the matching FS::cust_main -records. Currently, I, I, I and/or I may be -specified (the appropriate ship_ field is also searched). - -Additional options are the same as FS::Record::qsearch - -=cut - -sub fuzzy_search { - my( $self, $fuzzy, $hash, @opt) = @_; - #$self - $hash ||= {}; - my @cust_main = (); - - check_and_rebuild_fuzzyfiles(); - foreach my $field ( keys %$fuzzy ) { - - my $all = $self->all_X($field); - next unless scalar(@$all); - - my %match = (); - $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) ); - - my @fcust = (); - foreach ( keys %match ) { - push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt); - push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt); - } - my %fsaw = (); - push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust; - } - - # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes - my %saw = (); - @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main; - - @cust_main; - -} - -=item masked FIELD - -Returns a masked version of the named field - -=cut - -sub masked { -my ($self,$field) = @_; - -# Show last four - -'x'x(length($self->getfield($field))-4). - substr($self->getfield($field), (length($self->getfield($field))-4)); - + my $class = shift; + FS::cust_main::Search->search(@_); } =back @@ -5360,427 +5027,6 @@ my ($self,$field) = @_; =over 4 -=item smart_search OPTION => VALUE ... - -Accepts the following options: I, 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, 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 -(i.e. I). - -Returns a (possibly empty) array of FS::cust_main objects. - -=cut - -sub smart_search { - my %options = @_; - - #here is the agent virtualization - my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; - - my @cust_main = (); - - my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'}; - my $search = delete $options{'search'}; - ( my $alphanum_search = $search ) =~ s/\W//g; - - if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search - - #false laziness w/Record::ut_phone - my $phonen = "$1-$2-$3"; - $phonen .= " x$4" if $4; - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %options }, - 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). - ' ( '. - join(' OR ', map "$_ = '$phonen'", - qw( daytime night fax - ship_daytime ship_night ship_fax ) - ). - ' ) '. - " AND $agentnums_sql", #agent virtualization - } ); - - unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match - #try looking for matches with extensions unless one was specified - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %options }, - 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). - ' ( '. - join(' OR ', map "$_ LIKE '$phonen\%'", - qw( daytime night - ship_daytime ship_night ) - ). - ' ) '. - " AND $agentnums_sql", #agent virtualization - } ); - - } - - # custnum search (also try agent_custid), with some tweaking options if your - # legacy cust "numbers" have letters - } - - if ( $search =~ /^\s*(\d+)\s*$/ - || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' - && $search =~ /^\s*(\w\w?\d+)\s*$/ - ) - || ( $conf->exists('address1-search' ) - && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D - ) - ) - { - - my $num = $1; - - if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { 'custnum' => $num, %options }, - 'extra_sql' => " AND $agentnums_sql", #agent virtualization - } ); - } - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { 'agent_custid' => $num, %options }, - 'extra_sql' => " AND $agentnums_sql", #agent virtualization - } ); - - if ( $conf->exists('address1-search') ) { - my $len = length($num); - $num = lc($num); - foreach my $prefix ( '', 'ship_' ) { - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %options, }, - 'extra_sql' => - ( keys(%options) ? ' AND ' : ' WHERE ' ). - " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ". - " AND $agentnums_sql", - } ); - } - } - - } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) { - - my($company, $last, $first) = ( $1, $2, $3 ); - - # "Company (Last, First)" - #this is probably something a browser remembered, - #so just do an exact search (but case-insensitive, so USPS standardization - #doesn't throw a wrench in the works) - - foreach my $prefix ( '', 'ship_' ) { - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %options }, - 'extra_sql' => - ( keys(%options) ? ' AND ' : ' WHERE ' ). - join(' AND ', - " LOWER(${prefix}first) = ". dbh->quote(lc($first)), - " LOWER(${prefix}last) = ". dbh->quote(lc($last)), - " LOWER(${prefix}company) = ". dbh->quote(lc($company)), - $agentnums_sql, - ), - } ); - } - - } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search - # try (ship_){last,company} - - my $value = lc($1); - - # # remove "(Last, First)" in "Company (Last, First)", otherwise the - # # full strings the browser remembers won't work - # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name - - use Lingua::EN::NameParse; - my $NameParse = new Lingua::EN::NameParse( - auto_clean => 1, - allow_reversed => 1, - ); - - my($last, $first) = ( '', '' ); - #maybe disable this too and just rely on NameParse? - if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First - - ($last, $first) = ( $1, $2 ); - - #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) { - } elsif ( ! $NameParse->parse($value) ) { - - my %name = $NameParse->components; - $first = $name{'given_name_1'}; - $last = $name{'surname_1'}; - - } - - if ( $first && $last ) { - - my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) ); - - #exact - my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; - $sql .= " - ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first ) - OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first ) - )"; - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => \%options, - 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization - } ); - - # or it just be something that was typed in... (try that in a sec) - - } - - my $q_value = dbh->quote($value); - - #exact - my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; - $sql .= " ( LOWER(last) = $q_value - OR LOWER(company) = $q_value - OR LOWER(ship_last) = $q_value - OR LOWER(ship_company) = $q_value - "; - $sql .= " OR LOWER(address1) = $q_value - OR LOWER(ship_address1) = $q_value - " - if $conf->exists('address1-search'); - $sql .= " )"; - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => \%options, - 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization - } ); - - #no exact match, trying substring/fuzzy - #always do substring & fuzzy (unless they're explicity config'ed off) - #getting complaints searches are not returning enough - unless ( @cust_main && $skip_fuzzy || $conf->exists('disable-fuzzy') ) { - - #still some false laziness w/search (was search/cust_main.cgi) - - #substring - - my @hashrefs = ( - { 'company' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, }, - ); - - if ( $first && $last ) { - - push @hashrefs, - { 'first' => { op=>'ILIKE', value=>"%$first%" }, - 'last' => { op=>'ILIKE', value=>"%$last%" }, - }, - { 'ship_first' => { op=>'ILIKE', value=>"%$first%" }, - 'ship_last' => { op=>'ILIKE', value=>"%$last%" }, - }, - ; - - } else { - - push @hashrefs, - { 'last' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, }, - ; - } - - if ( $conf->exists('address1-search') ) { - push @hashrefs, - { 'address1' => { op=>'ILIKE', value=>"%$value%" }, }, - { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, }, - ; - } - - foreach my $hashref ( @hashrefs ) { - - push @cust_main, qsearch( { - 'table' => 'cust_main', - 'hashref' => { %$hashref, - %options, - }, - 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton - } ); - - } - - #fuzzy - my @fuzopts = ( - \%options, #hashref - '', #select - " AND $agentnums_sql", #extra_sql #agent virtualization - ); - - if ( $first && $last ) { - push @cust_main, FS::cust_main->fuzzy_search( - { 'last' => $last, #fuzzy hashref - 'first' => $first }, # - @fuzopts - ); - } - foreach my $field ( 'last', 'company' ) { - push @cust_main, - FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts ); - } - if ( $conf->exists('address1-search') ) { - push @cust_main, - FS::cust_main->fuzzy_search( { 'address1' => $value }, @fuzopts ); - } - - } - - } - - #eliminate duplicates - my %saw = (); - @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; - - @cust_main; - -} - -=item email_search - -Accepts the following options: I, the email address to search for. The -email address will be searched for as an email invoice destination and as an -svc_acct account. - -#Any additional options are treated as an additional qualifier on the search -#(i.e. I). - -Returns a (possibly empty) array of FS::cust_main objects (but usually just -none or one). - -=cut - -sub email_search { - my %options = @_; - - local($DEBUG) = 1; - - my $email = delete $options{'email'}; - - #we're only being used by RT at the moment... no agent virtualization yet - #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; - - my @cust_main = (); - - if ( $email =~ /([^@]+)\@([^@]+)/ ) { - - my ( $user, $domain ) = ( $1, $2 ); - - warn "$me smart_search: searching for $user in domain $domain" - if $DEBUG; - - push @cust_main, - map $_->cust_main, - qsearch( { - 'table' => 'cust_main_invoice', - 'hashref' => { 'dest' => $email }, - } - ); - - push @cust_main, - map $_->cust_main, - grep $_, - map $_->cust_svc->cust_pkg, - qsearch( { - 'table' => 'svc_acct', - 'hashref' => { 'username' => $user, }, - 'extra_sql' => - 'AND ( SELECT domain FROM svc_domain - WHERE svc_acct.domsvc = svc_domain.svcnum - ) = '. dbh->quote($domain), - } - ); - } - - my %saw = (); - @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; - - warn "$me smart_search: found ". scalar(@cust_main). " unique customers" - if $DEBUG; - - @cust_main; - -} - -=item check_and_rebuild_fuzzyfiles - -=cut - -sub check_and_rebuild_fuzzyfiles { - my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields -} - -=item rebuild_fuzzyfiles - -=cut - -sub rebuild_fuzzyfiles { - - use Fcntl qw(:flock); - - my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - mkdir $dir, 0700 unless -d $dir; - - foreach my $fuzzy ( @fuzzyfields ) { - - open(LOCK,">>$dir/cust_main.$fuzzy") - or die "can't open $dir/cust_main.$fuzzy: $!"; - flock(LOCK,LOCK_EX) - or die "can't lock $dir/cust_main.$fuzzy: $!"; - - open (CACHE,">$dir/cust_main.$fuzzy.tmp") - or die "can't open $dir/cust_main.$fuzzy.tmp: $!"; - - foreach my $field ( $fuzzy, "ship_$fuzzy" ) { - my $sth = dbh->prepare("SELECT $field FROM cust_main". - " WHERE $field != '' AND $field IS NOT NULL"); - $sth->execute or die $sth->errstr; - - while ( my $row = $sth->fetchrow_arrayref ) { - print CACHE $row->[0]. "\n"; - } - - } - - close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!"; - - rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy"; - close LOCK; - } - -} - -=item all_X - -=cut - -sub all_X { - my( $self, $field ) = @_; - my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; - open(CACHE,"<$dir/cust_main.$field") - or die "can't open $dir/cust_main.$field: $!"; - my @array = map { chomp; $_; } ; - close CACHE; - \@array; -} - =item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1 =cut diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index e0e24b134..bfcba69c6 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -4,6 +4,7 @@ use strict; use vars qw( $conf $DEBUG $me ); use FS::UID qw( dbh ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw( send_email ); use FS::payby; use FS::cust_pay; use FS::cust_pay_pending; diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm new file mode 100644 index 000000000..3813f891a --- /dev/null +++ b/FS/FS/cust_main/Search.pm @@ -0,0 +1,828 @@ +package FS::cust_main::Search; + +use strict; +use base qw( Exporter ); +use vars qw( @EXPORT_OK $DEBUG $me $conf @fuzzyfields ); +use String::Approx qw(amatch); +use FS::UID qw( dbh ); +use FS::Record qw( qsearch ); +use FS::cust_main; +use FS::cust_main_invoice; +use FS::svc_acct; + +@EXPORT_OK = qw( smart_search ); + +# 1 is mostly method/subroutine entry and options +# 2 traces progress of some operations +# 3 is even more information including possibly sensitive data +$DEBUG = 0; +$me = '[FS::cust_main::Search]'; + +@fuzzyfields = @FS::cust_main::fuzzyfields; + +install_callback FS::UID sub { + $conf = new FS::Conf; + #yes, need it for stuff below (prolly should be cached) +}; + +=head1 NAME + +FS::cust_main::Search - Customer searching + +=head1 SYNOPSIS + + use FS::cust_main::Search; + + FS::cust_main::Search::smart_search(%options); + + FS::cust_main::Search::email_search(%options); + + FS::cust_main::Search->search( \%options ); + + FS::cust_main::Search->fuzzy_search( \%fuzzy_hashref ); + +=head1 SUBROUTINES + +=over 4 + +=item smart_search OPTION => VALUE ... + +Accepts the following options: I, 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, 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 +(i.e. I). + +Returns a (possibly empty) array of FS::cust_main objects. + +=cut + +sub smart_search { + my %options = @_; + + #here is the agent virtualization + my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; + + my @cust_main = (); + + my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'}; + my $search = delete $options{'search'}; + ( my $alphanum_search = $search ) =~ s/\W//g; + + if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search + + #false laziness w/Record::ut_phone + my $phonen = "$1-$2-$3"; + $phonen .= " x$4" if $4; + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %options }, + 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). + ' ( '. + join(' OR ', map "$_ = '$phonen'", + qw( daytime night fax + ship_daytime ship_night ship_fax ) + ). + ' ) '. + " AND $agentnums_sql", #agent virtualization + } ); + + unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match + #try looking for matches with extensions unless one was specified + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %options }, + 'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ). + ' ( '. + join(' OR ', map "$_ LIKE '$phonen\%'", + qw( daytime night + ship_daytime ship_night ) + ). + ' ) '. + " AND $agentnums_sql", #agent virtualization + } ); + + } + + # custnum search (also try agent_custid), with some tweaking options if your + # legacy cust "numbers" have letters + } + + if ( $search =~ /^\s*(\d+)\s*$/ + || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' + && $search =~ /^\s*(\w\w?\d+)\s*$/ + ) + || ( $conf->exists('address1-search' ) + && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D + ) + ) + { + + my $num = $1; + + if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $num, %options }, + 'extra_sql' => " AND $agentnums_sql", #agent virtualization + } ); + } + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { 'agent_custid' => $num, %options }, + 'extra_sql' => " AND $agentnums_sql", #agent virtualization + } ); + + if ( $conf->exists('address1-search') ) { + my $len = length($num); + $num = lc($num); + foreach my $prefix ( '', 'ship_' ) { + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %options, }, + 'extra_sql' => + ( keys(%options) ? ' AND ' : ' WHERE ' ). + " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ". + " AND $agentnums_sql", + } ); + } + } + + } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) { + + my($company, $last, $first) = ( $1, $2, $3 ); + + # "Company (Last, First)" + #this is probably something a browser remembered, + #so just do an exact search (but case-insensitive, so USPS standardization + #doesn't throw a wrench in the works) + + foreach my $prefix ( '', 'ship_' ) { + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %options }, + 'extra_sql' => + ( keys(%options) ? ' AND ' : ' WHERE ' ). + join(' AND ', + " LOWER(${prefix}first) = ". dbh->quote(lc($first)), + " LOWER(${prefix}last) = ". dbh->quote(lc($last)), + " LOWER(${prefix}company) = ". dbh->quote(lc($company)), + $agentnums_sql, + ), + } ); + } + + } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search + # try (ship_){last,company} + + my $value = lc($1); + + # # remove "(Last, First)" in "Company (Last, First)", otherwise the + # # full strings the browser remembers won't work + # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name + + use Lingua::EN::NameParse; + my $NameParse = new Lingua::EN::NameParse( + auto_clean => 1, + allow_reversed => 1, + ); + + my($last, $first) = ( '', '' ); + #maybe disable this too and just rely on NameParse? + if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First + + ($last, $first) = ( $1, $2 ); + + #} elsif ( $value =~ /^(.+)\s+(.+)$/ ) { + } elsif ( ! $NameParse->parse($value) ) { + + my %name = $NameParse->components; + $first = $name{'given_name_1'}; + $last = $name{'surname_1'}; + + } + + if ( $first && $last ) { + + my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) ); + + #exact + my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; + $sql .= " + ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first ) + OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first ) + )"; + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => \%options, + 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization + } ); + + # or it just be something that was typed in... (try that in a sec) + + } + + my $q_value = dbh->quote($value); + + #exact + my $sql = scalar(keys %options) ? ' AND ' : ' WHERE '; + $sql .= " ( LOWER(last) = $q_value + OR LOWER(company) = $q_value + OR LOWER(ship_last) = $q_value + OR LOWER(ship_company) = $q_value + "; + $sql .= " OR LOWER(address1) = $q_value + OR LOWER(ship_address1) = $q_value + " + if $conf->exists('address1-search'); + $sql .= " )"; + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => \%options, + 'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization + } ); + + #no exact match, trying substring/fuzzy + #always do substring & fuzzy (unless they're explicity config'ed off) + #getting complaints searches are not returning enough + unless ( @cust_main && $skip_fuzzy || $conf->exists('disable-fuzzy') ) { + + #still some false laziness w/search (was search/cust_main.cgi) + + #substring + + my @hashrefs = ( + { 'company' => { op=>'ILIKE', value=>"%$value%" }, }, + { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, }, + ); + + if ( $first && $last ) { + + push @hashrefs, + { 'first' => { op=>'ILIKE', value=>"%$first%" }, + 'last' => { op=>'ILIKE', value=>"%$last%" }, + }, + { 'ship_first' => { op=>'ILIKE', value=>"%$first%" }, + 'ship_last' => { op=>'ILIKE', value=>"%$last%" }, + }, + ; + + } else { + + push @hashrefs, + { 'last' => { op=>'ILIKE', value=>"%$value%" }, }, + { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, }, + ; + } + + if ( $conf->exists('address1-search') ) { + push @hashrefs, + { 'address1' => { op=>'ILIKE', value=>"%$value%" }, }, + { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, }, + ; + } + + foreach my $hashref ( @hashrefs ) { + + push @cust_main, qsearch( { + 'table' => 'cust_main', + 'hashref' => { %$hashref, + %options, + }, + 'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton + } ); + + } + + #fuzzy + my @fuzopts = ( + \%options, #hashref + '', #select + " AND $agentnums_sql", #extra_sql #agent virtualization + ); + + if ( $first && $last ) { + push @cust_main, FS::cust_main::Search->fuzzy_search( + { 'last' => $last, #fuzzy hashref + 'first' => $first }, # + @fuzopts + ); + } + foreach my $field ( 'last', 'company' ) { + push @cust_main, + FS::cust_main::Search->fuzzy_search( { $field => $value }, @fuzopts ); + } + if ( $conf->exists('address1-search') ) { + push @cust_main, + FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, @fuzopts ); + } + + } + + } + + #eliminate duplicates + my %saw = (); + @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; + + @cust_main; + +} + +=item email_search + +Accepts the following options: I, the email address to search for. The +email address will be searched for as an email invoice destination and as an +svc_acct account. + +#Any additional options are treated as an additional qualifier on the search +#(i.e. I). + +Returns a (possibly empty) array of FS::cust_main objects (but usually just +none or one). + +=cut + +sub email_search { + my %options = @_; + + local($DEBUG) = 1; + + my $email = delete $options{'email'}; + + #we're only being used by RT at the moment... no agent virtualization yet + #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; + + my @cust_main = (); + + if ( $email =~ /([^@]+)\@([^@]+)/ ) { + + my ( $user, $domain ) = ( $1, $2 ); + + warn "$me smart_search: searching for $user in domain $domain" + if $DEBUG; + + push @cust_main, + map $_->cust_main, + qsearch( { + 'table' => 'cust_main_invoice', + 'hashref' => { 'dest' => $email }, + } + ); + + push @cust_main, + map $_->cust_main, + grep $_, + map $_->cust_svc->cust_pkg, + qsearch( { + 'table' => 'svc_acct', + 'hashref' => { 'username' => $user, }, + 'extra_sql' => + 'AND ( SELECT domain FROM svc_domain + WHERE svc_acct.domsvc = svc_domain.svcnum + ) = '. dbh->quote($domain), + } + ); + } + + my %saw = (); + @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; + + warn "$me smart_search: found ". scalar(@cust_main). " unique customers" + if $DEBUG; + + @cust_main; + +} + +=back + +=head1 CLASS METHODS + +=over 4 + +=item search HASHREF + +(Class method) + +Returns a qsearch hash expression to search for parameters specified in +HASHREF. Valid parameters are + +=over 4 + +=item agentnum + +=item status + +=item cancelled_pkgs + +bool + +=item signupdate + +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 cust_fields + +=item flattened_pkgs + +bool + +=back + +=cut + +sub search { + my ($class, $params) = @_; + + my $dbh = dbh; + + my @where = (); + my $orderby; + + ## + # parse agent + ## + + if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) { + push @where, + "cust_main.agentnum = $1"; + } + + ## + # do the same for user + ## + + if ( $params->{'usernum'} =~ /^(\d+)$/ and $1 ) { + push @where, + "cust_main.usernum = $1"; + } + + ## + # parse status + ## + + #prospect ordered active inactive suspended cancelled + if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) { + my $method = $params->{'status'}. '_sql'; + #push @where, $class->$method(); + push @where, FS::cust_main->$method(); + } + + ## + # parse cancelled package checkbox + ## + + my $pkgwhere = ""; + + $pkgwhere .= "AND (cancel = 0 or cancel is null)" + unless $params->{'cancelled_pkgs'}; + + ## + # parse without census tract checkbox + ## + + push @where, "(censustract = '' or censustract is null)" + if $params->{'no_censustract'}; + + ## + # dates + ## + + foreach my $field (qw( signupdate )) { + + next unless exists($params->{$field}); + + my($beginning, $ending, $hour) = @{$params->{$field}}; + + push @where, + "cust_main.$field IS NOT NULL", + "cust_main.$field >= $beginning", + "cust_main.$field <= $ending"; + + # XXX: do this for mysql and/or pull it out of here + if(defined $hour) { + if ($dbh->{Driver}->{Name} eq 'Pg') { + push @where, "extract(hour from to_timestamp(cust_main.$field)) = $hour"; + } + else { + warn "search by time of day not supported on ".$dbh->{Driver}->{Name}." databases"; + } + } + + $orderby ||= "ORDER BY cust_main.$field"; + + } + + ### + # classnum + ### + + if ( $params->{'classnum'} ) { + + my @classnum = ref( $params->{'classnum'} ) + ? @{ $params->{'classnum'} } + : ( $params->{'classnum'} ); + + @classnum = grep /^(\d*)$/, @classnum; + + if ( @classnum ) { + push @where, '( '. join(' OR ', map { + $_ ? "cust_main.classnum = $_" + : "cust_main.classnum IS NULL" + } + @classnum + ). + ' )'; + } + + } + + ### + # 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 + ### + + if ( $params->{'invoice_terms'} =~ /^([\w ]+)$/ ) { + my $terms = $1; + if ( $1 eq 'NULL' ) { + push @where, + "( cust_main.invoice_terms IS NULL OR cust_main.invoice_terms = '' )"; + } else { + push @where, + "cust_main.invoice_terms IS NOT NULL", + "cust_main.invoice_terms = '$1'"; + } + } + + ## + # amounts + ## + + if ( $params->{'current_balance'} ) { + + #my $balance_sql = $class->balance_sql(); + my $balance_sql = FS::cust_main->balance_sql(); + + my @current_balance = + ref( $params->{'current_balance'} ) + ? @{ $params->{'current_balance'} } + : ( $params->{'current_balance'} ); + + push @where, map { s/current_balance/$balance_sql/; $_ } + @current_balance; + + } + + ## + # custbatch + ## + + if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) { + push @where, + "cust_main.custbatch = '$1'"; + } + + ## + # setup queries, subs, etc. for the search + ## + + $orderby ||= 'ORDER BY custnum'; + + # here is the agent virtualization + push @where, $FS::CurrentUser::CurrentUser->agentnums_sql; + + my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : ''; + + my $addl_from = 'LEFT JOIN cust_pkg USING ( custnum ) '; + + my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql"; + + my $select = join(', ', + 'cust_main.custnum', + FS::UI::Web::cust_sql_fields($params->{'cust_fields'}), + ); + + my(@extra_headers) = (); + my(@extra_fields) = (); + + if ($params->{'flattened_pkgs'}) { + + if ($dbh->{Driver}->{Name} eq 'Pg') { + + $select .= ", array_to_string(array(select pkg from cust_pkg left join part_pkg using ( pkgpart ) where cust_main.custnum = cust_pkg.custnum $pkgwhere),'|') as magic"; + + }elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) { + $select .= ", GROUP_CONCAT(pkg SEPARATOR '|') as magic"; + $addl_from .= " LEFT JOIN part_pkg using ( pkgpart )"; + }else{ + warn "warning: unknown database type ". $dbh->{Driver}->{Name}. + "omitting packing information from report."; + } + + my $header_query = "SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count FROM cust_main $addl_from $extra_sql $pkgwhere group by cust_main.custnum order by count desc limit 1"; + + my $sth = dbh->prepare($header_query) or die dbh->errstr; + $sth->execute() or die $sth->errstr; + my $headerrow = $sth->fetchrow_arrayref; + my $headercount = $headerrow ? $headerrow->[0] : 0; + while($headercount) { + unshift @extra_headers, "Package ". $headercount; + unshift @extra_fields, eval q!sub {my $c = shift; + my @a = split '\|', $c->magic; + my $p = $a[!.--$headercount. q!]; + $p; + };!; + } + + } + + my $sql_query = { + 'table' => 'cust_main', + 'select' => $select, + 'hashref' => {}, + 'extra_sql' => $extra_sql, + 'order_by' => $orderby, + 'count_query' => $count_query, + 'extra_headers' => \@extra_headers, + 'extra_fields' => \@extra_fields, + }; + +} + +=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ] + +Performs a fuzzy (approximate) search and returns the matching FS::cust_main +records. Currently, I, I, I and/or I may be +specified (the appropriate ship_ field is also searched). + +Additional options are the same as FS::Record::qsearch + +=cut + +sub fuzzy_search { + my( $self, $fuzzy, $hash, @opt) = @_; + #$self + $hash ||= {}; + my @cust_main = (); + + check_and_rebuild_fuzzyfiles(); + foreach my $field ( keys %$fuzzy ) { + + my $all = $self->all_X($field); + next unless scalar(@$all); + + my %match = (); + $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) ); + + my @fcust = (); + foreach ( keys %match ) { + push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt); + push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt); + } + my %fsaw = (); + push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust; + } + + # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes + my %saw = (); + @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main; + + @cust_main; + +} + +=back + +=head1 UTILITY SUBROUTINES + +=over 4 + +=item check_and_rebuild_fuzzyfiles + +=cut + +sub check_and_rebuild_fuzzyfiles { + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields +} + +=item rebuild_fuzzyfiles + +=cut + +sub rebuild_fuzzyfiles { + + use Fcntl qw(:flock); + + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + mkdir $dir, 0700 unless -d $dir; + + foreach my $fuzzy ( @fuzzyfields ) { + + open(LOCK,">>$dir/cust_main.$fuzzy") + or die "can't open $dir/cust_main.$fuzzy: $!"; + flock(LOCK,LOCK_EX) + or die "can't lock $dir/cust_main.$fuzzy: $!"; + + open (CACHE,">$dir/cust_main.$fuzzy.tmp") + or die "can't open $dir/cust_main.$fuzzy.tmp: $!"; + + foreach my $field ( $fuzzy, "ship_$fuzzy" ) { + my $sth = dbh->prepare("SELECT $field FROM cust_main". + " WHERE $field != '' AND $field IS NOT NULL"); + $sth->execute or die $sth->errstr; + + while ( my $row = $sth->fetchrow_arrayref ) { + print CACHE $row->[0]. "\n"; + } + + } + + close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!"; + + rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy"; + close LOCK; + } + +} + +=item all_X + +=cut + +sub all_X { + my( $self, $field ) = @_; + my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + open(CACHE,"<$dir/cust_main.$field") + or die "can't open $dir/cust_main.$field: $!"; + my @array = map { chomp; $_; } ; + close CACHE; + \@array; +} + +=head1 BUGS + +Bed bugs + +=head1 SEE ALSO + +L, L + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index 64c7a0696..af1041db4 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -71,6 +71,7 @@ FS/cust_main.pm FS/cust_main/Billing.pm FS/cust_main/Billing_Realtime.pm FS/cust_main/Import.pm +FS/cust_main/Search.pm FS/cust_main_Mixin.pm FS/cust_main_county.pm FS/cust_main_invoice.pm diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi index 93519b740..93f960f5c 100755 --- a/httemplate/search/cust_main.cgi +++ b/httemplate/search/cust_main.cgi @@ -634,7 +634,7 @@ % } % % if ( $last_type{'Fuzzy'} || $last_type{'All'} ) { -% push @cust_main, FS::cust_main->fuzzy_search( { 'last' => $last } ); +% push @cust_main, FS::cust_main::Search->fuzzy_search( { 'last' => $last } ); % } % % #if ($last_type{'Sound-alike'}) { @@ -681,7 +681,7 @@ % } % % if ( $company_type{'Fuzzy'} || $company_type{'All'} ) { -% push @cust_main, FS::cust_main->fuzzy_search( { 'company' => $company } ); +% push @cust_main, FS::cust_main::Search->fuzzy_search( { 'company' => $company } ); % } % % if ($company_type{'Sound-alike'}) { diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html index 4465c3324..b6db4e434 100755 --- a/httemplate/search/cust_main.html +++ b/httemplate/search/cust_main.html @@ -84,7 +84,7 @@ $search_hash{'current_balance'} = # etc ### -my $sql_query = FS::cust_main->search(\%search_hash); +my $sql_query = FS::cust_main::Search->search(\%search_hash); my $count_query = delete($sql_query->{'count_query'}); my @extra_headers = @{ delete($sql_query->{'extra_headers'}) }; my @extra_fields = @{ delete($sql_query->{'extra_fields'}) }; diff --git a/rt/lib/RT/URI/freeside/Internal.pm b/rt/lib/RT/URI/freeside/Internal.pm index bd7c42ccf..b5af134ac 100644 --- a/rt/lib/RT/URI/freeside/Internal.pm +++ b/rt/lib/RT/URI/freeside/Internal.pm @@ -104,13 +104,13 @@ sub FreesideGetConfig { sub smart_search { #Subroutine - return map { { $_->hash } } &FS::cust_main::smart_search(@_); + return map { { $_->hash } } &FS::cust_main::Search::smart_search(@_); } sub email_search { #Subroutine - return map { { $_->hash } } &FS::cust_main::email_search(@_); + return map { { $_->hash } } &FS::cust_main::Search::email_search(@_); } -- 2.11.0