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::Location
FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
- FS::geocode_Mixin
+ FS::geocode_Mixin FS::Quotable_Mixin
FS::o2m_Common
FS::Record
);
use File::Temp; #qw( tempfile );
use Business::CreditCard 0.28;
use Locale::Country;
-use FS::UID qw( getotaker dbh driver_name );
+use FS::UID qw( dbh driver_name );
use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
use FS::Misc qw( generate_email send_email generate_ps do_print );
use FS::Msgcat qw(gettext);
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;
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
$payby = 'PREP' if $amount;
- } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+ } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
$payby = $1;
$self->payby('BILL');
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;
}
}
- 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;
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';
tie my %financial_tables, 'Tie::IxHash',
'cust_bill' => 'invoices',
+ 'cust_bill_void' => 'voided invoices',
'cust_statement' => 'statements',
'cust_credit' => 'credits',
'cust_pay' => 'payments',
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)$/
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 ) {
|| $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_snumbern('spouse_birthdate')
+ || $self->ut_snumbern('anniversary_date')
|| $self->ut_textn('company')
|| $self->ut_anything('comments')
|| $self->ut_numbern('referral_custnum')
|| $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 ])
;
+ my $company = $self->company;
+ $company =~ s/^\s+//;
+ $company =~ s/\s+$//;
+ $company =~ s/\s+/ /g;
+ $self->company($company);
+
#barf. need message catalogs. i18n. etc.
$error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
}
- #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);
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 );
L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
runs the payment using a realtime gateway.
+Options may include:
+
+B<amount>: the amount to be paid; defaults to the customer's balance minus
+any payments in transit.
+
+B<payby>: the payment method; defaults to cust_main.payby
+
+B<realtime>: runs this as a realtime payment instead of adding it to a
+batch. Deprecated.
+
+B<invnum>: sets cust_pay_batch.invnum.
+
+B<address1>, B<address2>, B<city>, B<state>, B<zip>, B<country>: sets
+the billing address for the payment; defaults to the customer's billing
+location.
+
+B<payinfo>, B<paydate>, B<payname>: sets the payment account, expiration
+date, and name; defaults to those fields in cust_main.
+
=cut
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
} );
'setuptax' => '', # or 'Y' for tax exempt
+ 'locationnum'=> 1234, # optional
+
#internal taxation
'taxclass' => 'Tax class',
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;
$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;
'quantity' => $quantity,
'start_date' => $start_date,
'no_auto' => $no_auto,
+ 'locationnum'=> $locationnum,
} );
$error = $cust_pkg->insert;
=cut
+=item cust_bill_void
+
+Returns all the voided invoices (see L<FS::cust_bill_void>) 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 : { @_ };
=item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
-Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
+Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
Optionally, a list or hashref of additional arguments to the qsearch call can
be passed.
$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);
+#}
+
+=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
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
}
#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)
#
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