summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormark <mark>2010-06-08 22:25:01 +0000
committermark <mark>2010-06-08 22:25:01 +0000
commitb7dbecfb82aea90a4289089927d0b17436b2ed5a (patch)
tree8f22377f952d54aba7efcde11a2cd3f2e8a8cf40
parent3d0320979331ac4b68fc07fd4203d74c1b05aff4 (diff)
RT#947: batch download of invoice PDFs
-rw-r--r--FS/FS/Conf.pm6
-rw-r--r--FS/FS/Mason.pm2
-rw-r--r--FS/FS/Schema.pm35
-rw-r--r--FS/FS/bill_batch.pm151
-rw-r--r--FS/FS/cust_bill.pm27
-rw-r--r--FS/FS/cust_bill_batch.pm70
-rw-r--r--FS/FS/cust_bill_batch_option.pm126
-rw-r--r--httemplate/elements/menu.html2
-rw-r--r--httemplate/elements/progress-init.html44
-rw-r--r--httemplate/misc/process/bill_batch-print.html5
-rwxr-xr-xhttemplate/search/bill_batch.cgi65
-rw-r--r--httemplate/view/bill_batch.cgi102
12 files changed, 634 insertions, 1 deletions
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 89a36af..9046b26 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 150a6c0..84c9f7f 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 859224f..01512f9 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 0000000..136db0d
--- /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<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 1270530..a1dab4a 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<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
index 0000000..4569e6b
--- /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<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
index 0000000..9bba830
--- /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<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;
+
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 5ce49c3..ef105b1 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 194fc74..20eb9bf 100644
--- a/httemplate/elements/progress-init.html
+++ b/httemplate/elements/progress-init.html
@@ -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
index 0000000..54d639e
--- /dev/null
+++ b/httemplate/misc/process/bill_batch-print.html
@@ -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
index 0000000..e5abc89
--- /dev/null
+++ b/httemplate/search/bill_batch.cgi
@@ -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
index 0000000..7275756
--- /dev/null
+++ b/httemplate/view/bill_batch.cgi
@@ -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>