From: mark Date: Tue, 8 Jun 2010 22:25:01 +0000 (+0000) Subject: RT#947: batch download of invoice PDFs X-Git-Tag: root_of_svc_elec_features~179 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=b7dbecfb82aea90a4289089927d0b17436b2ed5a RT#947: batch download of invoice PDFs --- diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 89a36af43..9046b261b 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -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', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 150a6c081..84c9f7fa9 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -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 ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 859224fb0..01512f9f6 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -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 index 000000000..136db0d9e --- /dev/null +++ b/FS/FS/bill_batch.pm @@ -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, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 127053013..a1dab4ac0 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -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). 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 index 000000000..4569e6bc8 --- /dev/null +++ b/FS/FS/cust_bill_batch.pm @@ -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) + +=item batchnum - batchn number (see C) + +=back + +=head1 METHODS + +=over 4 + +=item bill_batch + +Returns the C object. + +=cut + +sub bill_batch { + my $self = shift; + FS::bill_batch->by_key($self->batchnum); +} + +=item cust_bill + +Returns the C object. + +=cut + +sub cust_bill { + my $self = shift; + FS::cust_bill->by_key($self->invnum); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, 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 index 000000000..9bba830fd --- /dev/null +++ b/FS/FS/cust_bill_batch_option.pm @@ -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 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, schema.html from the base documentation. + +=cut + +1; + diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 5ce49c3a7..ef105b1d6 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -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' ] diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html index 194fc7480..20eb9bfa6 100644 --- a/httemplate/elements/progress-init.html +++ b/httemplate/elements/progress-init.html @@ -1,3 +1,47 @@ +<%doc> +Example: +In misc/something.html: + +
+ + + <% 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!' }, + ); +
+ +<% 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 = 'Download this batch
'; + if($batch->status eq 'O') { + $cgi->param('close' => 1); + $cgi->delete('magic'); + $html_init .= 'Close this batch
'; + } + $html_init .= '
'; +} + +my $link = [ "$p/view/cust_bill.cgi?", 'invnum' ]; +my $clink = [ "$p/view/cust_main.cgi?", 'custnum' ]; +my $money_char = $conf->config('money_char') || '$'; + +