RT# 78547 Allow for simulated billing within a transaction
[freeside.git] / FS / FS / cust_main.pm
index d9ca150..aa8ae2a 100644 (file)
@@ -79,6 +79,7 @@ use FS::sales;
 use FS::cust_payby;
 use FS::contact;
 use FS::reason;
+use FS::Misc::Savepoint;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -377,6 +378,10 @@ sub insert {
        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
     if $DEBUG;
 
+  return "You are not permitted to change customer invoicing terms."
+    if $self->invoice_terms #i.e. not the default
+    && ! $FS::CurrentUser::CurrentUser->access_right('Edit customer invoice terms');
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -1382,6 +1387,10 @@ sub replace {
     && ! $self->locale
     && $conf->exists('cust_main-require_locale');
 
+  return "You are not permitted to change customer invoicing terms."
+    if $old->invoice_terms ne $self->invoice_terms
+    && ! $curuser->access_right('Edit customer invoice terms');
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -1506,6 +1515,16 @@ sub replace {
     $implicit_contact->set('emailaddress', $email);
     $implicit_contact->set('invoice_dest', 'Y');
     $implicit_contact->set('custnum', $self->custnum);
+    my $i_cust_contact =
+      qsearchs('cust_contact', {
+                                 contactnum  => $implicit_contact->contactnum,
+                                 custnum     => $self->custnum,
+                               }
+      );
+    if ( $i_cust_contact ) {
+      $implicit_contact->set($_, $i_cust_contact->$_)
+        foreach qw( classnum selfservice_access comment );
+    }
 
     my $error;
     if ( $implicit_contact->contactnum ) {
@@ -2423,11 +2442,15 @@ sub cancel_pkgs {
   my( $self, %opt ) = @_;
 
   # we're going to cancel services, which is not reversible
+  #   unless exports are suppressed
   die "cancel_pkgs cannot be run inside a transaction"
-    if $FS::UID::AutoCommit == 0;
+    if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
 
+  my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
 
+  savepoint_create('cancel_pkgs');
+
   return ( 'access denied' )
     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
@@ -2444,7 +2467,8 @@ sub cancel_pkgs {
       my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
       my $error = $ban->insert;
       if ($error) {
-        dbh->rollback;
+        savepoint_rollback_and_release('cancel_pkgs');
+        dbh->rollback if $oldAutoCommit;
         return ( $error );
       }
 
@@ -2464,13 +2488,14 @@ sub cancel_pkgs {
                              'time'     => $cancel_time );
     if ($error) {
       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
-      dbh->rollback;
+      savepoint_rollback_and_release('cancel_pkgs');
+      dbh->rollback if $oldAutoCommit;
       return ( "Error billing during cancellation: $error" );
     }
   }
-  dbh->commit;
+  savepoint_release('cancel_pkgs');
+  dbh->commit if $oldAutoCommit;
 
-  $FS::UID::AutoCommit = 1;
   my @errors;
   # now cancel all services, the same way we would for individual packages.
   # if any of them fail, cancel the rest anyway.
@@ -2483,11 +2508,23 @@ sub cancel_pkgs {
   warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
     $self->custnum."\n"
     if $DEBUG;
+  my $i = 0;
   foreach my $cust_svc (@sorted_cust_svc) {
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     my $part_svc = $cust_svc->part_svc;
     next if ( defined($part_svc) and $part_svc->preserve );
-    my $error = $cust_svc->cancel; # immediate cancel, no date option
-    push @errors, $error if $error;
+    # immediate cancel, no date option
+    # transactionize individually
+    my $error = try { $cust_svc->cancel } catch { $_ };
+    if ( $error ) {
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
+      push @errors, $error;
+    } else {
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
+    }
   }
   if (@errors) {
     return @errors;
@@ -2502,8 +2539,11 @@ sub cancel_pkgs {
     @cprs = @{ delete $opt{'cust_pkg_reason'} };
   }
   my $null_reason;
+  $i = 0;
   foreach (@pkgs) {
     my %lopt = %opt;
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     if (@cprs) {
       my $cpr = shift @cprs;
       if ( $cpr ) {
@@ -2523,7 +2563,14 @@ sub cancel_pkgs {
       }
     }
     my $error = $_->cancel(%lopt);
-    push @errors, 'pkgnum '.$_->pkgnum.': '.$error if $error;
+    if ( $error ) {
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
+      push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
+    } else {
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
+    }
   }
 
   return @errors;
@@ -3481,6 +3528,7 @@ sub charge {
   my $cust_pkg_ref = '';
   my ( $bill_now, $invoice_terms ) = ( 0, '' );
   my $locationnum;
+  my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
   if ( ref( $_[0] ) ) {
     $amount     = $_[0]->{amount};
     $setup_cost = $_[0]->{setup_cost};
@@ -3501,6 +3549,9 @@ sub charge {
     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
     $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
     $separate_bill = $_[0]->{separate_bill} || '';
+    $discountnum = $_[0]->{setup_discountnum};
+    $discountnum_amount = $_[0]->{setup_discountnum_amount};
+    $discountnum_percent = $_[0]->{setup_discountnum_percent};
   } else { # yuck
     $amount     = shift;
     $setup_cost = '';
@@ -3564,13 +3615,16 @@ sub charge {
   }
 
   my $cust_pkg = new FS::cust_pkg ( {
-    'custnum'    => $self->custnum,
-    'pkgpart'    => $pkgpart,
-    'quantity'   => $quantity,
-    'start_date' => $start_date,
-    'no_auto'    => $no_auto,
-    'separate_bill' => $separate_bill,
-    'locationnum'=> $locationnum,
+    'custnum'                   => $self->custnum,
+    'pkgpart'                   => $pkgpart,
+    'quantity'                  => $quantity,
+    'start_date'                => $start_date,
+    'no_auto'                   => $no_auto,
+    'separate_bill'             => $separate_bill,
+    'locationnum'               => $locationnum,
+    'setup_discountnum'         => $discountnum,
+    'setup_discountnum_amount'  => $discountnum_amount,
+    'setup_discountnum_percent' => $discountnum_percent,
   } );
 
   $error = $cust_pkg->insert;
@@ -3639,6 +3693,20 @@ sub num_cust_attachments_deleted {
   );
 }
 
+=item max_invnum
+
+Returns the most recent invnum (invoice number) for this customer.
+
+=cut
+
+sub max_invnum {
+  my $self = shift;
+  $self->scalar_sql(
+    " SELECT MAX(invnum) FROM cust_bill WHERE custnum = ?",
+    $self->custnum
+  );
+}
+
 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
 
 Returns all the invoices (see L<FS::cust_bill>) for this customer.
@@ -4041,6 +4109,27 @@ sub name {
   $name;
 }
 
+=item batch_payment_payname
+
+Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company,
+based on if a company name exists and is the account being used a business account.
+
+=cut
+
+sub batch_payment_payname {
+  my $self = shift;
+  my $cust_pay_batch = shift;
+  my $name;
+
+  if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; }
+  else { $name = $self->first .' '. $self->last; }
+
+  $name = $self->company
+    if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company);
+
+  $name;
+}
+
 =item service_contact
 
 Returns the L<FS::contact> object for this customer that has the 'Service'
@@ -4792,6 +4881,8 @@ PAYBYLOOP:
         next if grep(/^$field$/, qw( custpaybynum payby weight ) );
         next if grep(/^$field$/, @preserve );
         next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
+        # check if paymask exists,  if so stop and don't save, no need for a duplicate.
+        return '' if $new->get('paymask') eq $cust_payby->get('paymask');
       }
       # now check fields that can replace if one value is blank
       my $replace = 0;
@@ -5632,6 +5723,16 @@ sub process_bill_and_collect {
   $cust_main->bill_and_collect( %$param );
 }
 
+=item pending_invoice_count
+
+Return number of cust_bill with pending=Y for this customer
+
+=cut
+
+sub pending_invoice_count {
+  FS::cust_bill->count( 'custnum = '.shift->custnum."AND pending = 'Y'" );
+}
+
 #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
@@ -5732,15 +5833,17 @@ sub _upgrade_data { #class method
       # at the time we do this, also migrate paytype into cust_pay_batch
       # so that batches that are open before the migration can still be 
       # processed
-      my @cust_pay_batch = qsearch('cust_pay_batch', {
-          'custnum' => $cust_main->custnum,
-          'payby'   => 'CHEK',
-          'paytype' => '',
-      });
-      foreach my $cust_pay_batch (@cust_pay_batch) {
-        $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
-        my $error = $cust_pay_batch->replace;
-        die "$error (setting cust_pay_batch.paytype)" if $error;
+      if ( $cust_main->get('paytype') ) {
+        my @cust_pay_batch = qsearch('cust_pay_batch', {
+            'custnum' => $cust_main->custnum,
+            'payby'   => 'CHEK',
+            'paytype' => '',
+        });
+        foreach my $cust_pay_batch (@cust_pay_batch) {
+          $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
+          my $error = $cust_pay_batch->replace;
+          die "$error (setting cust_pay_batch.paytype)" if $error;
+        }
       }
 
       $cust_main->complimentary('Y') if $cust_main->payby eq 'COMP';
@@ -5871,12 +5974,13 @@ sub _upgrade_next_recnum {
   my $recnum = shift @$recnums;
   return $recnum if $recnum;
   my $tclass = 'FS::'.$table;
+  my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : '';
   my $sql = 'SELECT '.$tclass->primary_key.
             ' FROM '.$table.
             ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
             "   AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
-            "   AND ( length(payinfo) > 80 OR paycardtype = 'Tokenized' ) ".
-            ' ORDER BY '.$tclass->primary_key.' LIMIT 500';;
+            "   AND ( length(payinfo) < 80$paycardtypecheck ) ".
+            ' ORDER BY '.$tclass->primary_key.' LIMIT 500';
   my $sth = $dbh->prepare($sql) or die $dbh->errstr;
   $sth->execute() or die $sth->errstr;
   my @recnums;