use Mail::Internet 1.44;
use Mail::Header;
use Text::Template;
+use File::Temp 0.14;
+use String::ShellQuote;
use FS::UID qw( datasrc );
use FS::Record qw( qsearch qsearchs );
use FS::cust_main;
$balance;
}
-=item send
+=item send [ TEMPLATENAME [ , AGENTNUM ] ]
Sends this invoice to the destinations configured for this customer: send
emails or print. See L<FS::cust_main_invoice>.
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+AGENTNUM, if specified, means that this invoice will only be sent for customers
+of the specified agent.
+
=cut
sub send {
- my($self,$template) = @_;
+ my $self = shift;
+ my $template = scalar(@_) ? shift : '';
+ return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
+
my @print_text = $self->print_text('', $template);
my @invoicing_list = $self->cust_main->invoicing_list;
$!=0;
$message->smtpsend( Host => $smtpmachine )
or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
- or return "(customer # ". $self->custnum. ") can't send invoice email".
- " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
- " via server $smtpmachine with SMTP: $!";
+ or die "(customer # ". $self->custnum. ") can't send invoice email".
+ " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
+ " via server $smtpmachine with SMTP: $!\n";
}
if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
open(LPR, "|$lpr")
- or return "Can't open pipe to $lpr: $!";
+ or die "Can't open pipe to $lpr: $!\n";
print LPR @print_text;
close LPR
- or return $! ? "Error closing $lpr: $!"
- : "Exit status $? from $lpr";
+ or die $! ? "Error closing $lpr: $!\n"
+ : "Exit status $? from $lpr\n";
}
'';
} );
my $error = $cust_pay->insert;
if ( $error ) {
- # gah, even with transactions.
- my $e = 'WARNING: Card/ACH debited but database not updated - '.
- 'error applying payment, invnum #' . $self->invnum.
- " ($processor): $error";
- warn $e;
- return $e;
- } else {
- return '';
+ $cust_pay->invnum(''); #try again with no specific invnum
+ my $error2 = $cust_pay->insert;
+ if ( $error2 ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH debited but database not updated - '.
+ "error inserting payment ($processor): $error2".
+ ' (previously tried insert with invnum #' . $self->invnum.
+ ": $error )";
+ warn $e;
+ return $e;
+ }
}
+ return ''; #no error
+
#} elsif ( $options{'report_badcard'} ) {
} else {
if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
&& grep { $_ ne 'POST' } $cust_main->invoicing_list
- && ! grep { $_ eq $transaction->error_message }
+ && ! grep { $transaction->error_message =~ /$_/ }
$conf->config('emaildecline-exclude')
) {
my @templ = $conf->config('declinetemplate');
'';
}
+sub _agent_template {
+ my $self = shift;
+
+ my $cust_bill_event = qsearchs( 'part_bill_event',
+ {
+ 'payby' => $self->cust_main->payby,
+ 'plan' => 'send_agent',
+ 'eventcode' => { 'op' => 'LIKE',
+ 'value' => '_%, '. $self->cust_main->agentnum. ');' },
+ },
+ '',
+ 'ORDER BY seconds LIMIT 1'
+ );
+
+ return '' unless $cust_bill_event;
+
+ if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
+ return $1;
+ } else {
+ warn "can't parse eventcode for agent-specific invoice template";
+ return '';
+ }
+
+}
+
=item print_text [ TIME [ , TEMPLATE ] ]
Returns an text invoice, as a list of lines.
sprintf("%10.2f", $balance_due ) ];
#create the template
+ $template ||= $self->_agent_template;
my $templatefile = 'invoice_template';
- $templatefile .= "_$template" if $template;
+ $templatefile .= "_$template" if length($template);
my @invoice_template = $conf->config($templatefile)
- or die "cannot load config file $templatefile";
+ or die "cannot load config file $templatefile";
$invoice_lines = 0;
my $wasfunc = 0;
foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
}
-=item print_ps [ TIME [ , TEMPLATE ] ]
+=item print_latex [ TIME [ , TEMPLATE ] ]
-Returns an postscript invoice, as a scalar.
+Internal method - returns a filename of a filled-in LaTeX template for this
+invoice (Note: add ".tex" to get the actual filename).
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
TIME an optional value used to control the printing of overdue messages. The
default is now. It isn't the date of the invoice; that's the `_date' field.
=cut
#still some false laziness w/print_text
-sub print_ps {
+sub print_latex {
my( $self, $today, $template ) = @_;
$today ||= time;
@buf = ();
#create the template
+ $template ||= $self->_agent_template;
my $templatefile = 'invoice_latex';
- $templatefile .= "_$template" if $template;
+ my $suffix = length($template) ? "_$template" : '';
+ $templatefile .= $suffix;
my @invoice_template = $conf->config($templatefile)
or die "cannot load config file $templatefile";
my %invoice_data = (
'invnum' => $self->invnum,
'date' => time2str('%b %o, %Y', $self->_date),
- 'agent' => $cust_main->agent->agent,
- 'payname' => $cust_main->payname,
- 'company' => $cust_main->company,
- 'address1' => $cust_main->address1,
- 'address2' => $cust_main->address2,
- 'city' => $cust_main->city,
- 'state' => $cust_main->state,
- 'zip' => $cust_main->zip,
- 'country' => $cust_main->country,
+ 'agent' => _latex_escape($cust_main->agent->agent),
+ 'payname' => _latex_escape($cust_main->payname),
+ 'company' => _latex_escape($cust_main->company),
+ 'address1' => _latex_escape($cust_main->address1),
+ 'address2' => _latex_escape($cust_main->address2),
+ 'city' => _latex_escape($cust_main->city),
+ 'state' => _latex_escape($cust_main->state),
+ 'zip' => _latex_escape($cust_main->zip),
+ 'country' => _latex_escape($cust_main->country),
'footer' => join("\n", $conf->config('invoice_latexfooter') ),
+ 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
'quantity' => 1,
'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
- 'notes' => join("\n", $conf->config('invoice_latexnotes') ),
+ #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
);
- $invoice_data{'footer'} =~ s/\n+$//;
- $invoice_data{'notes'} =~ s/\n+$//;
-
my $countrydefault = $conf->config('countrydefault') || 'US';
$invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
+ #do variable substitutions in notes
+ $invoice_data{'notes'} =
+ join("\n",
+ map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+ $conf->config_orbase('invoice_latexnotes', $suffix)
+ );
+
+ $invoice_data{'footer'} =~ s/\n+$//;
+ $invoice_data{'smallfooter'} =~ s/\n+$//;
+ $invoice_data{'notes'} =~ s/\n+$//;
+
$invoice_data{'po_line'} =
( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
- ? "Purchase Order #". $cust_main->payinfo
+ ? _latex_escape("Purchase Order #". $cust_main->payinfo)
: '~';
my @line_item = ();
foreach my $line_item ( $self->_items ) {
#foreach my $line_item ( $self->_items_pkg ) {
$invoice_data{'ref'} = $line_item->{'pkgnum'};
- $invoice_data{'description'} = $line_item->{'description'};
+ $invoice_data{'description'} = _latex_escape($line_item->{'description'});
if ( exists $line_item->{'ext_description'} ) {
$invoice_data{'description'} .=
"\\tabularnewline\n~~".
- join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
+ join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
}
$invoice_data{'amount'} = $line_item->{'amount'};
$invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
my $taxtotal = 0;
foreach my $tax ( $self->_items_tax ) {
- $invoice_data{'total_item'} = $tax->{'description'};
+ $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
$taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
push @total_fill,
map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
# credits
foreach my $credit ( $self->_items_credits ) {
- $invoice_data{'total_item'} = $credit->{'description'};
+ $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
#$credittotal
$invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
push @total_fill,
# payments
foreach my $payment ( $self->_items_payments ) {
- $invoice_data{'total_item'} = $payment->{'description'};
+ $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
#$paymenttotal
$invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
push @total_fill,
$var;
}
- my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
- my $unique = int(rand(2**31)); #UGH... use File::Temp or something
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
+ DIR => $dir,
+ SUFFIX => '.tex',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ print $fh join("\n", @filled_in ), "\n";
+ close $fh;
+
+ $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
+ return $1;
+
+}
+
+=item print_ps [ TIME [ , TEMPLATE ] ]
+Returns an postscript invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages. The
+default is now. It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_ps {
+ my $self = shift;
+
+ my $file = $self->print_latex(@_);
+
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
chdir($dir);
- my $file = $self->invnum. ".$unique";
- open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
- print TEX join("\n", @filled_in ), "\n";
- close TEX;
+ my $sfile = shell_quote $file;
+
+ system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+ or die "pslatex $file.tex failed: $!";
+ system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+ or die "pslatex $file.tex failed: $!";
- #error checking!!
- system('pslatex', "$file.tex");
- system('pslatex', "$file.tex");
- #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
- system('dvips', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
+ system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
+ or die "dvips failed: $!";
- open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
+ open(POSTSCRIPT, "<$file.ps")
+ or die "can't open $file.ps: $! (error in LaTeX template?)\n";
- #rm $file.dvi $file.log $file.aux
- #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
- unlink("$file.dvi", "$file.log", "$file.aux");
+ unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
my $ps = '';
while (<POSTSCRIPT>) {
}
-# quick subroutine for print_ps
+=item print_pdf [ TIME [ , TEMPLATE ] ]
+
+Returns an PDF invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages. The
+default is now. It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_pdf {
+ my $self = shift;
+
+ my $file = $self->print_latex(@_);
+
+ my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+ chdir($dir);
+
+ #system('pdflatex', "$file.tex");
+ #system('pdflatex', "$file.tex");
+ #! LaTeX Error: Unknown graphics extension: .eps.
+
+ my $sfile = shell_quote $file;
+
+ system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+ or die "pslatex $file.tex failed: $!";
+ system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+ or die "pslatex $file.tex failed: $!";
+
+ #system('dvipdf', "$file.dvi", "$file.pdf" );
+ system(
+ "dvips -q -t letter -f $sfile.dvi ".
+ "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
+ " -c save pop -"
+ ) == 0
+ or die "dvips | gs failed: $!";
+
+ open(PDF, "<$file.pdf")
+ or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
+
+ unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
+
+ my $pdf = '';
+ while (<PDF>) {
+ $pdf .= $_;
+ }
+
+ close PDF;
+
+ return $pdf;
+
+}
+
+# quick subroutine for print_latex
#
# There are ten characters that LaTeX treats as special characters, which
# means that they do not simply typeset themselves:
sub balance_due_msg {
my $self = shift;
my $msg = 'Balance Due';
+ return $msg unless $conf->exists('invoice_default_terms');
if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
$msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
} elsif ( $conf->config('invoice_default_terms') ) {
my @b = ();
foreach ( @pr_cust_bill ) {
push @b, {
- 'description' => 'Previous Balance, Invoice \#'. $_->invnum.
+ 'description' => 'Previous Balance, Invoice #'. $_->invnum.
' ('. time2str('%x',$_->_date). ')',
#'pkgpart' => 'N/A',
'pkgnum' => 'N/A',
my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
my $pkg = $part_pkg->pkg;
+ my %labels;
+ #tie %labels, 'Tie::IxHash';
+ push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
+ my @ext_description;
+ foreach my $label ( keys %labels ) {
+ my @values = @{ $labels{$label} };
+ my $num = scalar(@values);
+ if ( $num > 5 ) {
+ push @ext_description, "$label ($num)";
+ } else {
+ push @ext_description, map { "$label: $_" } @values;
+ }
+ }
+
if ( $cust_bill_pkg->setup != 0 ) {
my $description = $pkg;
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
- my @d = ();
- @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
+ my @d = @ext_description;
+ push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
push @b, {
'description' => $description,
#'pkgpart' => $part_pkg->pkgpart,
'pkgnum' => $cust_pkg->pkgnum,
'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
- 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
- $cust_pkg->labels ),
- @d,
- ],
+ 'ext_description' => \@d,
};
}
#'pkgpart' => $part_pkg->pkgpart,
'pkgnum' => $cust_pkg->pkgnum,
'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
- 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
- $cust_pkg->labels ),
+ 'ext_description' => [ @ext_description,
$cust_bill_pkg->details,
],
};
#'description' => 'Credit ref\#'. $_->crednum.
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
- 'description' => 'Credit applied'.
+ 'description' => 'Credit applied '.
time2str("%x",$_->cust_credit->_date). $reason,
'amount' => sprintf("%10.2f",$_->amount),
};
print_text formatting (and some logic :/) is in source, but needs to be
slurped in from a file. Also number of lines ($=).
-missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
-or something similar so the look can be completely customized?)
-
=head1 SEE ALSO
L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,