summaryrefslogtreecommitdiff
path: root/FS/FS
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2012-06-13 16:18:49 -0700
committerMark Wells <mark@freeside.biz>2012-06-13 16:18:49 -0700
commit9ef78be87df0f0f880ff5d903ed6243b67369cf0 (patch)
treee69278b1e33baf2b9c0356ed31435fc0f0188b01 /FS/FS
parentdaa09251fec52517b630b3f6935041dc7c795f90 (diff)
table of FTP targets for invoice spool upload, #17620
Diffstat (limited to 'FS/FS')
-rw-r--r--FS/FS/Conf.pm36
-rw-r--r--FS/FS/Cron/upload.pm290
-rw-r--r--FS/FS/Mason.pm1
-rw-r--r--FS/FS/Misc.pm10
-rw-r--r--FS/FS/Record.pm16
-rw-r--r--FS/FS/Schema.pm17
-rw-r--r--FS/FS/cust_bill.pm94
-rw-r--r--FS/FS/ftp_target.pm194
-rw-r--r--FS/FS/part_event/Action/cust_bill_send_csv_ftp.pm7
-rw-r--r--FS/FS/part_event/Action/cust_bill_spool_csv.pm15
10 files changed, 542 insertions, 138 deletions
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'),
);
}