customer import: add progress bar & redirect to a search of the imported customers...
authorivan <ivan>
Thu, 14 Aug 2008 11:56:07 +0000 (11:56 +0000)
committerivan <ivan>
Thu, 14 Aug 2008 11:56:07 +0000 (11:56 +0000)
FS/FS/Schema.pm
FS/FS/UID.pm
FS/FS/cust_main.pm
httemplate/elements/file-upload.html
httemplate/elements/form-file_upload.html [new file with mode: 0644]
httemplate/elements/progress-init.html
httemplate/elements/progress-popup.html
httemplate/misc/cust_main-import.cgi
httemplate/misc/file-upload.html
httemplate/misc/process/cust_main-import.cgi
httemplate/search/cust_main.html

index 33b3064..e461fc1 100644 (file)
@@ -571,6 +571,7 @@ sub tables_hashref {
         'custnum',  'serial',  '',     '', '', '', 
         'agentnum', 'int',  '',     '', '', '', 
         'agent_custid', 'varchar', 'NULL', $char_d, '', '',
+        'custbatch', 'varchar', 'NULL', $char_d, '', '',
 #        'titlenum', 'int',  'NULL',   '', '', '', 
         'last',     'varchar', '',     $char_d, '', '', 
 #        'middle',   'varchar', 'NULL', $char_d, '', '', 
@@ -636,6 +637,7 @@ sub tables_hashref {
                    [ 'ship_last' ], [ 'ship_company' ],
                    [ 'ship_daytime' ], [ 'ship_night' ], [ 'ship_fax' ],
                    [ 'payby' ], [ 'paydate' ],
+                   [ 'agentnum' ], [ 'custbatch' ],
                  ],
     },
 
