From 9ef78be87df0f0f880ff5d903ed6243b67369cf0 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Wed, 13 Jun 2012 16:18:49 -0700 Subject: [PATCH] table of FTP targets for invoice spool upload, #17620 --- 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/ftp_target.t | 5 + 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, 684 insertions(+), 138 deletions(-) create mode 100644 FS/FS/ftp_target.pm create mode 100644 FS/t/ftp_target.t 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 diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 151e31c7d..0314992d8 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 c2667978d..51e0d6868 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 b0f20ec65..6c4a1b81d 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 297e39fbc..2be9ec203 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 a93a10ac6..0ac269f4c 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 547658904..a90c73a95 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 5b41d4b3b..d94ab2094 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). +=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the +customer has the corresponding invoice destinations set (see +L). -=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), will +append to that spool. L 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 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 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 000000000..bf9fc891a --- /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 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. + +=cut + +sub handling_types { + '', + #'billco', #not implemented this way yet + 'bridgestone', +} + +=back + +=head1 SEE ALSO + +L, 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 71bbaa89b..8d2e36cd1 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 1504a4fa9..24c26ff6a 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 8bb4eee6d..030e69bbf 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 000000000..1a5928118 --- /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 000000000..4a5782058 --- /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 ], +&> + + +<% include('/elements/footer.html') %> + +<%once> + +my $count_query = 'SELECT COUNT(*) FROM ftp_target'; + + +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); + +my $link = [ $p.'edit/ftp_target.html?', 'targetnum' ]; + diff --git a/httemplate/edit/ftp_target.html b/httemplate/edit/ftp_target.html new file mode 100755 index 000000000..aebf9aaed --- /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; + } +}; + + diff --git a/httemplate/edit/process/ftp_target.html b/httemplate/edit/process/ftp_target.html new file mode 100644 index 000000000..35f56c490 --- /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'); + + diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index cf79af9d5..06f7d59ea 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 000000000..c8bd29701 --- /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; + + -- 2.11.0