RT#947: batch download of invoice PDFs
authormark <mark>
Tue, 8 Jun 2010 22:25:01 +0000 (22:25 +0000)
committermark <mark>
Tue, 8 Jun 2010 22:25:01 +0000 (22:25 +0000)
12 files changed:
FS/FS/Conf.pm
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/bill_batch.pm [new file with mode: 0644]
FS/FS/cust_bill.pm
FS/FS/cust_bill_batch.pm [new file with mode: 0644]
FS/FS/cust_bill_batch_option.pm [new file with mode: 0644]
httemplate/elements/menu.html
httemplate/elements/progress-init.html
httemplate/misc/process/bill_batch-print.html [new file with mode: 0644]
httemplate/search/bill_batch.cgi [new file with mode: 0755]
httemplate/view/bill_batch.cgi [new file with mode: 0644]

index 89a36af..9046b26 100644 (file)
@@ -1036,6 +1036,12 @@ worry that config_items is freeside-specific and icky.
     'type'        => 'textarea'
   },
 
+  {
+    'key'         => 'invoice_print_pdf',
+    'section'     => 'invoicing',
+    'description' => 'Store postal invoices for download in PDF format rather than printing them directly.',
+    'type'        => 'checkbox',
+  },
 
   { 
     'key'         => 'invoice_default_terms',
index 150a6c0..84c9f7f 100644 (file)
@@ -240,6 +240,8 @@ if ( -e $addl_handler_use_file ) {
   use FS::cgp_rule;
   use FS::cgp_rule_condition;
   use FS::cgp_rule_action;
+  use FS::bill_batch;
+  use FS::cust_bill_batch;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 859224f..01512f9 100644 (file)
@@ -2848,6 +2848,41 @@ sub tables_hashref {
       'index'       => [['listnum'],['svcnum'],['contactemailnum'],['email']],
     },
 
+    'bill_batch' => {
+      'columns' => [
+        'batchnum',         'serial',     '', '', '', '',
+        'status',             'char', 'NULL','1', '', '',
+        'pdf',                'blob', 'NULL', '', '', '',
+      ],
+      'primary_key' => 'batchnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'cust_bill_batch' => {
+      'columns' => [
+        'billbatchnum',     'serial',     '', '', '', '',
+        'batchnum',            'int',     '', '', '', '',
+        'invnum',              'int',     '', '', '', '',
+      ],
+      'primary_key' => 'billbatchnum',
+      'unique'      => [],
+      'index'       => [ [ 'batchnum' ], [ 'invnum' ] ],
+    },
+
+    'cust_bill_batch_option' => {
+      'columns' => [
+        'optionnum', 'serial', '', '', '', '', 
+        'billbatchnum', 'int', '', '', '', '', 
+        'optionname', 'varchar', '', $char_d, '', '', 
+        'optionvalue', 'text', 'NULL', '', '', '', 
+      ],
+      'primary_key' => 'optionnum',
+      'unique'      => [],
+      'index'       => [ [ 'billbatchnum' ], [ 'optionname' ] ],
+    },
+
+
 
     # name type nullability length default local
 
diff --git a/FS/FS/bill_batch.pm b/FS/FS/bill_batch.pm
new file mode 100644 (file)
index 0000000..136db0d
--- /dev/null
@@ -0,0 +1,151 @@
+package FS::bill_batch;
+
+use strict;
+use vars qw( @ISA $me $DEBUG );
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::cust_bill_batch;
+
+@ISA = qw( FS::Record );
+$me = '[ FS::bill_batch ]';
+$DEBUG=0;
+
+sub table { 'bill_batch' }
+
+sub nohistory_fields { 'pdf' }
+
+=head1 NAME
+
+FS::bill_batch - Object methods for bill_batch records
+
+=head1 SYNOPSIS
+
+  use FS::bill_batch;
+
+  $open_batch = FS::bill_batch->get_open_batch;
+  
+  my $pdf = $open_batch->print_pdf;
+  
+  $error = $open_batch->close;
+  
+=head1 DESCRIPTION
+
+An FS::bill_batch object represents a batch of invoices.  FS::bill_batch 
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item batchnum - primary key
+
+=item status - either 'O' (open) or 'R' (resolved/closed).
+
+=item pdf - blob field for temporarily storing the invoice as a PDF.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item print_pdf
+
+Typeset the entire batch as a PDF file.  Returns the PDF as a string.
+
+=cut
+
+sub print_pdf {
+  eval 'use CAM::PDF';
+  warn "Failed to load CAM::PDF: '$@'\n" if $@;
+
+  my $self = shift;
+  my $job = shift;
+  $job->update_statustext(0) if $job;
+  my @invoices = sort { $a->invnum <=> $b->invnum }
+                 qsearch('cust_bill_batch', { batchnum => $self->batchnum });
+  return "No invoices in batch ".$self->batchnum.'.' if !@invoices;
+
+  my $pdf_out;
+  my $num = 0;
+  foreach my $invoice (@invoices) {
+    my $part = $invoice->cust_bill->print_pdf({$invoice->options});
+    die 'Failed creating PDF from invoice '.$invoice->invnum.'\n' if !$part;
+
+    if($pdf_out) {
+      $pdf_out->appendPDF(CAM::PDF->new($part));
+    }
+    else {
+      $pdf_out = CAM::PDF->new($part);
+    }
+    if($job) {
+      # update progressbar
+      $num++;
+      my $error = $job->update_statustext(int(100 * $num/scalar(@invoices)));
+      die $error if $error;
+    }
+  }
+
+  return $pdf_out->toPDF;
+}
+
+=item close
+
+Set the status of the batch to 'R' (resolved).
+
+=cut
+
+sub close {
+  my $self = shift;
+  $self->status('R');
+  return $self->replace;
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=item get_open_batch
+
+Returns the currently open batch.  There should only be one at a time.
+
+=cut
+
+sub get_open_batch {
+  my $class = shift;
+  my $batch = qsearchs('bill_batch', { status => 'O' });
+  return $batch if $batch;
+  $batch = FS::bill_batch->new({status => 'O'});
+  my $error = $batch->insert;
+  die $error if $error;
+  return $batch;
+}
+
+use Storable 'thaw';
+use Data::Dumper;
+use MIME::Base64;
+
+sub process_print_pdf {
+  my $job = shift;
+  my $param = thaw(decode_base64(shift));
+  warn Dumper($param) if $DEBUG;
+  die "no batchnum specified!\n" if ! exists($param->{batchnum});
+  my $batch = FS::bill_batch->by_key($param->{batchnum});
+  die "batch '$param->{batchnum}' not found!\n" if !$batch;
+
+  my $pdf = $batch->print_pdf($job);
+  $batch->pdf($pdf);
+  my $error = $batch->replace;
+  die $error if $error;
+}
+
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 1270530..a1dab4a 100644 (file)
@@ -34,6 +34,8 @@ use FS::cust_bill_pay;
 use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
+use FS::bill_batch;
+use FS::cust_bill_batch;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
@@ -1300,7 +1302,13 @@ sub print {
     'notice_name' => $notice_name,
   );
 
-  do_print $self->lpr_data(\%opt);
+  if($conf->exists('invoice_print_pdf')) {
+    # Add the invoice to the current batch.
+    $self->batch_invoice(\%opt);
+  }
+  else {
+    do_print $self->lpr_data(\%opt);
+  }
 }
 
 =item fax_invoice HASHREF | [ TEMPLATE ] 
@@ -1346,6 +1354,23 @@ sub fax_invoice {
 
 }
 
+=item batch_invoice [ HASHREF ]
+
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+isn't an open batch, one will be created.
+
+=cut
+
+sub batch_invoice {
+  my ($self, $opt) = @_;
+  my $batch = FS::bill_batch->get_open_batch;
+  my $cust_bill_batch = FS::cust_bill_batch->new({
+      batchnum => $batch->batchnum,
+      invnum   => $self->invnum,
+  });
+  return $cust_bill_batch->insert($opt);
+}
+
 =item ftp_invoice [ TEMPLATENAME ] 
 
 Sends this invoice data via FTP.
diff --git a/FS/FS/cust_bill_batch.pm b/FS/FS/cust_bill_batch.pm
new file mode 100644 (file)
index 0000000..4569e6b
--- /dev/null
@@ -0,0 +1,70 @@
+package FS::cust_bill_batch;
+
+use strict;
+use vars qw( @ISA $me $DEBUG );
+use FS::Record qw( qsearch qsearchs dbh );
+
+@ISA = qw( FS::option_Common );
+$me = '[ FS::cust_bill_batch ]';
+$DEBUG=0;
+
+sub table { 'cust_bill_batch' }
+
+=head1 NAME
+
+FS::cust_bill_batch - Object methods for cust_bill_batch records
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_batch object represents the inclusion of an invoice in a 
+processing batch.  FS::cust_bill_batch inherits from FS::option_Common.  The 
+following fields are currently supported:
+
+=over 4
+
+=item billbatchnum - primary key
+
+=item invnum - invoice number (see C<FS::cust_bill>)
+
+=item batchnum - batchn number (see C<FS::bill_batch>)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item bill_batch
+
+Returns the C<FS::bill_batch> object.
+
+=cut
+
+sub bill_batch { 
+  my $self = shift;
+  FS::bill_batch->by_key($self->batchnum);
+}
+
+=item cust_bill
+
+Returns the C<FS::cust_bill> object.
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  FS::cust_bill->by_key($self->invnum);
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_batch_option.pm b/FS/FS/cust_bill_batch_option.pm
new file mode 100644 (file)
index 0000000..9bba830
--- /dev/null
@@ -0,0 +1,126 @@
+package FS::cust_bill_batch_option;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::cust_bill_batch_option - Object methods for cust_bill_batch_option records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_batch_option;
+
+  $record = new FS::cust_bill_batch_option \%hash;
+  $record = new FS::cust_bill_batch_option { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_batch_option object represents an option key and value for
+an invoice batch entry.  FS::cust_bill_batch_option inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item optionnum - primary key
+
+=item billbatchnum - 
+
+=item optionname - 
+
+=item optionvalue - 
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new option.  To add the option to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_bill_batch_option'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=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.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid option.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('optionnum')
+    || $self->ut_foreign_key('billbatchnum', 'cust_bill_batch', 'billbatchnum')
+    || $self->ut_text('optionname')
+    || $self->ut_textn('optionvalue')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 5ce49c3..ef105b1 100644 (file)
@@ -357,6 +357,8 @@ $tools_menu{'Quick payment entry'} =  [ $fsurl.'misc/batch-cust_pay.html', 'Ente
 $tools_menu{'Process payment batches'} = [ $fsurl.'search/pay_batch.cgi?magic=_date;open=1;intransit=1', 'Process credit card and electronic check batches' ]
   if ( $conf->exists('batch-enable') || $conf->config('batch-enable_payby') )
      && $curuser->access_right('Process batches');
+$tools_menu{'Process invoice batches'} = [ $fsurl.'search/bill_batch.cgi' ] 
+  if ( $conf->exists('invoice_print_pdf') );
 $tools_menu{'Job Queue'} =  [ $fsurl.'search/queue.html', 'View pending job queue' ]
   if $curuser->access_right('Job queue');
 $tools_menu{'Ticketing'} = [ \%tools_ticketing, 'Ticketing tools' ]
index 194fc74..20eb9bf 100644 (file)
@@ -1,3 +1,47 @@
+<%doc>
+Example:
+In misc/something.html:
+
+  <FORM NAME="MyForm">
+  <INPUT TYPE="hidden" NAME="recordnum" VALUE="42">
+  <INPUT TYPE="hidden" NAME="what_to_do" VALUE="delete">
+  <% include( '/elements/progress-init.html',
+             'MyForm', 
+             [ 'recordnum', 'what_to_do' ],
+             $p.'misc/process_something.html',
+             { url => $p.'where_to_go_next.html' },
+         #or { message => 'Finished!' },
+         );
+  </FORM>
+  <SCRIPT TYPE="text/javascript>process();</SCRIPT>
+
+In misc/process_something.html:
+
+<%init>
+my $server = FS::UI::Web::JSRPC->new('FS::something::process_whatever', $cgi);
+</%init>
+<% $server->process %>
+
+In FS/something.pm:
+
+sub process_whatever { #class method
+  my $job = shift;
+  my $param = thaw(base64_decode(shift));
+  # param = { 'recordnum' => 42, 'what_to_do' => delete }
+  # make use of this as you like
+  do_phase1;
+  $job->update_statustext(20);
+  do_phase2;
+  $job->update_statustext(40);
+  do_phase3;
+  $job->update_statustext(60);
+  # etc.
+  return 'BLAH BLAH NOBODY WILL EVER SEE THIS RETURN VALUE';
+}
+
+I am not responsible for errors in the above documentation.
+
+</%doc>
 <% include('/elements/xmlhttp.html',
               'method' => 'POST',
               'url'    => $action,
diff --git a/httemplate/misc/process/bill_batch-print.html b/httemplate/misc/process/bill_batch-print.html
new file mode 100644 (file)
index 0000000..54d639e
--- /dev/null
@@ -0,0 +1,5 @@
+% die "access denied"
+%   unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+% my $server = FS::UI::Web::JSRPC->new('FS::bill_batch::process_print_pdf', $cgi);
+<% $server->process %>
+<%init></%init>
diff --git a/httemplate/search/bill_batch.cgi b/httemplate/search/bill_batch.cgi
new file mode 100755 (executable)
index 0000000..e5abc89
--- /dev/null
@@ -0,0 +1,65 @@
+<% include( 'elements/search.html',
+                 'title'         => 'Invoice Batches',
+                'name_singular' => 'batch',
+                'query'         => { 'table'     => 'bill_batch',
+                                     'hashref'   => $hashref,
+                                     'extra_sql' => $extra_sql.
+                                        'ORDER BY batchnum DESC',
+                                   },
+                'count_query'   => "$count_query $extra_sql",
+                'header'        => [ 'Batch',
+                                     'Item Count',
+                                     'Status',
+                                      '',
+                                    ],
+                'align'         => 'rrcc',
+                'fields'        => [ 'batchnum',
+                                      sub {
+                                        my $st = "SELECT COUNT(*) from cust_bill_batch WHERE batchnum=" . shift->batchnum;
+                                        my $sth = dbh->prepare($st)
+                                          or die dbh->errstr. "doing $st";
+                                        $sth->execute
+                                         or die "Error executing \"$st\": ". $sth->errstr;
+                                        $sth->fetchrow_arrayref->[0];
+                                     },
+                                     sub {
+                                       $statusmap{shift->status};
+                                     },
+                                      sub { shift->status eq 'O' ? 
+                                            'Download and close' : 'Download' 
+                                      },
+                                   ],
+                'links'         => [
+                                      $link,
+                                      $link,
+                                      $link,
+                                      $dlink,
+                                    ],
+                'style'         => [
+                                     '',
+                                      '',
+                                      '',
+                                     sub { shift->status eq 'O' ? "b" : '' },
+                                   ],
+                 'really_disable_download' => 1,
+      )
+
+%>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+my %statusmap = ('O'=>'Open', 'R'=>'Closed');
+my $hashref = {};
+my $count_query = 'SELECT COUNT(*) FROM bill_batch';
+
+my $extra_sql = ''; # may add something here later
+my $link = [ "${p}view/bill_batch.cgi?batchnum=", 'batchnum' ];
+my $dlink = sub {
+  [ "${p}view/bill_batch.cgi?magic=print;".
+      (shift->status eq 'O' ? 'close=1;' : '').
+      'batchnum=',
+    'batchnum'] 
+};
+</%init>
diff --git a/httemplate/view/bill_batch.cgi b/httemplate/view/bill_batch.cgi
new file mode 100644 (file)
index 0000000..7275756
--- /dev/null
@@ -0,0 +1,102 @@
+% if($magic eq 'print') {
+<% include('/elements/header.html', "Download Batch") %>
+<FORM NAME="OneTrueForm">
+<INPUT TYPE="hidden" NAME="batchnum" VALUE="<% $batchnum %>">
+% $cgi->delete('magic');
+<% include('/elements/progress-init.html',
+              'OneTrueForm',
+              [ 'batchnum' ],
+              $p.'misc/process/bill_batch-print.html',
+              {'url' => $cgi->self_url . ';magic=download'},
+              '',
+) %></FORM>
+<SCRIPT TYPE="text/javascript">process();</SCRIPT>
+<% include('/elements/footer.html') %>
+% }
+% 
+% elsif($magic eq 'download') {
+%   $m->clear_buffer;
+%   $r->content_type('application/pdf');
+%   $r->headers_out->add('Content-Disposition' => 'attachment;filename="invoice_batch_'.$batchnum.'.pdf"');
+<% $batch->pdf %>
+%   $batch->pdf('');
+%   my $error = $batch->replace;
+%   warn "error deleting cached PDF: '$error'\n" if $error;
+% }
+% else {
+<% include('/search/elements/search.html', 
+              'title'     => $close ?
+                              "Batch $batchnum closed." :
+                              "Invoice Batch $batchnum",
+              'name'      => 'invoices',
+              'query'     => { 'table'   => 'cust_bill_batch',
+                               'select'  => join(', ',
+                                          'cust_bill.*',
+                                          FS::UI::Web::cust_sql_fields(),
+                                          'cust_main.custnum AS cust_main_custnum',
+                                ),
+                               'hashref' => { },
+                               'addl_from' => 
+                                 'LEFT JOIN cust_bill USING ( invnum ) '.
+                                 'LEFT JOIN cust_main USING ( custnum )',
+                               'extra_sql' => '',
+                                 " WHERE batchnum = $batchnum",
+                             },
+              'count_query' => "SELECT COUNT(*) FROM cust_bill_batch WHERE batchnum = $batchnum",
+              'html_init' => $html_init,
+              'header'    => [ 'Invoice #',
+                               'Amount',
+                               'Date',
+                               'Customer',
+                             ],
+              'fields'    => [ sub { shift->cust_bill->display_invnum },
+                               sub { sprintf($money_char.'%.2f', 
+                                      shift->cust_bill->charged ) },
+                               sub { time2str('%b %d %Y', 
+                                      shift->cust_bill->_date ) },
+                               sub { shift->cust_bill->cust_main->name },
+                             ],
+              'align'     => 'rrll',
+              'links'     => [ ($link) x 3, $clink,
+                             ],
+              'really_disable_download' => 1,
+) %>
+% }
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('View invoices');
+
+use Data::Dumper;
+warn Dumper($cgi->Vars);
+
+my $conf = new FS::Conf;
+my $batch;
+my $batchnum = $cgi->param('batchnum');
+
+$batch = FS::bill_batch->by_key($batchnum);
+die "Batch '$batchnum' not found!\n" if !$batch;
+
+my $magic = $cgi->param('magic');
+my $html_init = '';
+
+my $close = $cgi->param('close');
+$batch->close if $close;
+
+if(!$magic) {
+  $cgi->param('magic' => 'print');
+  $cgi->delete('close');
+  $html_init  = '<A HREF="'.$cgi->self_url.'">Download this batch</A><BR>';
+  if($batch->status eq 'O') {
+    $cgi->param('close' => 1);
+    $cgi->delete('magic');
+    $html_init .= '<A HREF="'.$cgi->self_url.'">Close this batch</A><BR>';
+  }
+  $html_init .= '<BR>';
+}
+
+my $link = [ "$p/view/cust_bill.cgi?", 'invnum' ];
+my $clink = [ "$p/view/cust_main.cgi?", 'custnum' ];
+my $money_char = $conf->config('money_char') || '$';
+
+</%init>