X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=9a83099de2a568ba87277c8a7e785138169287dc;hb=f786ebaff8b6704e4e180428aaaa9afeaea7ecb9;hp=d18ec1cc7e482c4afe9206d224857210daa11c3d;hpb=aa1210004d3249eb1c9616f748b97f2ce91de9ae;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index d18ec1cc7..9a83099de 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2,13 +2,15 @@ package FS::cust_main; require 5.006; use strict; - #FS::cust_main:_Marketgear when they're ready to move to 2.1 use base qw( FS::cust_main::Packages FS::cust_main::Status + FS::cust_main::NationalID FS::cust_main::Billing FS::cust_main::Billing_Realtime FS::cust_main::Billing_Discount + FS::cust_main::Billing_ThirdParty FS::cust_main::Location + FS::cust_main::Credit_Limit FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin - FS::geocode_Mixin + FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin FS::o2m_Common FS::Record ); @@ -42,6 +44,7 @@ use FS::payby; use FS::cust_pkg; use FS::cust_svc; use FS::cust_bill; +use FS::cust_bill_void; use FS::legacy_cust_bill; use FS::cust_pay; use FS::cust_pay_pending; @@ -72,6 +75,7 @@ use FS::cust_main_note; use FS::cust_attachment; use FS::contact; use FS::Locales; +use FS::upgrade_journal; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -388,7 +392,7 @@ sub insert { $payby = 'PREP' if $amount; - } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) { + } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) { $payby = $1; $self->payby('BILL'); @@ -452,8 +456,10 @@ sub insert { warn " setting $l.custnum\n" if $DEBUG > 1; my $loc = $self->$l; - $loc->set(custnum => $self->custnum); - $error ||= $loc->replace; + unless ( $loc->custnum ) { + $loc->set(custnum => $self->custnum); + $error ||= $loc->replace; + } if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -546,14 +552,6 @@ sub insert { } } - if ( $self->can('start_copy_skel') ) { - my $error = $self->start_copy_skel; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - warn " ordering packages\n" if $DEBUG > 1; @@ -1241,9 +1239,12 @@ sub merge { return "Can't merge a customer into self" if $self->custnum == $new_custnum; - unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) { - return "Invalid new customer number: $new_custnum"; - } + my $new_cust_main = qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) + or return "Invalid new customer number: $new_custnum"; + + return 'Access denied: "Merge customer across agents" access right required to merge into a customer of a different agent' + if $self->agentnum != $new_cust_main->agentnum + && ! $FS::CurrentUser::CurrentUser->access_right('Merge customer across agents'); local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -1277,12 +1278,14 @@ sub merge { } tie my %financial_tables, 'Tie::IxHash', - 'cust_bill' => 'invoices', - 'cust_statement' => 'statements', - 'cust_credit' => 'credits', - 'cust_pay' => 'payments', - 'cust_pay_void' => 'voided payments', - 'cust_refund' => 'refunds', + 'cust_bill' => 'invoices', + 'cust_bill_void' => 'voided invoices', + 'cust_statement' => 'statements', + 'cust_credit' => 'credits', + 'cust_credit_void' => 'voided credits', + 'cust_pay' => 'payments', + 'cust_pay_void' => 'voided payments', + 'cust_refund' => 'refunds', ; foreach my $table ( keys %financial_tables ) { @@ -1479,20 +1482,6 @@ sub replace { return "You are not permitted to create complimentary accounts."; } - # should be unnecessary--geocode will default to null on new locations - #if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode') - # && $conf->exists('enable_taxproducts') - # ) - #{ - # my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip) - # ? 'ship_' : ''; - # $self->set('geocode', '') - # if $old->get($pre.'zip') ne $self->get($pre.'zip') - # && length($self->get($pre.'zip')) >= 10; - #} - - # set_coord/coord_auto stuff is now handled by cust_location - local($ignore_expired_card) = 1 if $old->payby =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/ @@ -1523,43 +1512,17 @@ sub replace { my $old_loc = $old->$l; my $new_loc = $self->$l; - if ( !$new_loc->locationnum ) { - # changing location - # If the new location is all empty fields, or if it's identical to - # the old location in all fields, don't replace. - my @nonempty = grep { $new_loc->$_ } $self->location_fields; - next if !@nonempty; - my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields; - - if ( @unlike or $old_loc->disabled ) { - warn " changed $l fields: ".join(',',@unlike)."\n" - if $DEBUG; - $new_loc->set(custnum => $self->custnum); - - # insert it--the old location will be disabled later - my $error = $new_loc->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - - } else { - # no fields have changed and $old_loc isn't disabled, so don't change it - next; - } - - } - elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) { + # find the existing location if there is one + $new_loc->set('custnum' => $self->custnum); + my $error = $new_loc->find_or_insert; + if ( $error ) { $dbh->rollback if $oldAutoCommit; - return "$l belongs to customer ".$new_loc->custnum; + return $error; } - # else the new location belongs to this customer so we're good - - # set the foo_locationnum now that we have one. $self->set($l.'num', $new_loc->locationnum); - } #for $l + # replace the customer record my $error = $self->SUPER::replace($old); if ( $error ) { @@ -1734,13 +1697,25 @@ sub queue_fuzzyfiles_update { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + foreach my $field ( 'first', 'last', 'company', 'ship_company' ) { + my $queue = new FS::queue { + 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield' + }; + my @args = "cust_main.$field", $self->get($field); + my $error = $queue->insert( @args ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "queueing job (transaction rolled back): $error"; + } + } + my @locations = $self->bill_location; push @locations, $self->ship_location if $self->has_ship_address; foreach my $location (@locations) { my $queue = new FS::queue { - 'job' => 'FS::cust_main::Search::append_fuzzyfiles' + 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield' }; - my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields; + my @args = 'cust_location.address1', $location->address1; my $error = $queue->insert( @args ); if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -1775,12 +1750,18 @@ sub check { || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum') || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum') || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum') + || $self->ut_foreign_keyn('salesnum', 'sales', 'salesnum') || $self->ut_textn('custbatch') || $self->ut_name('last') || $self->ut_name('first') - || $self->ut_snumbern('birthdate') || $self->ut_snumbern('signupdate') + || $self->ut_snumbern('birthdate') + || $self->ut_namen('spouse_last') + || $self->ut_namen('spouse_first') + || $self->ut_snumbern('spouse_birthdate') + || $self->ut_snumbern('anniversary_date') || $self->ut_textn('company') + || $self->ut_textn('ship_company') || $self->ut_anything('comments') || $self->ut_numbern('referral_custnum') || $self->ut_textn('stateid') @@ -1789,12 +1770,22 @@ sub check { || $self->ut_floatn('cdr_termination_percentage') || $self->ut_floatn('credit_limit') || $self->ut_numbern('billday') - || $self->ut_enum('edit_subject', [ '', 'Y' ] ) - || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] ) - || $self->ut_enum('invoice_noemail', [ '', 'Y' ] ) + || $self->ut_numbern('prorate_day') + || $self->ut_flag('edit_subject') + || $self->ut_flag('calling_list_exempt') + || $self->ut_flag('invoice_noemail') + || $self->ut_flag('message_noemail') || $self->ut_enum('locale', [ '', FS::Locales->locales ]) ; + foreach (qw(company ship_company)) { + my $company = $self->get($_); + $company =~ s/^\s+//; + $company =~ s/\s+$//; + $company =~ s/\s+/ /g; + $self->set($_, $company); + } + #barf. need message catalogs. i18n. etc. $error .= "Please select an advertising source." if $error =~ /^Illegal or empty \(numeric\) refnum: /; @@ -1850,8 +1841,6 @@ sub check { } - #ship_ fields are gone - #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/ # or return "Illegal payby: ". $self->payby; #$self->payby($1); @@ -2027,7 +2016,8 @@ sub check { if ( $self->paydate eq '' || $self->paydate eq '-' ) { return "Expiration date required" - unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/; + # shouldn't payinfo_check do this? + unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/; $self->paydate(''); } else { my( $m, $y ); @@ -2055,9 +2045,17 @@ sub check { ) { $self->payname( $self->first. " ". $self->getfield('last') ); } else { - $self->payname =~ /^([\w \,\.\-\'\&]+)$/ - or return gettext('illegal_name'). " payname: ". $self->payname; - $self->payname($1); + + if ( $self->payby =~ /^(CHEK|DCHK)$/ ) { + $self->payname =~ /^([\w \,\.\-\']*)$/ + or return gettext('illegal_name'). " payname: ". $self->payname; + $self->payname($1); + } else { + $self->payname =~ /^([\w \,\.\-\'\&]*)$/ + or return gettext('illegal_name'). " payname: ". $self->payname; + $self->payname($1); + } + } return "Please select an invoicing locale" @@ -2462,6 +2460,25 @@ Adds a payment for this invoice to the pending credit card batch (see L), or, if the B option is set to a true value, runs the payment using a realtime gateway. +Options may include: + +B: the amount to be paid; defaults to the customer's balance minus +any payments in transit. + +B: the payment method; defaults to cust_main.payby + +B: runs this as a realtime payment instead of adding it to a +batch. Deprecated. + +B: sets cust_pay_batch.invnum. + +B, B, B, B, B, B: sets +the billing address for the payment; defaults to the customer's billing +location. + +B, B, B: sets the payment account, expiration +date, and name; defaults to those fields in cust_main. + =cut sub batch_card { @@ -2539,10 +2556,10 @@ sub batch_card { 'state' => $options{state} || $loc->state, 'zip' => $options{zip} || $loc->zip, 'country' => $options{country} || $loc->country, - 'payby' => $options{payby} || $loc->payby, - 'payinfo' => $options{payinfo} || $loc->payinfo, - 'exp' => $options{paydate} || $loc->paydate, - 'payname' => $options{payname} || $loc->payname, + 'payby' => $options{payby} || $self->payby, + 'payinfo' => $options{payinfo} || $self->payinfo, + 'exp' => $options{paydate} || $self->paydate, + 'payname' => $options{payname} || $self->payname, 'amount' => $amount, # consolidating } ); @@ -2822,7 +2839,7 @@ UNIX timestamps; see L). Also see L and L for conversion functions. The empty string can be passed to disable that time constraint completely. -Available options are: +Accepts the same options as L: =over 4 @@ -2830,6 +2847,12 @@ Available options are: set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering) +=item cutoff + +An absolute cutoff time. Payments, credits, and refunds I after this +time will be ignored. Note that START_TIME and END_TIME only limit the date +range for invoices and I payments, credits, and refunds. + =back =cut @@ -3335,6 +3358,8 @@ reason, and a 'reason_type' option must be passed to indicate the FS::reason_type for the new reason. An I option may be passed to set the credit's I field. +Likewise for I, I, I and +I. Any other options are passed to FS::cust_credit::insert. @@ -3360,10 +3385,10 @@ sub credit { $cust_credit->set('reason', $reason) } - for (qw( addlinfo eventnum )) { - $cust_credit->$_( delete $options{$_} ) - if exists($options{$_}); - } + $cust_credit->$_( delete $options{$_} ) + foreach grep exists($options{$_}), + qw( addlinfo eventnum ), + map "commission_$_", qw( agentnum salesnum pkgnum ); $cust_credit->insert(%options); @@ -3388,6 +3413,8 @@ New-style, with a hashref of options: 'setuptax' => '', # or 'Y' for tax exempt + 'locationnum'=> 1234, # optional + #internal taxation 'taxclass' => 'Tax class', @@ -3419,6 +3446,7 @@ sub charge { my $no_auto = ''; my $cust_pkg_ref = ''; my ( $bill_now, $invoice_terms ) = ( 0, '' ); + my $locationnum; if ( ref( $_[0] ) ) { $amount = $_[0]->{amount}; $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1; @@ -3436,6 +3464,7 @@ sub charge { $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : ''; $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : ''; $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : ''; + $locationnum = $_[0]->{locationnum} || $self->ship_locationnum; } else { $amount = shift; $quantity = 1; @@ -3502,6 +3531,7 @@ sub charge { 'quantity' => $quantity, 'start_date' => $start_date, 'no_auto' => $no_auto, + 'locationnum'=> $locationnum, } ); $error = $cust_pkg->insert; @@ -3624,6 +3654,20 @@ be passed. =cut +=item cust_bill_void + +Returns all the voided invoices (see L) for this customer. + +=cut + +sub cust_bill_void { + my $self = shift; + + map { $_ } #return $self->num_cust_bill_void unless wantarray; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } ) +} + sub cust_statement { my $self = shift; my $opt = ref($_[0]) ? shift : { @_ }; @@ -3720,6 +3764,19 @@ sub cust_credit_pkgnum { ); } +=item cust_credit_void + +Returns all voided credits (see L) for this customer. + +=cut + +sub cust_credit_void { + my $self = shift; + map { $_ } + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } ) +} + =item cust_pay Returns all the payments (see L) for this customer. @@ -3780,7 +3837,7 @@ sub cust_pay_void { =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ] -Returns all batched payments (see L) for this customer. +Returns all batched payments (see L) for this customer. Optionally, a list or hashref of additional arguments to the qsearch call can be passed. @@ -4052,15 +4109,44 @@ sub ship_contact_firstlast { $contact->get('first') . ' '. $contact->get('last'); } -=item country_full +#XXX this doesn't work in 3.x+ +#=item country_full +# +#Returns this customer's full country name +# +#=cut +# +#sub country_full { +# my $self = shift; +# code2country($self->country); +#} + +sub bill_country_full { + my $self = shift; + code2country($self->bill_location->country); +} + +sub ship_country_full { + my $self = shift; + code2country($self->ship_location->country); +} + +=item county_state_county [ PREFIX ] -Returns this customer's full country name +Returns a string consisting of just the county, state and country. =cut -sub country_full { +sub county_state_country { my $self = shift; - code2country($self->country); + my $locationnum; + if ( @_ && $_[0] && $self->has_ship_address ) { + $locationnum = $self->ship_locationnum; + } else { + $locationnum = $self->bill_locationnum; + } + my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum }); + $cust_location->county_state_country; } =item geocode DATA_VENDOR @@ -4139,14 +4225,17 @@ sub cust_statuscolor { __PACKAGE__->statuscolors->{$self->cust_status}; } -=item tickets +=item tickets [ STATUS ] Returns an array of hashes representing the customer's RT tickets. +An optional status (or arrayref or hashref of statuses) may be specified. + =cut sub tickets { my $self = shift; + my $status = ( @_ && $_[0] ) ? shift : ''; my $num = $conf->config('cust_main-max_tickets') || 10; my @tickets = (); @@ -4154,7 +4243,12 @@ sub tickets { if ( $conf->config('ticket_system') ) { unless ( $conf->config('ticket_system-custom_priority_field') ) { - @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) }; + @tickets = @{ FS::TicketSystem->customer_tickets( $self->custnum, + $num, + undef, + $status, + ) + }; } else { @@ -4166,6 +4260,7 @@ sub tickets { @{ FS::TicketSystem->customer_tickets( $self->custnum, $num - scalar(@tickets), $priority, + $status, ) }; } @@ -4875,15 +4970,18 @@ sub queueable_print { my %opt = @_; my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } ) - or die "invalid customer number: " . $opt{custvnum}; + or die "invalid customer number: " . $opt{custnum}; - my $error = $self->print( $opt{template} ); + my $error = $self->print( { 'template' => $opt{template} } ); die $error if $error; } sub print { my ($self, $template) = (shift, shift); - do_print [ $self->print_ps($template) ]; + do_print( + [ $self->print_ps($template) ], + 'agentnum' => $self->agentnum, + ); } #these three subs should just go away once agent stuff is all config overrides @@ -4973,6 +5071,9 @@ sub queued_bill { my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } ); warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid + #without this errors don't get rolled back + $args{'fatal'} = 1; # runs from job queue, will be caught + $cust_main->bill_and_collect( %args ); } @@ -4988,49 +5089,13 @@ sub process_bill_and_collect { $cust_main->bill_and_collect( %$param ); } -=item process_censustract_update CUSTNUM - -Queueable function to update the census tract to the current year (as set in -the 'census_year' configuration variable) and retrieve the new tract code. - -=cut - -sub process_censustract_update { - eval "use FS::Misc::Geo qw(get_censustract)"; - die $@ if $@; - my $custnum = shift; - my $cust_main = qsearchs( 'cust_main', { custnum => $custnum }) - or die "custnum '$custnum' not found!\n"; - - my $new_year = $conf->config('census_year') or return; - my $new_tract = get_censustract({ $cust_main->location_hash }, $new_year); - if ( $new_tract =~ /^\d/ ) { - # then it's a tract code - $cust_main->set('censustract', $new_tract); - $cust_main->set('censusyear', $new_year); - - local($ignore_expired_card) = 1; - local($ignore_illegal_zip) = 1; - local($ignore_banned_card) = 1; - local($skip_fuzzyfiles) = 1; - local($import) = 1; #prevent automatic geocoding (need its own variable?) - my $error = $cust_main->replace; - die $error if $error; - } - else { - # it's an error message - die $new_tract; - } - return; -} - #starting to take quite a while for big dbs +# (JRNL: journaled so it only happens once per database) # - seq scan of h_cust_main (yuck), but not going to index paycvv, so -# - seq scan of cust_main on signupdate... index signupdate? will that help? -# - seq scan of cust_main on paydate... index on substrings? maybe set an -# upgrade journal flag now that we have that, yyyy-m-dd paydates are ancient -# - seq scan of cust_main on payinfo.. certainly not going toi ndex that... -# upgrade journal again? this is also an ancient problem +# JRNL seq scan of cust_main on signupdate... index signupdate? will that help? +# JRNL seq scan of cust_main on paydate... index on substrings? maybe set an +# JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that... +# JRNL leading/trailing spaces in first, last, company # - otaker upgrade? journal and call it good? (double check to make sure # we're not still setting otaker here) # @@ -5041,38 +5106,74 @@ sub _upgrade_data { #class method my @statements = ( 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL', - 'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL', ); - # fix yyyy-m-dd formatted paydates - if ( driver_name =~ /^mysql/i ) { + + #this seems to be the only expensive one.. why does it take so long? + unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) { push @statements, - "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + 'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL'; + FS::upgrade_journal->set_done('cust_main__signupdate'); } - else { # the SQL standard - push @statements, - "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + + unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) { + + # fix yyyy-m-dd formatted paydates + if ( driver_name =~ /^mysql/i ) { + push @statements, + "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + } else { # the SQL standard + push @statements, + "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'"; + } + FS::upgrade_journal->set_done('cust_main__paydate'); } - push @statements, #fix the weird BILL with a cc# in payinfo problem - #DCRD to be safe - "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' ); + unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) { + + push @statements, #fix the weird BILL with a cc# in payinfo problem + #DCRD to be safe + "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' ); + + FS::upgrade_journal->set_done('cust_main__payinfo'); + + } my $t = time; foreach my $sql ( @statements ) { my $sth = dbh->prepare($sql) or die dbh->errstr; $sth->execute or die $sth->errstr; - warn (time - $t). " seconds\n"; - $t = time; + #warn ( (time - $t). " seconds\n" ); + #$t = time; } local($ignore_expired_card) = 1; local($ignore_banned_card) = 1; local($skip_fuzzyfiles) = 1; local($import) = 1; #prevent automatic geocoding (need its own variable?) - $class->_upgrade_otaker(%opts); FS::cust_main::Location->_upgrade_data(%opts); + unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) { + + foreach my $cust_main ( qsearch({ + 'table' => 'cust_main', + 'hashref' => {}, + 'extra_sql' => 'WHERE '. + join(' OR ', + map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '% %'", + qw( first last company ) + ), + }) ) { + my $error = $cust_main->replace; + die $error if $error; + } + + FS::upgrade_journal->set_done('cust_main__trimspaces'); + + } + + $class->_upgrade_otaker(%opts); + } =back