+ my $locationnum = '';
+ foreach my $cust_pkg ( $self->all_pkgs ) {
+ $cust_pkg->custnum($new_custnum);
+
+ unless ( $cust_pkg->locationnum ) {
+ unless ( $locationnum ) {
+ my $cust_location = new FS::cust_location {
+ $self->location_hash,
+ 'custnum' => $new_custnum,
+ };
+ my $error = $cust_location->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $locationnum = $cust_location->locationnum;
+ }
+ $cust_pkg->locationnum($locationnum);
+ }
+
+ my $error = $cust_pkg->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ # add customer (ship) name to svc_phone.phone_name if blank
+ my @cust_svc = $cust_pkg->cust_svc;
+ foreach my $cust_svc (@cust_svc) {
+ my($label, $value, $svcdb) = $cust_svc->label;
+ next unless $svcdb eq 'svc_phone';
+ my $svc_phone = $cust_svc->svc_x;
+ next if $svc_phone->phone_name;
+ $svc_phone->phone_name($name);
+ my $error = $svc_phone->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ #not considered:
+ # cust_tax_exempt (texas tax exemptions)
+ # cust_recon (some sort of not-well understood thing for OnPac)
+
+ #these are moved over
+ foreach my $table (qw(
+ cust_tag cust_location contact cust_attachment cust_main_note
+ cust_tax_adjustment cust_pay_batch queue
+ )) {
+ foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
+ $record->custnum($new_custnum);
+ my $error = $record->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ }
+
+ #these aren't preserved
+ foreach my $table (qw(
+ cust_main_exemption cust_main_invoice
+ )) {
+ foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
+ my $error = $record->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ }
+
+
+ my $sth = $dbh->prepare(
+ 'UPDATE cust_main SET referral_custnum = ? WHERE referral_custnum = ?'
+ ) or do {
+ my $errstr = $dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $sth->execute($new_custnum, $self->custnum) or do {
+ my $errstr = $sth->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+
+ #tickets
+
+ my $ticket_dbh = '';
+ if ($conf->config('ticket_system') eq 'RT_Internal') {
+ $ticket_dbh = $dbh;
+ } elsif ($conf->config('ticket_system') eq 'RT_External') {
+ my ($datasrc, $user, $pass) = $conf->config('ticket_system-rt_external_datasrc');
+ $ticket_dbh = DBI->connect($datasrc, $user, $pass, { 'ChopBlanks' => 1 });
+ #or die "RT_External DBI->connect error: $DBI::errstr\n";
+ }
+
+ if ( $ticket_dbh ) {
+
+ my $ticket_sth = $ticket_dbh->prepare(
+ 'UPDATE Links SET Target = ? WHERE Target = ?'
+ ) or do {
+ my $errstr = $ticket_dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $ticket_sth->execute('freeside://freeside/cust_main/'.$new_custnum,
+ 'freeside://freeside/cust_main/'.$self->custnum)
+ or do {
+ my $errstr = $ticket_sth->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+
+ }
+
+ #delete the customer record
+
+ my $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+To change the customer's address, set the pseudo-fields C<bill_location> and
+C<ship_location>. The address will still only change if at least one of the
+address fields differs from the existing values.
+
+INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
+be set as the invoicing list (see L<"invoicing_list">). Errors return as
+expected and rollback the entire transaction; it is not necessary to call
+check_invoicing_list first. Here's an example:
+
+ $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
+
+Currently available options are: I<tax_exemption>.
+
+The I<tax_exemption> option can be set to an arrayref of tax names or a hashref
+of tax names and exemption numbers. FS::cust_main_exemption records will be
+deleted and inserted as appropriate.
+
+=cut
+
+sub replace {
+ my $self = shift;
+
+ my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+ ? shift
+ : $self->replace_old;
+
+ my @param = @_;
+
+ warn "$me replace called\n"
+ if $DEBUG;
+
+ my $curuser = $FS::CurrentUser::CurrentUser;
+ if ( $self->payby eq 'COMP'
+ && $self->payby ne $old->payby
+ && ! $curuser->access_right('Complimentary customer')
+ )
+ {
+ return "You are not permitted to create complimentary accounts.";
+ }
+
+ local($ignore_expired_card) = 1
+ if $old->payby =~ /^(CARD|DCRD)$/
+ && $self->payby =~ /^(CARD|DCRD)$/
+ && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
+
+ local($ignore_banned_card) = 1
+ if ( $old->payby =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/
+ || $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
+ && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
+
+ if ( $self->payby =~ /^(CARD|DCRD)$/
+ && $old->payinfo ne $self->payinfo
+ && $old->paymask ne $self->paymask )
+ {
+ my $error = $self->check_payinfo_cardtype;
+ return $error if $error;
+
+ if ( $conf->exists('business-onlinepayment-verification') ) {
+ #need to standardize paydate for this, false laziness with check
+ my( $m, $y );
+ if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+ ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
+ } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+ ( $m, $y ) = ( $2, "19$1" );
+ } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
+ ( $m, $y ) = ( $3, "20$2" );
+ } else {
+ return "Illegal expiration date: ". $self->paydate;
+ }
+ $m = sprintf('%02d',$m);
+ $self->paydate("$y-$m-01");
+
+ $error = $self->realtime_verify_bop({ 'method'=>'CC' });
+ return $error if $error;
+ }
+ }
+
+ return "Invoicing locale is required"
+ if $old->locale
+ && ! $self->locale
+ && $conf->exists('cust_main-require_locale');
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ for my $l (qw(bill_location ship_location)) {
+ my $old_loc = $old->$l;
+ my $new_loc = $self->$l;
+
+ # 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 $error;
+ }
+ $self->set($l.'num', $new_loc->locationnum);
+ } #for $l
+
+ # replace the customer record
+ my $error = $self->SUPER::replace($old);
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ # now move packages to the new service location
+ $self->set('ship_location', ''); #flush cache
+ if ( $old->ship_locationnum and # should only be null during upgrade...
+ $old->ship_locationnum != $self->ship_locationnum ) {
+ $error = $old->ship_location->move_to($self->ship_location);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ # don't move packages based on the billing location, but
+ # disable it if it's no longer in use
+ if ( $old->bill_locationnum and
+ $old->bill_locationnum != $self->bill_locationnum ) {
+ $error = $old->bill_location->disable_if_unused;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
+ my $invoicing_list = shift @param;
+ $error = $self->check_invoicing_list( $invoicing_list );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $self->invoicing_list( $invoicing_list );
+ }
+
+ if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
+
+ #this could be more efficient than deleting and re-inserting, if it matters
+ foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
+ my $error = $cust_tag->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+ my $cust_tag = new FS::cust_tag { 'tagnum' => $tagnum,
+ 'custnum' => $self->custnum };
+ my $error = $cust_tag->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ my %options = @param;
+
+ my $tax_exemption = delete $options{'tax_exemption'};
+ if ( $tax_exemption ) {
+
+ $tax_exemption = { map { $_ => '' } @$tax_exemption }
+ if ref($tax_exemption) eq 'ARRAY';