omit from history
[freeside.git] / FS / FS / cust_pay.pm
index c80729a..20132fe 100644 (file)
@@ -22,6 +22,7 @@ use FS::cust_pay_refund;
 use FS::cust_main;
 use FS::cust_pkg;
 use FS::cust_pay_void;
+use FS::upgrade_journal;
 
 $DEBUG = 0;
 
@@ -36,6 +37,7 @@ FS::UID->install_callback( sub {
 } );
 
 @encrypted_fields = ('payinfo');
+sub nohistory_fields { ('payinfo'); }
 
 =head1 NAME
 
@@ -113,6 +115,22 @@ books closed flag, empty or `Y'
 
 Desired pkgnum when using experimental package balances.
 
+=item bank
+
+The bank where the payment was deposited.
+
+=item depositor
+
+The name of the depositor.
+
+=item account
+
+The deposit account number.
+
+=item teller
+
+The teller number.
+
 =back
 
 =head1 METHODS
@@ -189,29 +207,50 @@ sub insert {
 
   if ( my $credit_type = $conf->config('prepayment_discounts-credit_type') ) {
     if ( my $months = $self->discount_term ) {
-      #hmmm... error handling
-      my ($credit, $savings, $total) = 
-        $cust_main->discount_term_values($months);
+      # XXX this should be moved out somewhere, but discount_term_values
+      # doesn't fit right
+      my ($cust_bill) = ($cust_main->cust_bill)[-1]; # most recent invoice
+      return "can't accept prepayment for an unbilled customer" if !$cust_bill;
+
+      # %billing_pkgs contains this customer's active monthly packages. 
+      # Recurring fees for those packages will be credited and then rebilled 
+      # for the full discount term.  Other packages on the last invoice 
+      # (canceled, non-monthly recurring, or one-time charges) will be 
+      # left as they are.
+      my %billing_pkgs = map { $_->pkgnum => $_ } 
+                         grep { $_->part_pkg->freq eq '1' } 
+                         $cust_main->billing_pkgs;
+      my $credit = 0; # sum of recurring charges from that invoice
+      my $last_bill_date = 0; # the real bill date
+      foreach my $item ( $cust_bill->cust_bill_pkg ) {
+        next if !exists($billing_pkgs{$item->pkgnum}); # skip inactive packages
+        $credit += $item->recur;
+        $last_bill_date = $item->cust_pkg->last_bill 
+          if defined($item->cust_pkg) 
+            and $item->cust_pkg->last_bill > $last_bill_date
+      }
+
       my $cust_credit = new FS::cust_credit {
         'custnum' => $self->custnum,
-        'amount'  => $credit,
+        'amount'  => sprintf('%.2f', $credit),
         'reason'  => 'customer chose to prepay for discount',
       };
       $error = $cust_credit->insert('reason_type' => $credit_type);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
-        return "error inserting cust_pay: $error";
+        return "error inserting prepayment credit: $error";
       }
-      my @pkgs = $cust_main->_discount_pkgs_and_bill;
-      my $cust_bill = shift(@pkgs);
-      @pkgs = &FS::cust_main::Billing::_discountable_pkgs_at_term($months, @pkgs);
-      $_->bill($_->last_bill) foreach @pkgs;
-      $error = $cust_main->bill( 
-        'recurring_only' => 1,
-        'time'           => $cust_bill->invoice_date,
+      # don't apply it yet
+
+      # bill for the entire term
+      $_->bill($_->last_bill) foreach (values %billing_pkgs);
+      $error = $cust_main->bill(
+        # no recurring_only, we want unbilled packages with start dates to 
+        # get billed
         'no_usage_reset' => 1,
-        'pkg_list'       => \@pkgs,
-        'freq_override'   => $months,
+        'time'           => $last_bill_date, # not $cust_bill->_date
+        'pkg_list'       => [ values %billing_pkgs ],
+        'freq_override'  => $months,
       );
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
@@ -227,6 +266,8 @@ sub insert {
         $dbh->rollback if $oldAutoCommit;
         return "balance after prepay discount attempt: $new_balance";
       }
+      # user friendly: override the "apply only to this invoice" mode
+      $self->invnum('');
       
     }
 
@@ -470,8 +511,11 @@ sub check {
     || $self->ut_textn('payunique')
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_textn('bank')
+    || $self->ut_alphan('depositor')
+    || $self->ut_numbern('account')
+    || $self->ut_numbern('teller')
     || $self->payinfo_check()
-    || $self->ut_numbern('discount_term')
   ;
   return $error if $error;
 
@@ -486,6 +530,12 @@ sub check {
   return "invalid discount_term"
    if ($self->discount_term && $self->discount_term < 2);
 
+  if ( $self->payby eq 'CASH' and $conf->exists('require_cash_deposit_info') ) {
+    foreach (qw(bank depositor account teller)) {
+      return "$_ required" if $self->get($_) eq '';
+    }
+  }
+
 #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it
 #  # UNIQUE index should catch this too, without race conditions, but this
 #  # should give a better error message the other 99.9% of the time...
@@ -534,7 +584,7 @@ sub send_receipt {
 
   my $conf = new FS::Conf;
 
-  return '' unless $conf->exists('payment_receipt', $cust_main->agentnum);
+  return '' unless $conf->config_bool('payment_receipt', $cust_main->agentnum);
 
   my @invoicing_list = $cust_main->invoicing_list_emailonly;
   return '' unless @invoicing_list;
@@ -607,7 +657,7 @@ sub send_receipt {
 
     }
 
-  } else { #not manual
+  } elsif ( ! $cust_main->invoice_noemail ) { #not manual
 
     my $queue = new FS::queue {
        'paynum' => $self->paynum,
@@ -805,93 +855,103 @@ sub _upgrade_data {  #class method
   # otaker/ivan upgrade
   ##
 
-  #not the most efficient, but hey, it only has to run once
+  unless ( FS::upgrade_journal->is_done('cust_pay__otaker_ivan') ) {
 
-  my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
-              "  AND usernum IS NULL ".
-              "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
-              "              WHERE cust_main.custnum = cust_pay.custnum ) ";
+    #not the most efficient, but hey, it only has to run once
 
-  my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
+    my $where = "WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' ) ".
+                "  AND usernum IS NULL ".
+                "  AND 0 < ( SELECT COUNT(*) FROM cust_main                 ".
+                "              WHERE cust_main.custnum = cust_pay.custnum ) ";
 
-  my $sth = dbh->prepare($count_sql) or die dbh->errstr;
-  $sth->execute or die $sth->errstr;
-  my $total = $sth->fetchrow_arrayref->[0];
-  #warn "$total cust_pay records to update\n"
-  #  if $DEBUG;
-  local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
+    my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
 
-  my $count = 0;
-  my $lastprog = 0;
+    my $sth = dbh->prepare($count_sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+    my $total = $sth->fetchrow_arrayref->[0];
+    #warn "$total cust_pay records to update\n"
+    #  if $DEBUG;
+    local($DEBUG) = 2 if $total > 1000; #could be a while, force progress info
 
-  my @cust_pay = qsearch( {
-      'table'     => 'cust_pay',
-      'hashref'   => {},
-      'extra_sql' => $where,
-      'order_by'  => 'ORDER BY paynum',
-  } );
+    my $count = 0;
+    my $lastprog = 0;
 
-  foreach my $cust_pay (@cust_pay) {
+    my @cust_pay = qsearch( {
+        'table'     => 'cust_pay',
+        'hashref'   => {},
+        'extra_sql' => $where,
+        'order_by'  => 'ORDER BY paynum',
+    } );
 
-    my $h_cust_pay = $cust_pay->h_search('insert');
-    if ( $h_cust_pay ) {
-      next if $cust_pay->otaker eq $h_cust_pay->history_user;
-      #$cust_pay->otaker($h_cust_pay->history_user);
-      $cust_pay->set('otaker', $h_cust_pay->history_user);
-    } else {
-      $cust_pay->set('otaker', 'legacy');
-    }
+    foreach my $cust_pay (@cust_pay) {
 
-    delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
-    my $error = $cust_pay->replace;
+      my $h_cust_pay = $cust_pay->h_search('insert');
+      if ( $h_cust_pay ) {
+        next if $cust_pay->otaker eq $h_cust_pay->history_user;
+        #$cust_pay->otaker($h_cust_pay->history_user);
+        $cust_pay->set('otaker', $h_cust_pay->history_user);
+      } else {
+        $cust_pay->set('otaker', 'legacy');
+      }
 
-    if ( $error ) {
-      warn " *** WARNING: Error updating order taker for payment paynum ".
-           $cust_pay->paynun. ": $error\n";
-      next;
-    }
+      delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
+      my $error = $cust_pay->replace;
 
-    $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
+      if ( $error ) {
+        warn " *** WARNING: Error updating order taker for payment paynum ".
+             $cust_pay->paynun. ": $error\n";
+        next;
+      }
+
+      $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
+
+      $count++;
+      if ( $DEBUG > 1 && $lastprog + 30 < time ) {
+        warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
+        $lastprog = time;
+      }
 
-    $count++;
-    if ( $DEBUG > 1 && $lastprog + 30 < time ) {
-      warn "$me $count/$total (". sprintf('%.2f',100*$count/$total). '%)'. "\n";
-      $lastprog = time;
     }
 
+    FS::upgrade_journal->set_done('cust_pay__otaker_ivan');
   }
 
   ###
   # payinfo N/A upgrade
   ###
 
-  #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
+  unless ( FS::upgrade_journal->is_done('cust_pay__payinfo_na') ) {
 
-  my @na_cust_pay = qsearch( {
-    'table'     => 'cust_pay',
-    'hashref'   => {}, #could be encrypted# { 'payinfo' => 'N/A' },
-    'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
-  } );
+    #XXX remove the 'N/A (tokenized)' part (or just this entire thing)
 
-  foreach my $na ( @na_cust_pay ) {
+    my @na_cust_pay = qsearch( {
+      'table'     => 'cust_pay',
+      'hashref'   => {}, #could be encrypted# { 'payinfo' => 'N/A' },
+      'extra_sql' => "WHERE ( payinfo = 'N/A' OR paymask = 'N/AA' OR paymask = 'N/A (tokenized)' ) AND payby IN ( 'CARD', 'CHEK' )",
+    } );
 
-    next unless $na->payinfo eq 'N/A';
+    foreach my $na ( @na_cust_pay ) {
+
+      next unless $na->payinfo eq 'N/A';
+
+      my $cust_pay_pending =
+        qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
+      unless ( $cust_pay_pending ) {
+        warn " *** WARNING: not-yet recoverable N/A card for payment ".
+             $na->paynum. " (no cust_pay_pending)\n";
+        next;
+      }
+      $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
+      my $error = $na->replace;
+      if ( $error ) {
+        warn " *** WARNING: Error updating payinfo for payment paynum ".
+             $na->paynun. ": $error\n";
+        next;
+      }
 
-    my $cust_pay_pending =
-      qsearchs('cust_pay_pending', { 'paynum' => $na->paynum } );
-    unless ( $cust_pay_pending ) {
-      warn " *** WARNING: not-yet recoverable N/A card for payment ".
-           $na->paynum. " (no cust_pay_pending)\n";
-      next;
-    }
-    $na->$_($cust_pay_pending->$_) for qw( payinfo paymask );
-    my $error = $na->replace;
-    if ( $error ) {
-      warn " *** WARNING: Error updating payinfo for payment paynum ".
-           $na->paynun. ": $error\n";
-      next;
     }
 
+    FS::upgrade_journal->set_done('cust_pay__payinfo_na');
   }
 
   ###