index dd4850f..431efb1 100644 (file)
@@ -3,7 +3,7 @@ package FS::UID;
 use strict;
 use vars qw(
   @ISA @EXPORT_OK $DEBUG $me $cgi $dbh $freeside_uid $user 
-  $conf_dir $secrets $datasrc $db_user $db_pass %callback @callback
+  $conf_dir $cache_dir $secrets $datasrc $db_user $db_pass %callback @callback
   $driver_name $AutoCommit $callback_hack $use_confcompat
 );
 use subs qw(
@@ -25,7 +25,8 @@ $me = '[FS::UID]';
 
 $freeside_uid = scalar(getpwnam('freeside'));
 
-$conf_dir = "%%%FREESIDE_CONF%%%";
+$conf_dir  = "%%%FREESIDE_CONF%%%";
+$cache_dir = "%%%FREESIDE_CACHE%%%";
 
 $AutoCommit = 1; #ours, not DBI
 $use_confcompat = 1;
index e698bfa..e7b3445 100644 (file)
@@ -1211,6 +1211,7 @@ sub check {
     || $self->ut_number('agentnum')
     || $self->ut_textn('agent_custid')
     || $self->ut_number('refnum')
+    || $self->ut_textn('custbatch')
     || $self->ut_name('last')
     || $self->ut_name('first')
     || $self->ut_snumbern('birthdate')
@@ -5545,6 +5546,15 @@ sub search_sql {
                    @{ $params->{'current_balance'} };
 
   ##
+  # custbatch
+  ##
+
+  if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) {
+    push @where,
+      "cust_main.custbatch = '$1'";
+  }
+
+  ##
   # setup queries, subs, etc. for the search
   ##
 
@@ -6206,6 +6216,58 @@ sub append_fuzzyfiles {
   1;
 }
 
+=item process_batch_import
+
+Load a batch import as a queued JSRPC job
+
+=cut
+
+use Storable qw(thaw);
+use Data::Dumper;
+use MIME::Base64;
+sub process_batch_import {
+  my $job = shift;
+
+  my $param = thaw(decode_base64(shift));
+  warn Dumper($param) if $DEBUG;
+  
+  my $files = $param->{'uploaded_files'}
+    or die "No files provided.\n";
+
+  my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
+
+  my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
+  my $file = $dir. $files{'file'};
+
+  my $type;
+  if ( $file =~ /\.(\w+)$/i ) {
+    $type = lc($1);
+  } else {
+    #or error out???
+    warn "can't parse file type from filename $file; defaulting to CSV";
+    $type = 'csv';
+  }
+
+  my $error =
+    FS::cust_main::batch_import( {
+      job       => $job,
+      file      => $file,
+      type      => $type,
+      custbatch => $param->{custbatch},
+      agentnum  => $param->{'agentnum'},
+      refnum    => $param->{'refnum'},
+      pkgpart   => $param->{'pkgpart'},
+      #'fields'  => [qw( cust_pkg.setup dayphone first last address1 address2
+      #                 city state zip comments                          )],
+      'format'  => $param->{'format'},
+    } );
+
+  unlink $file;
+
+  die "$error\n" if $error;
+
+}
+
 =item batch_import
 
 =cut
@@ -6214,14 +6276,18 @@ sub append_fuzzyfiles {
 sub batch_import {
   my $param = shift;
 
-  my $fh       = $param->{filehandle};
-  my $type     = $param->{type} || 'csv';
+  my $job       = $param->{job};
+
+  my $filename  = $param->{file};
+  my $type      = $param->{type} || 'csv';
+
+  my $custbatch = $param->{custbatch};
 
-  my $agentnum = $param->{agentnum};
-  my $refnum   = $param->{refnum};
-  my $pkgpart  = $param->{pkgpart};
+  my $agentnum  = $param->{agentnum};
+  my $refnum    = $param->{refnum};
+  my $pkgpart   = $param->{pkgpart};
 
-  my $format   = $param->{'format'};
+  my $format    = $param->{'format'};
 
   my @fields;
   my $payby;
@@ -6257,28 +6323,30 @@ sub batch_import {
     die "unknown format $format";
   }
 
+  my $count;
   my $parser;
-  my $spoolfile = '';
+  my @buffer = ();
   if ( $type eq 'csv' ) {
+
     eval "use Text::CSV_XS;";
     die $@ if $@;
+
     $parser = new Text::CSV_XS;
+
+    @buffer = split(/\r?\n/, slurp($filename) );
+    $count = scalar(@buffer);
+
   } elsif ( $type eq 'xls' ) {
 
     eval "use Spreadsheet::ParseExcel;";
     die $@ if $@;
 
-    ( my $spool_fh, $spoolfile ) =
-      tempfile('cust_main-batch_import-XXXXXXXXXXXX',
-                 DIR    => '%%%FREESIDE_CACHE%%%',
-                 SUFFIX => '.xls',
-              );
-    print $spool_fh slurp($fh);
-    close $spool_fh or die $!;
-
-    my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($spoolfile);
+    my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($filename);
     $parser = $excel->{Worksheet}[0]; #first sheet
 
+    $count = $parser->{MaxRow} || $parser->{MinRow};
+    $count++;
+
   } else {
     die "Unknown file type $type\n";
   }
@@ -6298,12 +6366,14 @@ sub batch_import {
   
   my $line;
   my $row = 0;
+  my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
   while (1) {
 
     my @columns = ();
     if ( $type eq 'csv' ) {
 
-      last unless defined($line=<$fh>);
+      last unless scalar(@buffer);
+      $line = shift(@buffer);
 
       $parser->parse($line) or do {
         $dbh->rollback if $oldAutoCommit;
@@ -6328,11 +6398,12 @@ sub batch_import {
     #warn join('-',@columns);
 
     my %cust_main = (
-      agentnum => $agentnum,
-      refnum   => $refnum,
-      country  => $conf->config('countrydefault') || 'US',
-      payby    => $payby, #default
-      paydate  => '12/2037', #default
+      custbatch => $custbatch,
+      agentnum  => $agentnum,
+      refnum    => $refnum,
+      country   => $conf->config('countrydefault') || 'US',
+      payby     => $payby, #default
+      paydate   => '12/2037', #default
     );
     my $billtime = time;
     my %cust_pkg = ( pkgpart => $pkgpart );
@@ -6384,7 +6455,9 @@ sub batch_import {
       }
     }
 
-    $cust_main{'payby'} = 'CARD' if length($cust_main{'payinfo'});
+    $cust_main{'payby'} = 'CARD'
+      if defined $cust_main{'payinfo'}
+      && length  $cust_main{'payinfo'};
 
     my $invoicing_list = $cust_main{'invoicing_list'}
                            ? [ delete $cust_main{'invoicing_list'} ]
@@ -6416,7 +6489,7 @@ sub batch_import {
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "can't insert customer ". ( $line ? "for $line" : '' ). ": $error";
+      return "can't insert customer". ( $line ? " for $line" : '' ). ": $error";
     }
 
     if ( $format eq 'simple' ) {
@@ -6443,11 +6516,15 @@ sub batch_import {
     }
 
     $row++;
-  }
 
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    if ( $job && time - $min_sec > $last ) { #progress bar
+      $job->update_statustext( int(100 * $row / $count) );
+      $last = time;
+    }
+
+  }
 
-  unlink($spoolfile) if $spoolfile;
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
 
   return "Empty file!" unless $row;
 
index 2859a67..023bffb 100644 (file)
@@ -1,10 +1,13 @@
-
-<script type="text/javascript">
+<SCRIPT TYPE="text/javascript">
 
   function doUpload(form, callback) {
     var name = 'form' + Math.floor(Math.random() * 99999); // perlize?
     var d = document.createElement('DIV');
-    d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+name+'" name="'+name+'" onload="uploadComplete(\''+name+'\')"></iframe>';
+    d.innerHTML = '<iframe style="display:none" src="about:blank" ' +
+                           'id="'   + name + '" ' +
+                           'name="' + name + '" ' +
+                           'onload="uploadComplete(\'' + name + '\')">' +
+                  '</iframe>';
     document.body.appendChild(d);
 
     var i = document.getElementById(name);
@@ -32,8 +35,8 @@
     document.getElementById('r').innerHTML = d.body.innerHTML;
     if (typeof(i.onComplete) == 'function') {
       var p;
-      if (p = d.body.innerHTML.indexOf("Freeside File Upload Successful ") >= 0) {
-        var v = d.body.innerHTML.substr(p+33)
+      if (p = d.body.innerHTML.indexOf("File Upload Successful ") >= 0) {
+        var v = d.body.innerHTML.substr(p+24);
         var u = document.getElementById('uploaded_files');
         v = v.substr(0, v.indexOf(';'));
         u.value = v;
     }
   }
 
-</script>
+</SCRIPT>
+
+<INPUT TYPE="hidden" NAME="uploaded_files" ID="uploaded_files" VALUE="" />
+
+<INPUT TYPE="hidden" NAME="upload_fields" VALUE="<% join(',', @field) %>" />
 
-    <input type="hidden" name="uploaded_files" id="uploaded_files" value="" />
-    <input type="hidden" name="upload_fields" value="<% join(',', @field) %>" />
 % foreach (@field) {
-    <tr>
-      <th><% shift @label %></th>
-      <td><input type="file" name="<% $_ %>" /></td>
-    </tr>
+    <TR>
+      <TH><% shift @label %></TH>
+      <TD><INPUT TYPE="file" NAME="<% $_ %>" /></TD>
+    </TR>
 % }
-  <div style="display:<% $debug ? 'visible' : 'none' %>">Debugging: <pre id="r"></pre></div>
-
-<%init>
-my %param = @_;
 
-my $debug = $param{'debug'};
+<DIV STYLE="display:<% $param{debug} ? 'visible' : 'none' %>">
+  Debugging: <PRE ID="r"></PRE>
+</DIV>
 
-my $callback = $param{'callback'} || "''";
+<%init>
 
-my @label = ();
-if ( ref($param{'label'}) ) {
-  push @label, @{$param{'label'}};
-}else{
-  push @label, $param{'label'};
-}
+my %param = @_;
 
-my @field = ();
-if ( ref($param{'field'}) ) {
-  push @field, @{$param{'field'}};
-}else{
-  push @field, $param{'field'};
-}
+my @label = ref($param{'label'}) ? @{$param{'label'}} : ($param{'label'});
+my @field = ref($param{'field'}) ? @{$param{'field'}} : ($param{'field'});
 
 </%init>
diff --git a/httemplate/elements/form-file_upload.html b/httemplate/elements/form-file_upload.html
new file mode 100644 (file)
index 0000000..f398ba4
--- /dev/null
@@ -0,0 +1,93 @@
+<%doc>
+
+Example:
+
+  <% include( '/elements/form-file_upload.html',
+
+                'name'      => 'form_name',
+                'action'    => 'process/target.cgi', #progress-init target
+                'fields'    => [ 'other', 'form', 'fields' ],
+                'num_files' => 1, #or more
+
+                'url' => $url
+                #OR
+                'message' => 'Message',
+
+                #optional
+                'key' => 'unique_key', #for using more than once on a page
+            )
+
+% #... 
+
+% # num_files=>1
+  include( '/elements/file-upload.html',
+             'field'    => 'element',
+             'label'    => 'Label',
+         )
+
+% # OR
+
+% # num_files=>2 # or more
+  include( '/elements/file-upload.html',
+             'field'    => [ 'element', 'element2', ], #etc.
+             'label'    => [ 'Label',   'Label2',   ], #etc.
+         )
+
+
+%>
+
+</%doc>
+
+<% include( '/elements/progress-init.html',
+              $opt{name},
+              $opt{fields},
+              $opt{action},
+              $msg_or_url,
+              $opt{key},
+          )
+%>
+
+<SCRIPT>
+
+  function <% $opt{key} %>gotUploaded(success, message) {
+
+    var uploaded = document.getElementById('uploaded_files');
+    var a = uploaded.value.split(',');
+    if (uploaded.value.split(',').length == <% $opt{num_files} %>){
+      process(); 
+    }else{
+      var p = document.getElementById('uploadError');
+      p.innerHTML='<FONT SIZE="+1" COLOR="#ff0000">Error: '+message+'</FONT><BR><BR>';
+      p.style='display:visible';
+      return false;
+    }
+    
+  }
+
+</SCRIPT>
+
+<div style="display:none:" id="uploadError"></div>
+
+<FORM NAME     = "<% $opt{name} %>"
+      ACTION   = "<% $fsurl %>misc/file-upload.html"
+      METHOD   = "POST"
+      ENCTYPE  = "multipart/form-data"
+      onSubmit = "return doUpload(this, <% $opt{key} %>gotUploaded)"
+>
+
+<%init>
+
+#my( $formname, $fields, $action, $url_or_message, $key ) = @_;
+my %opt = ref($_[0]) ? %{ $_[0] } : @_;
+
+my $key = exists $opt{key} ? $opt{key} : '';
+
+push @{ $opt{fields} }, 'uploaded_files';
+
+my $msg_or_url = $opt{message}
+                   ? { 'message' => $opt{message},
+                       'url'     => $opt{url},
+                     }
+                   : $opt{url};
+
+</%init>
index 2cde86f..194fc74 100644 (file)
@@ -73,8 +73,9 @@ $key = '' unless defined $key;
 
 my $url_or_message_link;
 if ( ref($url_or_message) ) { #its a message or something
-  $url_or_message_link =
-    'message='. uri_escape( $url_or_message->{'message'} )
+  $url_or_message_link = 'message='. uri_escape( $url_or_message->{'message'} );
+  $url_or_message_link .= ';url='.   uri_escape( $url_or_message->{'url'} )
+    if $url_or_message->{'url'};
 } else {
   $url_or_message_link = "url=$url_or_message";
 }
index cda704a..0bd71ff 100644 (file)
@@ -41,15 +41,24 @@ function updateStatus( status_statustext ) {
     //jsrsExecute( '<%$p%>elements/jsrsServer.html', updateStatus, 'job_status', '<% $jobnum %>' );
     job_status( '<% $jobnum %>', updateStatus );
   } else if ( status.indexOf('complete') > -1 ) {
-% if ( $message ) { 
+% if ( $message ) {
+%
+%   my $onClick = $url
+%                   ? "window.top.location.href = \\'$url\\';"
+%                   : 'parent.nd(1);';
 
     document.getElementById("progress_message").innerHTML = "<% $message %>";
     document.getElementById("progress_bar").innerHTML = '';
-    document.getElementById("progress_percent").innerHTML = '<INPUT TYPE="button" VALUE="OK" onClick="parent.nd(1);">';
+    document.getElementById("progress_percent").innerHTML =
+      '<INPUT TYPE="button" VALUE="OK" onClick="<% $onClick %>">';
     document.getElementById("progress_jobnum").innerHTML = '';
-    if ( parent.document.<%$formname%>.submit.disabled == true ) {
-      parent.document.<%$formname%>.submit.disabled=false;
-    }
+
+%   unless ( $url ) {
+        if ( parent.document.<%$formname%>.submit.disabled == true ) {
+          parent.document.<%$formname%>.submit.disabled=false;
+        }
+%   }
+
 % } elsif ( $url ) { 
 
     window.top.location.href = '<% $url %>';
index 8e170c3..bbeaa51 100644 (file)
@@ -3,32 +3,45 @@
 Import a file containing customer records.
 <BR><BR>
 
-<FORM ACTION="process/cust_main-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+<% include( '/elements/form-file_upload.html',
+              'name'      => 'CustomerImportForm',
+              'action'    => 'process/cust_main-import.cgi',
+              'num_files' => 1,
+              'fields'    => [ 'agentnum', 'custbatch', 'format' ],
+              'message'   => 'Customer import successful',
+              'url'       => $p."search/cust_main.html?custbatch=$custbatch",
+          )
+%>
 
 <% &ntable("#cccccc", 2) %>
 
-<% include('/elements/tr-select-agent.html',
-              #'curr_value' => '', #$agentnum,
-              'label'       => "<B>Agent</B>",
-              'empty_label' => 'Select agent',
-           )
-%>
+  <% include( '/elements/tr-select-agent.html',
+                 #'curr_value' => '', #$agentnum,
+                 'label'       => "<B>Agent</B>",
+                 'empty_label' => 'Select agent',
+             )
+  %>
+
+  <INPUT TYPE="hidden" NAME="custbatch" VALUE="<% $custbatch %>"%>
+
+  <TR>
+    <TH ALIGN="right">Format</TH>
+    <TD>
+      <SELECT NAME="format">
+        <!-- <OPTION VALUE="simple">Simple -->
+        <OPTION VALUE="extended" SELECTED>Extended
+        <OPTION VALUE="extended-plus_company">Extended plus company
+      </SELECT>
+    </TD>
+  </TR>
+
+  <% include( '/elements/file-upload.html',
+                'field' => 'file',
+                'label' => 'Filename',
+            )
+  %>
 
-<TR>
-  <TH ALIGN="right">Format</TH>
-  <TD>
-    <SELECT NAME="format">
-<!--      <OPTION VALUE="simple">Simple -->
-      <OPTION VALUE="extended" SELECTED>Extended
-      <OPTION VALUE="extended-plus_company">Extended plus company
-    </SELECT>
-  </TD>
-</TR>
 
-<TR>
-  <TH ALIGN="right">Filename</TH>
-  <TD><INPUT TYPE="file" NAME="file"></TD>
-</TR>
 % #include('/elements/tr-select-part_referral.html')
 %
 
@@ -49,7 +62,15 @@ Import a file containing customer records.
 </TR>
 -->
 
-<TR><TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px"><INPUT TYPE="submit" VALUE="Import file"></TD></TR>
+  <TR>
+    <TD COLSPAN=2 ALIGN="center" STYLE="padding-top:6px">
+      <INPUT TYPE    = "submit"
+             ID      = "submit"
+             VALUE   = "Import file"
+             onClick = "document.CustomerImportForm.submit.disabled=true;"
+      >
+    </TD>
+  </TR>
 
 </TABLE>
 
@@ -109,4 +130,6 @@ my $req = qq!<font color="#ff0000">*</font>!;
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Import');
 
+my $custbatch = time2str('webimport-%Y/%m/%d-%T'. "-$$-". rand() * 2**32, time);
+
 </%init>
index 9649d36..469274c 100644 (file)
@@ -2,7 +2,7 @@
 % if ($error) {
 Error: <% $error %>
 % }else{
-Freeside File Upload Successful <% join(',', @filenames) %>;
+File Upload Successful <% join(',', @filenames) %>;
 % }
 <% include('/elements/footer.html') %>
 <%init>
@@ -17,7 +17,7 @@ $cgi->param('upload_fields') =~ /^([,\w]+)$/
   or $error = "invalid upload_fields";
 my $fields = $1;
 
-my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+my $dir = $FS::UID::cache_dir. "/cache.". $FS::UID::datasrc;
 
 foreach my $field (split /,/, $fields) {
   next if $error;
@@ -25,7 +25,13 @@ foreach my $field (split /,/, $fields) {
   my $fh = $cgi->upload($field)
     or $error = "No valid file was provided.";
 
+  my $suffix = '';
+  if ( $cgi->param($field) =~ /(\.\w+)$/i ) {
+    $suffix = lc($1);
+  }
+
   my $sh = new File::Temp( TEMPLATE => 'upload.XXXXXXXX',
+                           SUFFIX   => $suffix,
                            DIR      => $dir,
                            UNLINK   => 0,
                          )
index 2568d1c..df61eb6 100644 (file)
@@ -1,42 +1,9 @@
-% if ( $error ) {
-%   errorpage($error);
-%  } else {
-    <% include('/elements/header.html','Import successful') %> 
-    <% include('/elements/footer.html') %> 
-%  }
+<% $server->process %>
 <%init>
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Import');
 
-my $fh = $cgi->upload('file');
-my $error = '';
-if ( defined($fh) ) {
-
-  my $type;
-  if ( $cgi->param('file') =~ /\.(\w+)$/i ) {
-    $type = lc($1);
-  } else {
-    #or error out???
-    warn "can't parse file type from filename ". $cgi->param('file').
-         '; defaulting to CSV';
-    $type = 'csv';
-  }
-
-  $error =
-    FS::cust_main::batch_import( {
-      filehandle => $fh,
-      type       => $type,
-      agentnum   => scalar($cgi->param('agentnum')),
-      refnum     => scalar($cgi->param('refnum')),
-      pkgpart    => scalar($cgi->param('pkgpart')),
-      #'fields'    => [qw( cust_pkg.setup dayphone first last address1 address2
-      #                    city state zip comments                          )],
-      'format'   => scalar($cgi->param('format')),
-    } );
-
-} else {
-  $error = 'No file';
-}
+my $server = new FS::UI::Web::JSRPC 'FS::cust_main::process_batch_import', $cgi;
 
 </%init>
index cd5e51f..3282f0f 100755 (executable)
@@ -43,7 +43,9 @@ my %search_hash = ();
 #$search_hash{'query'} = $cgi->keywords;
 
 #scalars
-for my $param (qw( agentnum status cancelled_pkgs cust_fields flattened_pkgs)) {
+for my $param (qw(
+  agentnum status cancelled_pkgs cust_fields flattened_pkgs custbatch
+)) {
   $search_hash{$param} = scalar( $cgi->param($param) )
     if $cgi->param($param);
 }