1 package FS::Cron::upload;
4 use vars qw( @ISA @EXPORT_OK $me $DEBUG );
8 use FS::Record qw( qsearch qsearchs );
12 use FS::Misc qw( send_email ); #for bridgestone
13 use FS::upload_target;
16 use HTTP::Request::Common;
19 use List::Util qw( sum );
21 @ISA = qw( Exporter );
22 @EXPORT_OK = qw ( upload );
24 $me = '[FS::Cron::upload]';
27 # -v: enable debugging
29 # -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing.
30 # -r: Multi-process mode dry run option
31 # -a: Only process customers with the specified agentnum
38 $debug = 1 if $opt{'v'};
39 $debug = $opt{'l'} if $opt{'l'};
41 local $DEBUG = $debug if $debug;
43 warn "$me upload called\n" if $DEBUG;
47 my $date = time2str('%Y%m%d%H%M%S', $^T); # more?
49 my $conf = new FS::Conf;
51 my @agents = $opt{'a'} ? FS::agent->by_key($opt{'a'}) : qsearch('agent', {});
60 my @agentnums = ('', map {$_->agentnum} @agents);
62 foreach my $target (qsearch('upload_target', {})) {
63 # We don't know here if it's spooled on a per-agent basis or not.
64 # (It could even be both, via different events.) So queue up an
65 # upload for each agent, plus one with null agentnum, and we'll
66 # upload as many files as we find.
67 foreach my $a (@agentnums) {
71 'targetnum' => $target->targetnum,
72 'handling' => $target->handling,
77 # deprecated billco method
79 my $agentnum = $_->agentnum;
81 if ( $conf->config( 'billco-username', $agentnum, 1 ) ) {
82 my $username = $conf->config('billco-username', $agentnum, 1);
83 my $password = $conf->config('billco-password', $agentnum, 1);
84 my $clicode = $conf->config('billco-clicode', $agentnum, 1);
85 my $url = $conf->config('billco-url', $agentnum);
88 'agentnum' => $agentnum,
89 'username' => $username,
90 'password' => $password,
92 'clicode' => $clicode,
93 'handling' => 'billco',
98 # if there's nothing to do, don't hold up the rest of the process
101 # wait for any ongoing billing jobs to complete
104 my $sql = "SELECT count(*) FROM queue LEFT JOIN cust_main USING(custnum) ".
105 "WHERE queue.job='FS::cust_main::queued_bill' AND status != 'failed'";
107 $sql .= ' AND cust_main.agentnum IN('.
108 join(',', map {$_->agentnum} @agents).
111 my $sth = $dbh->prepare($sql) or die $dbh->errstr;
114 or die "Unexpected error executing statement $sql: ". $sth->errstr;
115 last if $sth->fetchrow_arrayref->[0] == 0;
116 warn "Waiting 5min for billing to complete...\n" if $DEBUG;
123 my $agentnum = $_->{agentnum};
128 warn "DRY RUN: would add agent $agentnum for queued upload\n";
130 my $queue = new FS::queue {
131 'job' => 'FS::Cron::upload::spool_upload',
133 my $error = $queue->insert( %$_ );
138 eval { spool_upload(%$_) };
139 warn "spool_upload failed: $@\n"
151 warn "$me spool_upload called\n" if $DEBUG;
152 my $conf = new FS::Conf;
153 my $dir = '%%%FREESIDE_EXPORT%%%/export.'. $FS::UID::datasrc. '/cust_bill';
155 my $date = $opt{date} or die "no date provided\n";
157 local $SIG{HUP} = 'IGNORE';
158 local $SIG{INT} = 'IGNORE';
159 local $SIG{QUIT} = 'IGNORE';
160 local $SIG{TERM} = 'IGNORE';
161 local $SIG{TSTP} = 'IGNORE';
162 local $SIG{PIPE} = 'IGNORE';
164 my $oldAutoCommit = $FS::UID::AutoCommit;
165 local $FS::UID::AutoCommit = 0;
168 my $agentnum = $opt{agentnum};
172 $agent = qsearchs( 'agent', { agentnum => $agentnum } )
173 or die "no such agent: $agentnum";
174 $agent->select_for_update; #mutex
177 if ( $opt{'handling'} eq 'billco' ) {
179 my $file = "agentnum$agentnum";
180 my $zipfile = "$dir/$file-$date.zip";
182 unless ( -f "$dir/$file-header.csv" ||
183 -f "$dir/$file-detail.csv" )
185 warn "$me neither $dir/$file-header.csv nor ".
186 "$dir/$file-detail.csv found\n" if $DEBUG > 1;
187 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
191 my $url = $opt{url} or die "no url for agent $agentnum\n";
192 $url =~ s/^\s+//; $url =~ s/\s+$//;
194 my $username = $opt{username} or die "no username for agent $agentnum\n";
195 my $password = $opt{password} or die "no password for agent $agentnum\n";
197 foreach ( qw ( header detail ) ) {
198 rename "$dir/$file-$_.csv",
199 "$dir/$file-$date-$_.csv";
202 my $command = "cd $dir; zip $zipfile ".
203 "$file-$date-header.csv ".
204 "$file-$date-detail.csv";
206 system($command) and die "$command failed\n";
208 unlink "$file-$date-header.csv",
209 "$file-$date-detail.csv";
211 if ( $url =~ /^http/i ) {
213 my $ua = new LWP::UserAgent;
214 my $res = $ua->request( POST( $url,
215 'Content_Type' => 'form-data',
216 'Content' => [ 'username' => $username,
218 'custid' => $username,
219 'clicode' => $opt{clicode},
220 'file1' => [ $zipfile ],
225 die "upload failed: ". $res->status_line. "\n"
226 unless $res->is_success;
228 } elsif ( $url =~ /^ftp:\/\/([\w\.]+)(\/.*)$/i ) {
230 my($hostname, $path) = ($1, $2);
232 my $ftp = new Net::FTP($hostname, Passive=>1)
233 or die "can't connect to $hostname: $@\n";
234 $ftp->login($username, $password)
235 or die "can't login to $hostname: ". $ftp->message."\n";
236 unless ( $ftp->cwd($path) ) {
237 my $msg = "can't cd $path on $hostname: ". $ftp->message. "\n";
238 ( $path eq '/' ) ? warn $msg : die $msg;
241 or die "can't set binary mode on $hostname\n";
244 or die "can't put $zipfile: ". $ftp->message. "\n";
249 die "unknown scheme in URL $url\n";
255 my $targetnum = $opt{targetnum};
256 my $upload_target = FS::upload_target->by_key($targetnum)
257 or die "FTP target $targetnum not found\n";
259 $dir .= "/target$targetnum";
262 my $file = $agentnum ? "agentnum$agentnum" : 'spool'; #.csv
264 unless ( -f "$dir/$file.csv" ) {
265 warn "$me $dir/$file.csv not found\n" if $DEBUG > 1;
266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 rename "$dir/$file.csv", "$dir/$file-$date.csv";
272 if ( $opt{'handling'} eq 'bridgestone' ) {
274 my $prefix = $conf->config('bridgestone-prefix', $agentnum);
276 warn "$me agent $agentnum has no bridgestone-prefix, skipped\n";
277 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
281 my $seq = $conf->config('bridgestone-batch_counter', $agentnum) || 1;
284 join(' ',$conf->config('company_address', $agentnum)) =~
285 /(\d{5}(\-\d{4})?)\s*$/;
286 my $ourzip = $1 || ''; #could be an explicit option if really needed
288 my $newfile = sprintf('%s_%s_%0.6d.dat',
290 time2str('%Y%m%d', time),
292 warn "copying spool to $newfile\n" if $DEBUG;
295 open $in, '<', "$dir/$file-$date.csv"
296 or die "unable to read $file-$date.csv\n";
297 open $out, '>', "$dir/$newfile" or die "unable to write $newfile\n";
298 #header--not sure how much of this generalizes at all
300 "%-6s%-4s%-27s%-6s%0.6d%-5s%-9s%-9s%-7s%0.8d%-7s%0.6d\n",
301 ' COMP:', 'VISP', '', ',SEQ#:', $seq, ',ZIP:', $ourzip, ',VERS:1.1',
302 ',RUNDT:', time2str('%m%d%Y', $^T),
303 ',RUNTM:', time2str('%H%M%S', $^T),
305 warn "HEADER: $head" if $DEBUG;
316 "%-6s%-4s%-27s%-6s%0.6d%-7s%0.9d%-9s%0.9d\n",
317 ' COMP:', 'VISP', '', ',SEQ:', $seq,
318 ',LINES:', $rows+2, ',LETTERS:', $rows,
320 warn "TRAILER: $trail" if $DEBUG;
326 my $zipfile = sprintf('%s_%0.6d.zip', $prefix, $seq);
327 my $command = "cd $dir; zip $zipfile $newfile";
328 warn "compressing to $zipfile\n$command\n" if $DEBUG;
329 system($command) and die "$command failed\n";
331 my $error = $upload_target->put($zipfile);
333 foreach ( qw ( header detail ) ) {
334 rename "$dir/$file-$date-$_.csv",
341 prepare_report('bridgestone-confirm_template',
343 agentnum=> $agentnum,
353 warn "setting batch counter to $seq\n" if $DEBUG;
354 $conf->set('bridgestone-batch_counter', $seq, $agentnum);
356 } elsif ( $opt{'handling'} eq 'ics' ) {
358 my ($basename, $regfile, $bigfile);
359 $basename = sprintf('c%sc1', time2str('%m%d', time));
360 $regfile = $basename . 'i.txt'; # for "regular" (short) invoices
361 $bigfile = $basename . 'b.txt'; # for "big" invoices
363 warn "copying spool to $regfile, $bigfile\n" if $DEBUG;
365 my ($in, $reg, $big); #filehandles
366 my %count = (B => 0, 1 => 0, 2 => 0); # number of invoices
367 my %sum = (B => 0, R => 0); # total of charges field
368 open $in, '<', "$dir/$file-$date.csv"
369 or die "unable to read $file-$date.csv\n";
371 open $reg, '>', "$dir/$regfile" or die "unable to write $regfile\n";
372 open $big, '>', "$dir/$bigfile" or die "unable to write $bigfile\n";
374 while (my $line = <$in>) {
376 my $tag = substr($line, -1, 1, '');
377 my $charge = substr($line, 252, 10);
379 print $big $line, "\n";
383 print $reg $line, "\n";
392 # zip up all three files for transport
393 my $zipfile = "$basename" . '.zip';
394 my $command = "cd $dir; zip $zipfile $regfile $bigfile";
395 system($command) and die "'$command' failed\n";
397 # upload them, unless we're using email, in which case
398 # the zip file will ride along with the report. yes, this
399 # kind of defeats the purpose of the upload_target interface,
400 # but at least we have a place to store the configuration.
402 if ( $upload_target->protocol ne 'email' ) {
403 $error = $upload_target->put("$dir/$zipfile");
408 $_ = sprintf('%.2f', $_);
411 my %report = prepare_report('ics-confirm_template',
413 agentnum => $agentnum,
419 if ( $upload_target->protocol eq 'email' ) {
421 join('@', $upload_target->username, $upload_target->hostname);
422 $report{'subject'} = $upload_target->subject;
423 $report{'mimeparts'} = [
424 { Path => "$dir/$zipfile",
425 Type => 'application/zip',
426 Encoding => 'base64',
427 Filename => $zipfile,
428 Disposition => 'attachment',
432 $error = send_email(%report);
435 # put the original spool file back
436 rename "$dir/$file-$date.csv", "$dir/$file.csv";
440 } else { # not bridgestone or ics
442 # this is the usual case
444 my $error = $upload_target->put("$file-$date.csv");
446 rename "$dir/$file-$date.csv", "$dir/$file.csv";
454 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
459 =item send_report CONFIG PARAMS
461 Retrieves the config value named CONFIG, parses it as a Text::Template,
462 extracts "to" and "subject" headers, and returns a hash that can be passed
463 to L<FS::Misc::send_email>.
465 PARAMS is a hashref to be passed to C<fill_in>. It must contain
466 'agentnum' to look up the per-agent config.
470 # we used it twice, so it's now a subroutine
474 my ($config, $params) = @_;
475 my $agentnum = $params->{agentnum};
476 my $conf = FS::Conf->new;
478 my $template = join("\n", $conf->config($config, $agentnum));
480 warn "$me agent $agentnum has no $config, no email report sent\n";
484 my $tmpl_obj = Text::Template->new(
485 TYPE => 'STRING', SOURCE => $template
487 my $content = $tmpl_obj->fill_in( HASH => $params );
488 my ($head, $body) = split("\n\n", $content, 2);
489 $head =~ /^subject:\s*(.*)$/im;
492 $head =~ /^to:\s*(.*)$/im;
497 from => $conf->config('invoice_from', $agentnum),