Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / cust_pay.pm
index 80a1e3e..5d4f67f 100644 (file)
@@ -116,6 +116,10 @@ books closed flag, empty or `Y'
 
 Desired pkgnum when using experimental package balances.
 
 
 Desired pkgnum when using experimental package balances.
 
+=item no_auto_apply
+
+Flag to only allow manual application of payment, empty or 'Y'
+
 =item bank
 
 The bank where the payment was deposited.
 =item bank
 
 The bank where the payment was deposited.
@@ -190,7 +194,13 @@ If the additional field discount_term is defined then a prepayment discount
 is taken for that length of time.  It is an error for the customer to owe
 after this payment is made.
 
 is taken for that length of time.  It is an error for the customer to owe
 after this payment is made.
 
-A hash of optional arguments may be passed.  Currently "manual" is supported.
+A hash of optional arguments may be passed.  The following arguments are
+supported:
+
+=over 4
+
+=item manual
+
 If true, a payment receipt is sent instead of a statement when
 'payment_receipt_email' configuration option is set.
 
 If true, a payment receipt is sent instead of a statement when
 'payment_receipt_email' configuration option is set.
 
@@ -203,6 +213,13 @@ payment is created directly from the web interface, from a user-initiated
 realtime payment, or from a third-party payment via self-service.  It should
 be I<false> when creating a payment from a billing event or from a batch.
 
 realtime payment, or from a third-party payment via self-service.  It should
 be I<false> when creating a payment from a billing event or from a batch.
 
+=item noemail
+
+Don't send an email receipt.  (Note: does not currently work when
+payment_receipt-trigger is set to something other than default / cust_bill)
+
+=back
+
 =cut
 
 sub insert {
 =cut
 
 sub insert {
@@ -226,6 +243,12 @@ sub insert {
         $dbh->rollback if $oldAutoCommit;
         return "Unknown cust_bill.invnum: ". $self->invnum;
       };
         $dbh->rollback if $oldAutoCommit;
         return "Unknown cust_bill.invnum: ". $self->invnum;
       };
+    if ($self->custnum && ($cust_bill->custnum ne $self->custnum)) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Invoice custnum ".$cust_bill->custnum
+        ." does not match specified custnum ".$self->custnum
+        ." for invoice ".$self->invnum;
+    }
     $self->custnum($cust_bill->custnum );
   }
 
     $self->custnum($cust_bill->custnum );
   }
 
@@ -379,6 +402,7 @@ sub insert {
   if ( $trigger eq 'cust_pay' ) {
     my $error = $self->send_receipt(
       'manual'    => $options{'manual'},
   if ( $trigger eq 'cust_pay' ) {
     my $error = $self->send_receipt(
       'manual'    => $options{'manual'},
+      'noemail'   => $options{'noemail'},
       'cust_bill' => $cust_bill,
       'cust_main' => $cust_main,
     );
       'cust_bill' => $cust_bill,
       'cust_main' => $cust_main,
     );
@@ -519,6 +543,7 @@ sub check {
     || $self->ut_textn('paybatch')
     || $self->ut_textn('payunique')
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_textn('paybatch')
     || $self->ut_textn('payunique')
     || $self->ut_enum('closed', [ '', 'Y' ])
+    || $self->ut_flag('no_auto_apply')
     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_textn('bank')
     || $self->ut_alphan('depositor')
     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_textn('bank')
     || $self->ut_alphan('depositor')
@@ -581,6 +606,12 @@ will be assumed.
 
 Customer (FS::cust_main) object (for efficiency).
 
 
 Customer (FS::cust_main) object (for efficiency).
 
+=item noemail
+
+Don't send an email receipt.
+
+=cut
+
 =back
 
 =cut
 =back
 
 =cut
@@ -674,7 +705,7 @@ sub send_receipt {
         'msgtype' => 'receipt',
       };
       $error = $queue->insert(
         'msgtype' => 'receipt',
       };
       $error = $queue->insert(
-        'from'    => $conf->config('invoice_from', $cust_main->agentnum),
+        'from'    => $conf->invoice_from_full( $cust_main->agentnum ),
                                    #invoice_from??? well as good as any
         'to'      => \@invoicing_list,
         'subject' => 'Payment receipt',
                                    #invoice_from??? well as good as any
         'to'      => \@invoicing_list,
         'subject' => 'Payment receipt',
@@ -687,7 +718,8 @@ sub send_receipt {
 
     }
 
 
     }
 
-  } elsif ( ! $cust_main->invoice_noemail ) { #not manual
+  #not manual and no noemail flag (here or on the customer)
+  } elsif ( ! $opt->{'noemail'} && ! $cust_main->invoice_noemail ) {
 
     my $queue = new FS::queue {
        'job'     => 'FS::cust_bill::queueable_email',
 
     my $queue = new FS::queue {
        'job'     => 'FS::cust_bill::queueable_email',
@@ -695,13 +727,22 @@ sub send_receipt {
        'custnum' => $cust_main->custnum,
     };
 
        'custnum' => $cust_main->custnum,
     };
 
-    $error = $queue->insert(
+    my %opt = (
       'invnum'      => $cust_bill->invnum,
       'invnum'      => $cust_bill->invnum,
-      'template'    => 'statement',
-      'notice_name' => 'Statement',
       'no_coupon'   => 1,
     );
 
       'no_coupon'   => 1,
     );
 
+    if ( my $mode = $conf->config('payment_receipt_statement_mode') ) {
+      $opt{'mode'} = $mode;
+    } else {
+      # backward compatibility, no good fix for this yet as some people may
+      # still have "invoice_latex_statement" and such options
+      $opt{'template'} = 'statement';
+      $opt{'notice_name'} = 'Statement';
+    }
+
+    $error = $queue->insert(%opt);
+
   }
   
   warn "send_receipt: $error\n" if $error;
   }
   
   warn "send_receipt: $error\n" if $error;
@@ -921,10 +962,11 @@ sub _upgrade_data {  #class method
 
     #not the most efficient, but hey, it only has to run once
 
 
     #not the most efficient, but hey, it only has to run once
 
-    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 $where = " WHERE ( otaker IS NULL OR otaker = '' OR otaker = 'ivan' )
+                    AND usernum IS NULL
+                    AND EXISTS ( SELECT 1 FROM cust_main                    
+                                   WHERE cust_main.custnum = cust_pay.custnum )
+                ";
 
     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
 
 
     my $count_sql = "SELECT COUNT(*) FROM cust_pay $where";
 
@@ -956,7 +998,6 @@ sub _upgrade_data {  #class method
         $cust_pay->set('otaker', 'legacy');
       }
 
         $cust_pay->set('otaker', 'legacy');
       }
 
-      delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
       my $error = $cust_pay->replace;
 
       if ( $error ) {
       my $error = $cust_pay->replace;
 
       if ( $error ) {
@@ -965,8 +1006,6 @@ sub _upgrade_data {  #class method
         next;
       }
 
         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";
       $count++;
       if ( $DEBUG > 1 && $lastprog + 30 < time ) {
         warn "$me $count/$total (".sprintf('%.2f',100*$count/$total). '%)'."\n";
@@ -1020,9 +1059,7 @@ sub _upgrade_data {  #class method
   # otaker->usernum upgrade
   ###
 
   # otaker->usernum upgrade
   ###
 
-  delete $FS::payby::hash{'COMP'}->{cust_pay}; #quelle kludge
   $class->_upgrade_otaker(%opt);
   $class->_upgrade_otaker(%opt);
-  $FS::payby::hash{'COMP'}->{cust_pay} = ''; #restore it
 
   # if we do this anywhere else, it should become an FS::Upgrade method
   my $num_to_upgrade = $class->count('paybatch is not null');
 
   # if we do this anywhere else, it should become an FS::Upgrade method
   my $num_to_upgrade = $class->count('paybatch is not null');
@@ -1131,6 +1168,89 @@ sub process_upgrade_paybatch {
 
 =over 4 
 
 
 =over 4 
 
+=item process_batch_import
+
+=cut
+
+sub process_batch_import {
+  my $job = shift;
+
+  my $hashcb = sub {
+    my %hash = @_;
+    my $custnum = $hash{'custnum'};
+    my $agentnum = $hash{'agentnum'};
+    my $agent_custid = $hash{'agent_custid'};
+    #standardize date
+    $hash{'_date'} = parse_datetime($hash{'_date'})
+      if $hash{'_date'} && $hash{'_date'} =~ /\D/;
+    #remove custnum_prefix
+    my $custnum_prefix = $conf->config('cust_main-custnum-display_prefix');
+    my $custnum_length = $conf->config('cust_main-custnum-display_length') || 8;
+    if (
+      $custnum_prefix 
+      && $custnum =~ /^$custnum_prefix(0*([1-9]\d*))$/
+      && length($1) == $custnum_length 
+    ) {
+      $custnum = $2;
+    }
+    # check agentnum against custnum and
+    # translate agent_custid into regular custnum
+    if ($custnum && $agent_custid) {
+      die "can't specify both custnum and agent_custid\n";
+    } elsif ($agentnum || $agent_custid) {
+      # here is the agent virtualization
+      my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
+      my %search;
+      $search{'agentnum'} = $agentnum
+        if $agentnum;
+      $search{'agent_custid'} = $agent_custid
+        if $agent_custid;
+      $search{'custnum'} = $custnum
+        if $custnum;
+      my $cust_main = qsearchs({
+        'table'     => 'cust_main',
+        'hashref'   => \%search,
+        'extra_sql' => $extra_sql,
+      });
+      die "can't find customer with" .
+        ($agentnum ? " agentnum $agentnum" : '') .
+        ($custnum  ? " custnum $custnum" : '') .
+        ($agent_custid ? " agent_custid $agent_custid" : '') . "\n"
+        unless $cust_main;
+      die "mismatched customer number\n"
+        if $custnum && ($custnum ne $cust_main->custnum);
+      $custnum = $cust_main->custnum;
+    }
+    $hash{'custnum'} = $custnum;
+    delete($hash{'agent_custid'});
+    return %hash;
+  };
+
+  my $opt = {
+    'table'        => 'cust_pay',
+    'params'       => [ '_date', 'agentnum', 'payby', 'paybatch' ],
+                        #agent_custid isn't a cust_pay field, see hash callback
+    'formats'      => { 'simple' =>
+                          [ qw(custnum agent_custid paid payinfo invnum) ] },
+    'format_types' => { 'simple' => '' }, #force infer from file extension
+    'default_csv'  => 1, #if not .xls, will read as csv, regardless of extension
+    'format_hash_callbacks' => { 'simple' => $hashcb },
+    'insert_args_callback'  => sub { ( 'manual'=>1 ); },
+    'postinsert_callback'   => sub {
+      my $cust_pay = shift;
+      my $cust_main = $cust_pay->cust_main
+                        or return "can't find customer to which payments apply";
+      my $error = $cust_main->apply_payments_and_credits;
+      return $error
+               ? "can't apply payments to customer ".$cust_pay->custnum."$error"
+               : '';
+    },
+  };
+
+  FS::Record::process_batch_import( $job, $opt, @_ );
+
+}
+
 =item batch_import HASHREF
 
 Inserts new payments.
 =item batch_import HASHREF
 
 Inserts new payments.