RT#30825: Modernize Bulk payment importing
authorJonathan Prykop <jonathan@freeside.biz>
Sat, 28 Feb 2015 06:04:43 +0000 (00:04 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Sat, 28 Feb 2015 06:04:43 +0000 (00:04 -0600)
FS/FS/Record.pm
FS/FS/cust_pay.pm
FS/FS/payby.pm
httemplate/elements/select-payby.html
httemplate/misc/cust_pay-import.cgi
httemplate/misc/process/cust_pay-import.cgi

index 35ed6f7..0f3685b 100644 (file)
@@ -1803,6 +1803,7 @@ sub process_batch_import {
     format_xml_formats         => $opt->{format_xml_formats},
     format_asn_formats         => $opt->{format_asn_formats},
     format_row_callbacks       => $opt->{format_row_callbacks},
+    format_hash_callbacks      => $opt->{format_hash_callbacks},
     #per-import
     job                        => $job,
     file                       => $file,
@@ -1811,6 +1812,7 @@ sub process_batch_import {
     params                     => { map { $_ => $param->{$_} } @pass_params },
     #?
     default_csv                => $opt->{default_csv},
+    preinsert_callback         => $opt->{preinsert_callback},
     postinsert_callback        => $opt->{postinsert_callback},
     insert_args_callback       => $opt->{insert_args_callback},
   );
@@ -1849,6 +1851,8 @@ Class method for batch imports.  Available params:
 
 =item format_row_callbacks
 
+=item format_hash_callbacks - After parsing, before object creation
+
 =item fields - Alternate way to specify import, specifying import fields directly as a listref
 
 =item preinsert_callback
@@ -1891,7 +1895,7 @@ sub batch_import {
 
   my( $type, $header, $sep_char,
       $fixedlength_format, $xml_format, $asn_format,
-      $parser_opt, $row_callback, @fields );
+      $parser_opt, $row_callback, $hash_callback, @fields );
 
   my $postinsert_callback = '';
   $postinsert_callback = $param->{'postinsert_callback'}
@@ -1947,6 +1951,11 @@ sub batch_import {
         ? $param->{'format_row_callbacks'}{ $param->{'format'} }
         : '';
 
+    $hash_callback =
+      $param->{'format_hash_callbacks'}
+        ? $param->{'format_hash_callbacks'}{ $param->{'format'} }
+        : '';
+
     @fields = @{ $formats->{ $format } };
 
   } elsif ( $param->{'fields'} ) {
@@ -1956,6 +1965,7 @@ sub batch_import {
     $sep_char = ',';
     $fixedlength_format = '';
     $row_callback = '';
+    $hash_callback = '';
     @fields = @{ $param->{'fields'} };
 
   } else {
@@ -2181,6 +2191,8 @@ sub batch_import {
       $hash{custnum} = $2;
     }
 
+    %hash = &{$hash_callback}(%hash) if $hash_callback;
+
     #my $table   = $param->{table};
     my $class = "FS::$table";
 
index 8274b3d..0a36aca 100644 (file)
@@ -239,6 +239,12 @@ sub insert {
         $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 );
   }
 
@@ -1157,6 +1163,87 @@ sub process_upgrade_paybatch {
 
 =over 4 
 
+=item process_batch_import
+
+=cut
+
+sub process_batch_import {
+  my $job = shift;
+
+  #agent_custid isn't a cust_pay field, see hash callback
+  my $format = [ qw(custnum agent_custid paid payinfo invnum) ];
+  my $hashcb = sub {
+    my %hash = @_;
+    my $custnum = $hash{'custnum'};
+    my $agent_custid = $hash{'agent_custid'};
+    #standardize date
+    $hash{'_date'} = parse_datetime($hash{'_date'})
+      if $hash{'_date'} && $hash{'_date'} =~ /\D/;
+    # translate agent_custid into regular custnum
+    if ($custnum && $agent_custid) {
+      die "can't specify both custnum and agent_custid\n";
+    } elsif ($agent_custid) {
+      # here is the agent virtualization
+      my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
+      my $agentnum = $hash{'agentnum'};
+      my %search = (
+        'agent_custid' => $agent_custid,
+        'agentnum'     => $agentnum,
+      );
+      my $cust_main = qsearchs({
+        'table'     => 'cust_main',
+        'hashref'   => \%search,
+        'extra_sql' => $extra_sql,
+      });
+      die "can't find customer with agent_custid $agent_custid\n"
+        unless $cust_main;
+      $custnum = $cust_main->custnum;
+    }
+    #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;
+    }
+    $hash{'custnum'} = $custnum;
+    delete($hash{'agent_custid'});
+    return %hash;
+  };
+
+  my $opt = { 'table'   => 'cust_pay',
+              'params'  => [ '_date', 'agentnum', 'payby', 'paybatch' ],
+              'formats' => {
+                'simple-csv' => $format,
+                'simple-xls' => $format,
+              },
+              'format_types' => {
+                'simple-csv' => 'csv',
+                'simple-xls' => 'xls',
+              },
+              'default_csv' => 1,
+              'format_hash_callbacks' => { 
+                'simple-csv' => $hashcb,
+                'simple-xls' => $hashcb,
+              },
+              'postinsert_callback' => sub {
+                 my $cust_pay = shift;
+                 my $cust_main = $cust_pay->cust_main ||
+                   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.
index 13423c4..5b4559d 100644 (file)
@@ -224,6 +224,28 @@ sub cust_payby2longname {
   map { $_ => $hash{$_}->{longname} } $self->cust_payby;
 }
 
+=item payment_payby
+
+Returns all values of payby that can be used by payments.
+
+=cut
+
+sub payment_payby {
+  my $self = shift;
+  grep { ! exists $hash{$_}->{cust_pay} } $self->payby;
+}
+
+=item payment_payby2longname
+
+Returns hash, keys are L</payment_payby> types, values are payby longname.
+
+=cut
+
+sub payment_payby2longname {
+  my $self = shift;
+  map { $_ => $hash{$_}->{longname} } $self->payment_payby;
+}
+
 =back
 
 =head1 BUGS
index b2d5421..2018874 100644 (file)
@@ -3,7 +3,7 @@
         <% $onchange %>
 >
 
-% unless ( $opt{'multiple'} ) {
+% unless ( $opt{'multiple'} || $opt{'no_all'} ) {
     <OPTION VALUE="" <% '' eq $value ? 'SELECTED' : '' %> ><% mt('all') |h %> 
 % }
 
index 05a6c4f..ee0154d 100644 (file)
@@ -1,22 +1,31 @@
 <& /elements/header.html, 'Batch Payment Import' &>
 
-Import a CSV file containing customer payments.
+Import a file containing customer payments.
 <BR><BR>
 
-<FORM ACTION="process/cust_pay-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+
+<% include( '/elements/form-file_upload.html',
+     'name'      => 'OneTrueForm',
+     'action'    => 'process/cust_pay-import.cgi', #progress-init target
+     'fields'    => [ 'agentnum', '_date', 'paybatch', 'format', 'payby' ],
+     'num_files' => 1,
+     'url' => popurl(2)."search/cust_pay.html?magic=paybatch;paybatch=$paybatch",
+     'message' => 'Batch Payment Imported',
+   )
+%>
 
 <% &ntable("#cccccc", 2) %>
 
+<INPUT TYPE="hidden" NAME="paybatch" VALUE="<% $paybatch | h %>">
+
 <& /elements/tr-select-agent.html,
-     #'curr_value' => '', #$agentnum,
      'label'       => "<B>Agent</B>",
      'empty_label' => 'Select agent',
 &>
 
 <& /elements/tr-input-date-field.html, {
      'name'  => '_date',
-     #'value' => '',
-     'label' => 'Date',
+     'label' => '<B>Date</B>',
    }
 &>
 
@@ -24,18 +33,26 @@ Import a CSV file containing customer payments.
   <TH ALIGN="right">Format</TH>
   <TD>
     <SELECT NAME="format">
-      <OPTION VALUE="simple">Simple
-<!--      <OPTION VALUE="extended" SELECTED>Extended -->
+      <OPTION VALUE="simple-csv">Comma-separated (.csv)</OPTION>
+      <OPTION VALUE="simple-xls">Excel (.xls)</OPTION>
     </SELECT>
   </TD>
 </TR>
 
-<TR>
-  <TH ALIGN="right">CSV filename</TH>
-  <TD><INPUT TYPE="file" NAME="csvfile"></TD>
-</TR>
+<% include( '/elements/tr-select-payby.html',
+     'paybys' => \%paybys,
+     'no_all' => 1,
+     'label'  => '<B>Payment type</B>',
+   )
+%>
+
+<% include( '/elements/file-upload.html',
+             'field'    => 'file',
+             'label'    => 'Filename',
+   )
+%>
 
-<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import CSV file"></TD></TR>
+<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import file"></TD></TR>
 
 </TABLE>
 
@@ -43,11 +60,10 @@ Import a CSV file containing customer payments.
 
 <BR>
 
-Simple file format is CSV, with the following field order: <i>custnum, agent_custid, amount, checknum</i>
+Simple file format is CSV or XLS, with the following field order: <i>custnum, agent_custid, amount, checknum, invnum</i>
 <BR><BR>
 
-<!-- Extended file format is not yet defined</i>
-<BR><BR> -->
+<!-- Extended file format is not yet defined -->
 
 Field information:
 
@@ -68,3 +84,9 @@ Field information:
 <BR>
 
 <& /elements/footer.html &>
+
+<%init>
+my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
+my %paybys;
+tie %paybys, 'Tie::IxHash', FS::payby->payment_payby2longname();
+</%init>
index 7711773..2981da4 100644 (file)
@@ -1,23 +1,5 @@
-<% $cgi->redirect(popurl(3). "search/cust_pay.html?magic=paybatch;paybatch=$paybatch") %> 
+<% $server->process %>
 <%init>
-
-my $fh = $cgi->upload('csvfile');
-
-# webbatch?  I suppose
-my $paybatch = time2str('webbatch-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
-
-my $error = defined($fh)
-  ? FS::cust_pay::batch_import( {
-      'filehandle' => $fh,
-      'format'     => scalar($cgi->param('format')),
-
-      'agentnum'   => scalar($cgi->param('agentnum')),
-      '_date'      => scalar($cgi->param('_date')),
-      'paybatch'   => $paybatch,
-    } )
-  : 'No file';
-
-errorpage($error)
-  if ( $error );
-
+my $server = new FS::UI::Web::JSRPC 'FS::cust_pay::process_batch_import', $cgi; 
 </%init>
+