[freeside-commits] branch master updated. 9ef78be87df0f0f880ff5d903ed6243b67369cf0

Mark Wells mark at 420.am
Wed Jun 13 16:19:05 PDT 2012


The branch, master has been updated
       via  9ef78be87df0f0f880ff5d903ed6243b67369cf0 (commit)
      from  daa09251fec52517b630b3f6935041dc7c795f90 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 9ef78be87df0f0f880ff5d903ed6243b67369cf0
Author: Mark Wells <mark at freeside.biz>
Date:   Wed Jun 13 16:18:49 2012 -0700

    table of FTP targets for invoice spool upload, #17620

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 151e31c..0314992 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -13,6 +13,7 @@ use FS::payby;
 use FS::conf;
 use FS::Record qw(qsearch qsearchs);
 use FS::UID qw(dbh datasrc use_confcompat);
+use FS::Misc;
 use FS::Misc::Geo;
 
 $base_dir = '%%%FREESIDE_CONF%%%';
@@ -3041,7 +3042,7 @@ and customer address. Include units.',
     'section'     => 'invoicing',
     'description' => 'Enable FTP of raw invoice data - format.',
     'type'        => 'select',
-    'select_enum' => [ '', 'default', 'oneline', 'billco', ],
+    'options'     => [ FS::Misc::spool_formats() ],
   },
 
   {
@@ -3077,7 +3078,7 @@ and customer address. Include units.',
     'section'     => 'invoicing',
     'description' => 'Enable spooling of raw invoice data - format.',
     'type'        => 'select',
-    'select_enum' => [ '', 'default', 'oneline', 'billco', ],
+    'options'     => [ FS::Misc::spool_formats() ],
   },
 
   {
@@ -3087,14 +3088,33 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
-    {
-    'key'         => 'cust_bill-ftp_spool',
-    'section'     => 'invoicing',
-    'description' => 'Enable FTP upload of the invoice spool during daily processing',
-    'type'        => 'checkbox',
+  {
+    'key'         => 'bridgestone-batch_counter',
+    'section'     => '',
+    'description' => 'Batch counter for spool files.  Increments every time a spool file is uploaded.',
+    'type'        => 'text',
+    'per_agent'   => 1,
+  },
+
+  {
+    'key'         => 'bridgestone-prefix',
+    'section'     => '',
+    'description' => 'Agent identifier for uploading to BABT printing service.',
+    'type'        => 'text',
+    'per_agent'   => 1,
+  },
+
+  {
+    'key'         => 'bridgestone-confirm_template',
+    'section'     => '',
+    'description' => 'Confirmation email template for uploading to BABT service.  Text::Template format, with variables "$zipfile" (name of the zipped file), "$seq" (sequence number), "$prefix" (user ID string), and "$rows" (number of records in the file).  Should include Subject: and To: headers, separated from the rest of the message by a blank line.',
+    # this could use a true message template, but it's hard to see how that
+    # would make the world a better place
+    'type'        => 'textarea',
+    'per_agent'   => 1,
   },
 
-{
+  {
     'key'         => 'svc_acct-usage_suspend',
     'section'     => 'billing',
     'description' => 'Suspends the package an account belongs to when svc_acct.seconds or a bytecount is decremented to 0 or below (accounts with an empty seconds and up|down|totalbytes value are ignored).  Typically used in conjunction with prepaid packages and freeside-sqlradius-radacctd.',
diff --git a/FS/FS/Cron/upload.pm b/FS/FS/Cron/upload.pm
index c266797..51e0d68 100644
--- a/FS/FS/Cron/upload.pm
+++ b/FS/FS/Cron/upload.pm
@@ -9,6 +9,8 @@ use FS::Record qw( qsearch qsearchs );
 use FS::Conf;
 use FS::queue;
 use FS::agent;
+use FS::Misc qw( send_email ); #for bridgestone
+use FS::ftp_target;
 use LWP::UserAgent;
 use HTTP::Request;
 use HTTP::Request::Common;
@@ -47,70 +49,50 @@ sub upload {
 
   my @agents = $opt{'a'} ? FS::agent->by_key($opt{'a'}) : qsearch('agent', {});
 
-  if ( $conf->exists('cust_bill-ftp_spool') ) {
-    my $url = $conf->config('cust_bill-ftpdir');
-    $url = "/$url" unless $url =~ m[^/];
-    $url = 'ftp://' . $conf->config('cust_bill-ftpserver') . $url;
-
-    my $format = $conf->config('cust_bill-ftpformat');
-    my $username = $conf->config('cust_bill-ftpusername');
-    my $password = $conf->config('cust_bill-ftppassword');
-
-    my %task = (
-      'date'      => $date,
-      'l'         => $opt{'l'},
-      'm'         => $opt{'m'},
-      'v'         => $opt{'v'},
-      'username'  => $username,
-      'password'  => $password,
-      'url'       => $url,
-      'format'    => $format,
-    );
-
-    if ( $conf->exists('cust_bill-spoolagent') ) {
-      # then push each agent's spool separately
-      foreach ( @agents ) {
-        push @tasks, { %task, 'agentnum' => $_->agentnum };
-      }
-    }
-    elsif ( $opt{'a'} ) {
-      warn "Per-agent processing, but cust_bill-spoolagent is not enabled.\nSkipped invoice upload.\n";
-    }
-    else {
-      push @tasks, \%task;
+  my %task = (
+    'date'      => $date,
+    'l'         => $opt{'l'},
+    'm'         => $opt{'m'},
+    'v'         => $opt{'v'},
+  );
+
+  my @agentnums = ('', map {$_->agentnum} @agents);
+
+  foreach my $target (qsearch('ftp_target', {})) {
+    # We don't know here if it's spooled on a per-agent basis or not.
+    # (It could even be both, via different events.)  So queue up an 
+    # upload for each agent, plus one with null agentnum, and we'll 
+    # upload as many files as we find.
+    foreach my $a (@agentnums) {
+      push @tasks, {
+        %task,
+        'agentnum'  => $a,
+        'targetnum' => $target->targetnum,
+        'handling'  => $target->handling,
+      };
     }
   }
 
-  else { #check each agent for billco upload settings
-
-    my %task = (
-      'date'      => $date,
-      'l'         => $opt{'l'},
-      'm'         => $opt{'m'},
-      'v'         => $opt{'v'},
-    );
-
-    foreach (@agents) {
-      my $agentnum = $_->agentnum;
-
-      if ( $conf->config( 'billco-username', $agentnum, 1 ) ) {
-        my $username = $conf->config('billco-username', $agentnum, 1);
-        my $password = $conf->config('billco-password', $agentnum, 1);
-        my $clicode  = $conf->config('billco-clicode',  $agentnum, 1);
-        my $url      = $conf->config('billco-url',      $agentnum);
-        push @tasks, {
-          %task,
-          'agentnum' => $agentnum,
-          'username' => $username,
-          'password' => $password,
-          'url'      => $url,
-          'clicode'  => $clicode,
-          'format'   => 'billco',
-        };
-      }
-    } # foreach @agents
-
-  } #!if cust_bill-ftp_spool
+  # deprecated billco method
+  foreach (@agents) {
+    my $agentnum = $_->agentnum;
+
+    if ( $conf->config( 'billco-username', $agentnum, 1 ) ) {
+      my $username = $conf->config('billco-username', $agentnum, 1);
+      my $password = $conf->config('billco-password', $agentnum, 1);
+      my $clicode  = $conf->config('billco-clicode',  $agentnum, 1);
+      my $url      = $conf->config('billco-url',      $agentnum);
+      push @tasks, {
+        %task,
+        'agentnum' => $agentnum,
+        'username' => $username,
+        'password' => $password,
+        'url'      => $url,
+        'clicode'  => $clicode,
+        'handling' => 'billco',
+      };
+    }
+  } # foreach @agents
 
   foreach (@tasks) {
 
@@ -146,14 +128,7 @@ sub spool_upload {
   my $conf = new FS::Conf;
   my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill';
 
-  my $agentnum = $opt{agentnum} or die "no agentnum provided\n";
-  my $url      = $opt{url} or die "no url for agent $agentnum\n";
-  $url =~ s/^\s+//; $url =~ s/\s+$//;
-
-  my $username = $opt{username} or die "no username for agent $agentnum\n";
-  my $password = $opt{password} or die "no password for agent $agentnum\n";
-
-  die "no date provided\n" unless $opt{date};
+  my $date = $opt{date} or die "no date provided\n";
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -166,23 +141,34 @@ sub spool_upload {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $agent = qsearchs( 'agent', { agentnum => $agentnum } )
-    or die "no such agent: $agentnum";
-  $agent->select_for_update; #mutex 
+  my $agentnum = $opt{agentnum};
+  my $agent;
+  if ( $agentnum ) {
+    $agent = qsearchs( 'agent', { agentnum => $agentnum } )
+      or die "no such agent: $agentnum";
+    $agent->select_for_update; #mutex 
+  }
 
-  if ( $opt{'format'} eq 'billco' ) {
+  if ( $opt{'handling'} eq 'billco' ) {
 
-    my $zipfile  = "$dir/agentnum$agentnum-$opt{date}.zip";
+    my $file = "agentnum$agentnum";
+    my $zipfile  = "$dir/$file-$date.zip";
 
-    unless ( -f "$dir/agentnum$agentnum-header.csv" ||
-             -f "$dir/agentnum$agentnum-detail.csv" )
+    unless ( -f "$dir/$file-header.csv" ||
+             -f "$dir/$file-detail.csv" )
     {
-      warn "$me neither $dir/agentnum$agentnum-header.csv nor ".
-           "$dir/agentnum$agentnum-detail.csv found\n" if $DEBUG;
+      warn "$me neither $dir/$file-header.csv nor ".
+           "$dir/$file-detail.csv found\n" if $DEBUG > 1;
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
       return;
     }
 
+    my $url      = $opt{url} or die "no url for agent $agentnum\n";
+    $url =~ s/^\s+//; $url =~ s/\s+$//;
+
+    my $username = $opt{username} or die "no username for agent $agentnum\n";
+    my $password = $opt{password} or die "no password for agent $agentnum\n";
+
     # a better way?
     if ($opt{m}) {
       my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ".
@@ -197,18 +183,18 @@ sub spool_upload {
     }
 
     foreach ( qw ( header detail ) ) {
-      rename "$dir/agentnum$agentnum-$_.csv",
-             "$dir/agentnum$agentnum-$opt{date}-$_.csv";
+      rename "$dir/$file-$_.csv",
+             "$dir/$file-$date-$_.csv";
     }
 
     my $command = "cd $dir; zip $zipfile ".
-                  "agentnum$agentnum-$opt{date}-header.csv ".
-                  "agentnum$agentnum-$opt{date}-detail.csv";
+                  "$file-$date-header.csv ".
+                  "$file-$date-detail.csv";
 
     system($command) and die "$command failed\n";
 
-    unlink "agentnum$agentnum-$opt{date}-header.csv",
-           "agentnum$agentnum-$opt{date}-detail.csv";
+    unlink "$file-$date-header.csv",
+           "$file-$date-detail.csv";
 
     if ( $url =~ /^http/i ) {
 
@@ -251,38 +237,132 @@ sub spool_upload {
       die "unknown scheme in URL $url\n";
     }
 
-  } else { #$opt{format} ne 'billco'
+  }
+  else { #not billco
+
+    my $targetnum = $opt{targetnum};
+    my $ftp_target = FS::ftp_target->by_key($targetnum)
+      or die "FTP target $targetnum not found\n";
+
+    $dir .= "/target$targetnum";
+    chdir($dir);
+
+    my $file  = $agentnum ? "agentnum$agentnum" : 'spool'; #.csv
 
-    my $date = $opt{date};
-    my $file = $opt{agentnum} ? "agentnum$opt{agentnum}" : 'spool'; #.csv
     unless ( -f "$dir/$file.csv" ) {
-      warn "$me $dir/$file.csv not found\n" if $DEBUG;
+      warn "$me $dir/$file.csv not found\n" if $DEBUG > 1;
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
       return;
     }
+
     rename "$dir/$file.csv", "$dir/$file-$date.csv";
 
-    #ftp only for now
-    if ( $url =~ m{^ftp://([\w\.]+)(/.*)$}i ) {
+    if ( $opt{'handling'} eq 'bridgestone' ) {
 
-      my ($hostname, $path) = ($1, $2);
-      my $ftp = new Net::FTP ($hostname)
-        or die "can't connect to $hostname: $@\n";
-      $ftp->login($username, $password)
-        or die "can't login to $hostname: ".$ftp->message."\n";
-      unless ( $ftp->cwd($path) ) {
-        my $msg = "can't cd $path on $hostname: ".$ftp->message."\n";
-        ( $path eq '/' ) ? warn $msg : die $msg;
+      my $prefix = $conf->config('bridgestone-prefix', $agentnum);
+      unless ( $prefix ) {
+        warn "$me agent $agentnum has no bridgestone-prefix, skipped\n";
+        $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+        return;
       }
-      chdir($dir);
-      $ftp->put("$file-$date.csv")
-        or die "can't put $file-$date.csv: ".$ftp->message."\n";
-      $ftp->quit;
 
-    } else {
-      die "malformed FTP URL $url\n";
+      my $seq = $conf->config('bridgestone-batch_counter', $agentnum) || 1;
+
+      # extract zip code
+      join(' ',$conf->config('company_address', $agentnum)) =~ 
+        /(\d{5}(\-\d{4})?)\s*$/;
+      my $ourzip = $1 || ''; #could be an explicit option if really needed
+      $ourzip  =~ s/\D//;
+      my $newfile = sprintf('%s_%s_%0.6d.dat', 
+                            $prefix,
+                            time2str('%Y%m%d', time),
+                            $seq);
+      warn "copying spool to $newfile\n" if $DEBUG;
+
+      my ($in, $out);
+      open $in, '<', "$dir/$file-$date.csv" 
+        or die "unable to read $file-$date.csv\n";
+      open $out, '>', "$dir/$newfile" or die "unable to write $newfile\n";
+      #header--not sure how much of this generalizes at all
+      my $head = sprintf(
+        "%-6s%-4s%-27s%-6s%0.6d%-5s%-9s%-9s%-7s%0.8d%-7s%0.6d\n",
+        ' COMP:', 'VISP', '', ',SEQ#:', $seq, ',ZIP:', $ourzip, ',VERS:1.1',
+        ',RUNDT:', time2str('%m%d%Y', $^T),
+        ',RUNTM:', time2str('%H%M%S', $^T),
+      );
+      warn "HEADER: $head" if $DEBUG;
+      print $out $head;
+
+      my $rows = 0;
+      while( <$in> ) {
+        print $out $_;
+        $rows++;
+      }
+
+      #trailer
+      my $trail = sprintf(
+        "%-6s%-4s%-27s%-6s%0.6d%-7s%0.9d%-9s%0.9d\n",
+        ' COMP:', 'VISP', '', ',SEQ:', $seq,
+        ',LINES:', $rows+2, ',LETTERS:', $rows,
+      );
+      warn "TRAILER: $trail" if $DEBUG;
+      print $out $trail;
+
+      close $in;
+      close $out;
+
+      my $zipfile = sprintf('%s_%0.6d.zip', $prefix, $seq);
+      my $command = "cd $dir; zip $zipfile $newfile";
+      warn "compressing to $zipfile\n$command\n" if $DEBUG;
+      system($command) and die "$command failed\n";
+
+      my $connection = $ftp_target->connect; # dies on error
+      $connection->put($zipfile);
+
+      my $template = join("\n",$conf->config('bridgestone-confirm_template'));
+      if ( $template ) {
+        my $tmpl_obj = Text::Template->new(
+          TYPE => 'STRING', SOURCE => $template
+        );
+        my $content = $tmpl_obj->fill_in( HASH =>
+          {
+            zipfile => $zipfile,
+            prefix  => $prefix,
+            seq     => $seq,
+            rows    => $rows,
+          }
+        );
+        my ($head, $body) = split("\n\n", $content, 2);
+        $head =~ /^subject:\s*(.*)$/im;
+        my $subject = $1;
+
+        $head =~ /^to:\s*(.*)$/im;
+        my $to = $1;
+
+        send_email(
+          to      => $to,
+          from    => $conf->config('invoice_from', $agentnum),
+          subject => $subject,
+          body    => $body,
+        );
+      } else { #!$template
+        warn "$me agent $agentnum has no bridgestone-confirm_template, no email sent\n";
+      }
+
+      $seq++;
+      warn "setting batch counter to $seq\n" if $DEBUG;
+      $conf->set('bridgestone-batch_counter', $seq, $agentnum);
+
+    } else { # not bridgestone
+
+      # this is the usual case
+
+      my $connection = $ftp_target->connect; # dies on error
+      $connection->put("$file-$date.csv");
+
     }
-  } #opt{format}
+
+  } #opt{handling}
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index b0f20ec..6c4a1b8 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -308,6 +308,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::access_groupsales;
   use FS::contact_class;
   use FS::part_svc_class;
+  use FS::ftp_target;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
index 297e39f..2be9ec2 100644
--- a/FS/FS/Misc.pm
+++ b/FS/FS/Misc.pm
@@ -913,6 +913,16 @@ sub ocr_image {
   @lines;
 }
 
+=item spool_formats
+  
+Returns a list of the invoice spool formats.
+
+=cut
+
+sub spool_formats {
+  qw(default oneline billco bridgestone)
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index a93a10a..0ac269f 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -2563,6 +2563,22 @@ sub ut_enumn {
     : '';
 }
 
+=item ut_flag COLUMN
+
+Check/untaint a column if it contains either an empty string or 'Y'.  This
+is the standard form for boolean flags in Freeside.
+
+=cut
+
+sub ut_flag {
+  my( $self, $field ) = @_;
+  my $value = uc($self->getfield($field));
+  if ( $value eq '' or $value eq 'Y' ) {
+    $self->setfield($field, $value);
+    return '';
+  }
+  return "Illegal (flag) field $field: $value";
+}
 
 =item ut_foreign_key COLUMN FOREIGN_TABLE FOREIGN_COLUMN
 
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 5476589..a90c73a 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3680,6 +3680,23 @@ sub tables_hashref {
       'index' => [ [ 'upgrade' ] ],
     },
 
+    'ftp_target' => {
+      'columns' => [
+        'targetnum', 'serial', '', '', '', '',
+        'agentnum', 'int', 'NULL', '', '', '',
+        'hostname', 'varchar', '', $char_d, '', '',
+        'port', 'int', '', '', '', '',
+        'username', 'varchar', '', $char_d, '', '',
+        'password', 'varchar', '', $char_d, '', '',
+        'path', 'varchar', '', $char_d, '', '',
+        'secure', 'char', 'NULL', 1, '', '',
+        'handling', 'varchar', 'NULL', $char_d, '', '',
+      ],
+      'primary_key' => 'targetnum',
+      'unique' => [ [ 'targetnum' ] ],
+      'index' => [],
+    },
+
     %{ tables_hashref_torrus() },
 
     # tables of ours for doing torrus virtual port combining
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 5b41d4b..d94ab20 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -1753,13 +1753,21 @@ Options are:
 
 =over 4
 
-=item format - 'default' or 'billco'
+=item format - any of FS::Misc::spool_formats
 
-=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
+=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
+customer has the corresponding invoice destinations set (see
+L<FS::cust_main_invoice>).
 
-=item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
+=item agent_spools - if set to a true value, will spool to per-agent files
+rather than a single global file
 
-=item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
+=item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will
+append to that spool.  L<FS::Cron::upload> will then send the spool file to
+that destination.
+
+=item balanceover - if set, only spools the invoice if the total amount owed on
+this invoice and all older invoices is greater than the specified amount.
 
 =back
 
@@ -1787,11 +1795,23 @@ sub spool_csv {
 
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
 
-  my $file =
-    "$spooldir/".
-    ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
-    ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
-    '.csv';
+  my $file;
+  if ( $opt{'agent_spools'} ) {
+    $file = 'agentnum'.$cust_main->agentnum;
+  } else {
+    $file = 'spool';
+  }
+
+  if ( $opt{'ftp_targetnum'} ) {
+    $spooldir .= '/target'.$opt{'ftp_targetnum'};
+    mkdir $spooldir, 0700 unless -d $spooldir;
+  } # otherwise it just goes into export.xxx/cust_bill
+
+  if ( lc($opt{'format'}) eq 'billco' ) {
+    $file .= '-header';
+  }
+
+  $file = "$spooldir/$file.csv";
   
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
@@ -1806,10 +1826,7 @@ sub spool_csv {
     flock(CSV, LOCK_UN);
     close CSV;
 
-    $file =
-      "$spooldir/".
-      ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
-      '-detail.csv';
+    $file =~ s/-header.csv$/-detail.csv/;
 
     open(CSV,">>$file") or die "can't open $file: $!";
     flock(CSV, LOCK_EX);
@@ -1831,7 +1848,7 @@ Returns CSV data for this invoice.
 
 Options are:
 
-format - 'default' or 'billco'
+format - 'default', 'billco', 'oneline', 'bridgestone'
 
 Returns a list consisting of two scalars.  The first is a single line of CSV
 header information for this invoice.  The second is one or more lines of CSV
@@ -1840,7 +1857,8 @@ detail information for this invoice.
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
-record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+record_type, invnum, custnum, _date, charged, first, last, company, address1, 
+address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
 
@@ -1945,6 +1963,26 @@ If I<format> is "billco", the fields of the detail CSV file are as follows:
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
+If format is 'oneline', there is no detail file.  Each invoice has a 
+header line only, with the fields:
+
+Agent number, agent name, customer number, first name, last name, address
+line 1, address line 2, city, state, zip, invoice date, invoice number,
+amount charged, amount due,
+
+and then, for each line item, three columns containing the package number,
+description, and amount.
+
+If format is 'bridgestone', there is no detail file.  Each invoice has a 
+header line with the following fields in a fixed-width format:
+
+Customer number (in display format), date, name (first last), company,
+address 1, address 2, city, state, zip.
+
+This is a mailing list format, and has no per-invoice fields.  To avoid
+sending redundant notices, the spooling event should have a "once" or 
+"once_percust_every" condition.
+
 =cut
 
 sub print_csv {
@@ -2041,6 +2079,31 @@ sub print_csv {
       @items,
     );
 
+  } elsif ( lc($opt{'format'}) eq 'bridgestone' ) {
+
+    # bypass the CSV stuff and just return this
+    my $longdate = time2str('%B %d, %Y', time); #current time, right?
+    my $zip = $cust_main->zip;
+    $zip =~ s/\D//;
+    my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
+      || '';
+    return (
+      sprintf(
+        "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
+        $prefix,
+        $cust_main->display_custnum,
+        $longdate,
+        uc(substr($cust_main->contact_firstlast,0,30)),
+        uc(substr($cust_main->company          ,0,30)),
+        uc(substr($cust_main->address1         ,0,30)),
+        uc(substr($cust_main->address2         ,0,30)),
+        uc(substr($cust_main->city             ,0,20)),
+        uc($cust_main->state),
+        $zip
+      ),
+      '' #detail
+      );
+
   } else {
   
     $csv->combine(
@@ -5442,6 +5505,7 @@ sub process_re_X {
 }
 
 sub re_X {
+  # spool_invoice ftp_invoice fax_invoice print_invoice
   my($method, $job, %param ) = @_;
   if ( $DEBUG ) {
     warn "re_X $method for job $job with param:\n".
diff --git a/FS/FS/ftp_target.pm b/FS/FS/ftp_target.pm
new file mode 100644
index 0000000..bf9fc89
--- /dev/null
+++ b/FS/FS/ftp_target.pm
@@ -0,0 +1,194 @@
+package FS::ftp_target;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use vars qw($me $DEBUG);
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::ftp_target - Object methods for ftp_target records
+
+=head1 SYNOPSIS
+
+  use FS::ftp_target;
+
+  $record = new FS::ftp_target \%hash;
+  $record = new FS::ftp_target { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::ftp_target object represents an account on a remote FTP or SFTP 
+server for transferring files.  FS::ftp_target inherits from FS::Record.
+
+=over 4
+
+=item targetnum - primary key
+
+=item agentnum - L<FS::agent> foreign key; can be null
+
+=item hostname - the DNS name of the FTP site
+
+=item username - username
+
+=item password - password
+
+=item path - the working directory to change to upon connecting
+
+=item secure - a flag ('Y' or null) for whether to use SFTP
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+sub table { 'ftp_target'; }
+
+=item new HASHREF
+
+Creates a new FTP target.  To add it to the database, see L<"insert">.
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  if ( !$self->get('port') ) {
+    if ( $self->secure ) {
+      $self->set('port', 22);
+    } else {
+      $self->set('port', 21);
+    }
+  }
+
+  my $error = 
+    $self->ut_numbern('targetnum')
+    || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
+    || $self->ut_text('hostname')
+    || $self->ut_text('username')
+    || $self->ut_text('password')
+    || $self->ut_number('port')
+    || $self->ut_text('path')
+    || $self->ut_flag('secure')
+    || $self->ut_enum('handling', [ $self->handling_types ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item connect
+
+Creates a Net::FTP or Net::SFTP::Foreign object (according to the setting
+of the 'secure' flag), connects to 'hostname', attempts to log in with 
+'username' and 'password', and changes the working directory to 'path'.
+On success, returns the object.  On failure, dies with an error message.
+
+=cut
+
+sub connect {
+  my $self = shift;
+  if ( $self->secure ) {
+    eval "use Net::SFTP::Foreign;";
+    die $@ if $@;
+    my %args = (
+      port      => $self->port,
+      user      => $self->username,
+      password  => $self->password,
+      more      => ($DEBUG ? '-v' : ''),
+      timeout   => 30,
+      autodie   => 1, #we're doing this anyway
+    );
+    my $sftp = Net::SFTP::Foreign->new($self->hostname, %args);
+    $sftp->setcwd($self->path);
+    return $sftp;
+  }
+  else {
+    eval "use Net::FTP;";
+    die $@ if $@;
+    my %args = ( 
+      Debug   => $DEBUG,
+      Port    => $self->port,
+      Passive => 1,# optional?
+    );
+    my $ftp = Net::FTP->new($self->hostname, %args)
+      or die "connect to ".$self->hostname." failed: $@";
+    $ftp->login($self->username, $self->password)
+      or die "login to ".$self->username.'@'.$self->hostname." failed: $@";
+    $ftp->binary; #optional?
+    $ftp->cwd($self->path)
+      or ($self->path eq '/')
+      or die "cwd to ".$self->hostname.'/'.$self->path." failed: $@";
+
+    return $ftp;
+  }
+}
+
+=item label
+
+Returns a descriptive label for this target.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->targetnum . ': ' . $self->username . '@' . $self->hostname;
+}
+
+=item handling_types
+
+Returns a list of values for the "handling" field, corresponding to the 
+known ways to preprocess a file before uploading.  Currently those are 
+implemented somewhat crudely in L<FS::Cron::upload>.
+
+=cut
+
+sub handling_types {
+  '',
+  #'billco', #not implemented this way yet
+  'bridgestone',
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm
index 71bbaa8..8d2e36c 100644
--- a/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm
@@ -2,6 +2,7 @@ package FS::part_event::Action::cust_bill_send_csv_ftp;
 
 use strict;
 use base qw( FS::part_event::Action );
+use FS::Misc;
 
 sub description { 'Upload CSV invoice data to an FTP server'; }
 
@@ -15,11 +16,7 @@ sub option_fields {
   (
     'ftpformat'   => { label   => 'Format',
                        type    =>'select',
-                       options => ['default', 'billco', 'oneline'],
-                       option_labels => { 'default' => 'Default',
-                                          'billco'  => 'Billco',
-                                          'oneline' => 'One line',
-                                        },
+                       options => [ FS::Misc::spool_formats() ],
                      },
     'ftpserver'   => 'FTP server',
     'ftpusername' => 'FTP username',
diff --git a/FS/FS/part_event/Action/cust_bill_spool_csv.pm b/FS/FS/part_event/Action/cust_bill_spool_csv.pm
index 1504a4f..24c26ff 100644
--- a/FS/FS/part_event/Action/cust_bill_spool_csv.pm
+++ b/FS/FS/part_event/Action/cust_bill_spool_csv.pm
@@ -2,6 +2,7 @@ package FS::part_event::Action::cust_bill_spool_csv;
 
 use strict;
 use base qw( FS::part_event::Action );
+use FS::Misc;
 
 sub description { 'Spool CSV invoice data'; }
 
@@ -15,11 +16,7 @@ sub option_fields {
   (
     'spoolformat'       => { label   => 'Format',
                              type    => 'select',
-                             options => ['default', 'billco', 'oneline'],
-                             option_labels => { 'default' => 'Default',
-                                                'billco'  => 'Billco',
-                                                'oneline' => 'One line',
-                                              },
+                             options => [ FS::Misc::spool_formats() ],
                            },
     'spoolbalanceover'  => { label =>
                                'If balance (this invoice and previous) over',
@@ -29,6 +26,13 @@ sub option_fields {
                              type  => 'checkbox',
                              value => '1',
                            },
+    'ftp_targetnum'     => { label    => 'Upload spool to FTP target',
+                             type     => 'select-table',
+                             table    => 'ftp_target',
+                             name_col => 'label',
+                             empty_label => '(do not upload)',
+                             order_by => 'targetnum',
+                           },
   );
 }
 
@@ -44,6 +48,7 @@ sub do_action {
     'format'       => $self->option('spoolformat'),
     'balanceover'  => $self->option('spoolbalanceover'),
     'agent_spools' => $self->option('spoolagent_spools'),
+    'ftp_targetnum'=> $self->option('ftp_targetnum'),
   );
 }
 
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 8bb4eee..030e69b 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -640,3 +640,5 @@ FS/access_groupsales.pm
 t/access_groupsales.t
 FS/part_svc_class.pm
 t/part_svc_class.t
+FS/ftp_target.pm
+t/ftp_target.t
diff --git a/FS/t/ftp_target.t b/FS/t/ftp_target.t
new file mode 100644
index 0000000..1a59281
--- /dev/null
+++ b/FS/t/ftp_target.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::ftp_target;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/ftp_target.html b/httemplate/browse/ftp_target.html
new file mode 100644
index 0000000..4a57820
--- /dev/null
+++ b/httemplate/browse/ftp_target.html
@@ -0,0 +1,56 @@
+<& elements/browse.html,
+  'title'       => 'FTP targets',
+  'menubar'     => [ 'Add a target' => $p.'edit/ftp_target.html', ],
+  'name'        => 'FTP targets',
+  'query'       => { 'table'   => 'ftp_target',
+                     'hashref' => {},
+                   },
+  'count_query' => $count_query,
+  'header'      => [ '#',
+                     'Server',
+                     'Username',
+                     'Password',
+                     'Path',
+                     'Protocol',
+                     '', #handling
+                   ],
+  'fields'      => [ 'targetnum',
+                     'hostname',
+                     'username',
+                     'password',
+                     'path',
+                     sub { 
+                       my $ftp_target = shift;
+                       my $label;
+                       if ($ftp_target->secure) {
+                         $label = 'SFTP';
+                         $label .= ' (port '.$ftp_target->port.')'
+                           if $ftp_target->port != 22;
+                       }
+                       else {
+                         $label = 'FTP';
+                         $label .= ' (port '.$ftp_target->port.')'
+                           if $ftp_target->port != 21;
+                       }
+                       $label;
+                     },
+                     'handling',
+                   ],
+  'links'       => [ $link, $link ],
+&>
+</TABLE>
+
+<% include('/elements/footer.html') %>
+
+<%once>
+
+my $count_query = 'SELECT COUNT(*) FROM ftp_target';
+
+</%once>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $link = [ $p.'edit/ftp_target.html?', 'targetnum' ];
+</%init>
diff --git a/httemplate/edit/ftp_target.html b/httemplate/edit/ftp_target.html
new file mode 100755
index 0000000..aebf9aa
--- /dev/null
+++ b/httemplate/edit/ftp_target.html
@@ -0,0 +1,46 @@
+<& elements/edit.html,
+  'post_url'    => popurl(1).'process/ftp_target.html',
+  'name'        => 'FTP target',
+  'table'       => 'ftp_target',
+  'viewall_url' => "${p}browse/ftp_target.html",
+  'labels'      => { targetnum => 'Target',
+                     hostname  => 'Server',
+                     username  => 'Username',
+                     password  => 'Password',
+                     path      => 'Directory',
+                     port      => 'Port',
+                     secure    => 'Use SFTP',
+                     handling  => 'Special handling',
+                   },
+  'fields'      => [
+                     { field => 'hostname', size => 40 },
+                     { field => 'port', size => 8 },
+                     { field => 'secure', type => 'checkbox', value => 'Y' },
+                     'username',
+                     'password',
+                     { field => 'path', size => 40 },
+                     { field => 'handling', 
+                       type => 'select',
+                       options => [ FS::ftp_target->handling_types ],
+                     },
+                   ],
+  'menubar'     => \@menubar,
+  'edit_callback' => $edit_callback,
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Configuration');
+
+my @menubar = ('View all FTP targets' => $p.'browse/ftp_target.html');
+my $edit_callback = sub {
+  my ($cgi, $object) = @_;
+  if ( $object->targetnum ) {
+    push @menubar, 'Delete this target', 
+                   $p.'misc/delete-ftp_target.html?'.$object->targetnum;
+  }
+};
+
+</%init>
diff --git a/httemplate/edit/process/ftp_target.html b/httemplate/edit/process/ftp_target.html
new file mode 100644
index 0000000..35f56c4
--- /dev/null
+++ b/httemplate/edit/process/ftp_target.html
@@ -0,0 +1,12 @@
+<& elements/process.html,
+           'table'            => 'ftp_target',
+           'viewall_dir'      => 'browse',
+           'agent_null'       => 1,
+&>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Configuration');
+
+</%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index cf79af9..06f7d59 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -579,6 +579,9 @@ $config_misc{'Inventory classes and inventory'} = [ $fsurl.'browse/inventory_cla
   || $curuser->access_right('Edit global inventory')
   || $curuser->access_right('Configuration');
 
+$config_misc{'FTP targets'} = [ $fsurl.'browse/ftp_target.html', 'FTP servers for billing and payment processing' ]
+  if $curuser->access_right('Configuration');
+
 tie my %config_menu, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration' ) ) {
   %config_menu = (
diff --git a/httemplate/misc/delete-ftp_target.html b/httemplate/misc/delete-ftp_target.html
new file mode 100644
index 0000000..c8bd297
--- /dev/null
+++ b/httemplate/misc/delete-ftp_target.html
@@ -0,0 +1,18 @@
+% if ( $error ) {
+%   errorpage($error);
+% } else {
+<% $cgi->redirect("${p}browse/ftp_target.html") %>
+% }
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal targetnum";
+my $targetnum = $1;
+
+my $target = qsearchs('ftp_target',{'targetnum'=>$targetnum});
+my $error = $target->delete;
+
+</%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Conf.pm                                     |   36 ++-
 FS/FS/Cron/upload.pm                              |  290 +++++++++++++--------
 FS/FS/Mason.pm                                    |    1 +
 FS/FS/Misc.pm                                     |   10 +
 FS/FS/Record.pm                                   |   16 ++
 FS/FS/Schema.pm                                   |   17 ++
 FS/FS/cust_bill.pm                                |   94 ++++++-
 FS/FS/ftp_target.pm                               |  194 ++++++++++++++
 FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm |    7 +-
 FS/FS/part_event/Action/cust_bill_spool_csv.pm    |   15 +-
 FS/MANIFEST                                       |    2 +
 FS/t/{ConfItem.t => ftp_target.t}                 |    2 +-
 httemplate/browse/ftp_target.html                 |   56 ++++
 httemplate/edit/ftp_target.html                   |   46 ++++
 httemplate/edit/process/ftp_target.html           |   12 +
 httemplate/elements/menu.html                     |    3 +
 httemplate/misc/delete-ftp_target.html            |   18 ++
 17 files changed, 680 insertions(+), 139 deletions(-)
 create mode 100644 FS/FS/ftp_target.pm
 copy FS/t/{ConfItem.t => ftp_target.t} (83%)
 create mode 100644 httemplate/browse/ftp_target.html
 create mode 100755 httemplate/edit/ftp_target.html
 create mode 100644 httemplate/edit/process/ftp_target.html
 create mode 100644 httemplate/misc/delete-ftp_target.html




More information about the freeside-commits mailing list