summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2012-07-03 18:51:51 -0700
committerIvan Kohler <ivan@freeside.biz>2012-07-03 18:51:51 -0700
commit81978af92ecdaaefeff5156d9ab3b4f99586df1c (patch)
tree19a4e6bbcc6648c506c48e304418001294ed7485
parent7abce2207dbee012fd442940dc42070f45ef8a16 (diff)
quotations, RT#16996
-rw-r--r--FS/FS/AccessRight.pm1
-rw-r--r--FS/FS/Conf.pm26
-rw-r--r--FS/FS/Schema.pm46
-rw-r--r--FS/FS/Template_Mixin.pm2525
-rw-r--r--FS/FS/Upgrade.pm8
-rw-r--r--FS/FS/access_right.pm1
-rw-r--r--FS/FS/cust_bill.pm2475
-rw-r--r--FS/FS/cust_main.pm2
-rw-r--r--FS/FS/prospect_main.pm17
-rw-r--r--FS/FS/quotation.pm20
-rw-r--r--FS/MANIFEST8
-rw-r--r--conf/quotation_html266
-rw-r--r--conf/quotation_latex362
-rw-r--r--conf/quotation_latexnotes8
-rw-r--r--httemplate/edit/process/quotation.html2
-rw-r--r--httemplate/edit/quotation.html15
-rw-r--r--httemplate/elements/tr-fixed-cust_main.html15
-rw-r--r--httemplate/elements/tr-fixed-date.html13
-rw-r--r--httemplate/elements/tr-fixed-prospect_main.html15
-rw-r--r--httemplate/view/prospect_main.html31
-rwxr-xr-xhttemplate/view/quotation.html25
21 files changed, 3390 insertions, 2491 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index eb9974a..4de2948 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -98,6 +98,7 @@ tie my %rights, 'Tie::IxHash',
#'New contact',
#'View customer contacts',
#'List contacts',
+ 'Generate quotation',
],
###
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 8c9d56f..7e64130 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -1197,7 +1197,15 @@ sub reason_type_options {
{
'key' => 'invoice_html',
'section' => 'invoicing',
- 'description' => 'Optional HTML template for invoices. See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:Administration#HTML_invoice_templates">billing documentation</a> for details.',
+ 'description' => 'HTML template for invoices. See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:Administration#HTML_invoice_templates">billing documentation</a> for details.',
+
+ 'type' => 'textarea',
+ },
+
+ {
+ 'key' => 'quotation_html',
+ 'section' => '',
+ 'description' => 'HTML template for quotations.',
'type' => 'textarea',
},
@@ -1245,6 +1253,13 @@ sub reason_type_options {
},
{
+ 'key' => 'quotation_latex',
+ 'section' => '',
+ 'description' => 'LaTeX template for typeset PostScript quotations.',
+ 'type' => 'textarea',
+ },
+
+ {
'key' => 'invoice_latextopmargin',
'section' => 'invoicing',
'description' => 'Optional LaTeX invoice topmargin setting. Include units.',
@@ -1303,6 +1318,15 @@ and customer address. Include units.',
},
{
+ 'key' => 'quotation_latexnotes',
+ 'section' => '',
+ 'description' => 'Notes section for LaTeX typeset PostScript quotations.',
+ 'type' => 'textarea',
+ 'per_agent' => 1,
+ 'per_locale' => 1,
+ },
+
+ {
'key' => 'invoice_latexfooter',
'section' => 'invoicing',
'description' => 'Footer for LaTeX typeset PostScript invoices.',
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index a90c73a..797b705 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1054,8 +1054,50 @@ sub tables_hashref {
'index' => [ [ 'company' ], [ 'agentnum' ], [ 'disabled' ] ],
},
- #eventually use for billing & ship from cust_main too
- #for now, just cust_pkg locations
+ 'quotation' => {
+ 'columns' => [
+ #regular fields
+ 'quotationnum', 'serial', '', '', '', '',
+ 'prospectnum', 'int', 'NULL', '', '', '',
+ 'custnum', 'int', 'NULL', '', '', '',
+ '_date', @date_type, '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
+ 'usernum', 'int', 'NULL', '', '', '',
+ #'total', @money_type, '', '',
+ #'quotation_term', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'quotationnum',
+ 'unique' => [],
+ 'index' => [ [ 'prospectnum' ], ['custnum'], ],
+ },
+
+ 'quotation_pkg' => {
+ 'columns' => [
+ 'quotationpkgnum', 'serial', '', '', '', '',
+ 'pkgpart', 'int', '', '', '', '',
+ 'locationnum', 'int', 'NULL', '', '', '',
+ 'start_date', @date_type, '', '',
+ 'contract_end', @date_type, '', '',
+ 'quantity', 'int', 'NULL', '', '', '',
+ 'waive_setup', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'quotationpkgnum',
+ 'unique' => [],
+ 'index' => [ ['pkgpart'], ],
+ },
+
+ 'quotation_pkg_discount' => {
+ 'columns' => [
+ 'quotationpkgdiscountnum', 'serial', '', '', '', '',
+ 'quotationpkgnum', 'int', '', '', '', '',
+ 'discountnum', 'int', '', '', '', '',
+ #'end_date', @date_type, '', '',
+ ],
+ 'primary_key' => 'quotationpkgdiscountnum',
+ 'unique' => [],
+ 'index' => [ [ 'quotationpkgnum' ], ], #[ 'discountnum' ] ],
+ },
+
'cust_location' => { #'location' now that its prospects too, but...
'columns' => [
'locationnum', 'serial', '', '', '', '',
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
new file mode 100644
index 0000000..19b452e
--- /dev/null
+++ b/FS/FS/Template_Mixin.pm
@@ -0,0 +1,2525 @@
+package FS::Template_Mixin;
+
+use strict;
+use vars qw( $DEBUG $me
+ $money_char $date_format $rdate_format $date_format_long );
+ # but NOT $conf
+use vars qw( $invoice_lines @buf ); #yuck
+use List::Util qw(sum);
+use Date::Format;
+use Date::Language;
+use Text::Template 1.20;
+use File::Temp 0.14;
+use HTML::Entities;
+use Locale::Country;
+use FS::UID;
+use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( generate_ps generate_pdf );
+use FS::pkg_category;
+use FS::pkg_class;
+use FS::L10N;
+
+$DEBUG = 0;
+$me = '[FS::Template_Mixin]';
+FS::UID->install_callback( sub {
+ my $conf = new FS::Conf; #global
+ $money_char = $conf->config('money_char') || '$';
+ $date_format = $conf->config('date_format') || '%x'; #/YY
+ $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
+ $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
+} );
+
+=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+
+Returns an text invoice, as a list of lines.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is 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.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_text {
+ my $self = shift;
+ my( $today, $template, %opt );
+ if ( ref($_[0]) ) {
+ %opt = %{ shift() };
+ $today = delete($opt{'time'}) || '';
+ $template = delete($opt{template}) || '';
+ } else {
+ ( $today, $template, %opt ) = @_;
+ }
+
+ my %params = ( 'format' => 'template' );
+ $params{'time'} = $today if $today;
+ $params{'template'} = $template if $template;
+ $params{$_} = $opt{$_}
+ foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+
+ $self->print_generic( %params );
+}
+
+=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+
+Internal method - returns a filename of a filled-in LaTeX template for this
+invoice (Note: add ".tex" to get the actual filename), and a filename of
+an associated logo (with the .eps extension included).
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is 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.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_latex {
+ my $self = shift;
+ my $conf = $self->conf;
+ my( $today, $template, %opt );
+ if ( ref($_[0]) ) {
+ %opt = %{ shift() };
+ $today = delete($opt{'time'}) || '';
+ $template = delete($opt{template}) || '';
+ } else {
+ ( $today, $template, %opt ) = @_;
+ }
+
+ my %params = ( 'format' => 'latex' );
+ $params{'time'} = $today if $today;
+ $params{'template'} = $template if $template;
+ $params{$_} = $opt{$_}
+ foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+
+ $template ||= $self->_agent_template
+ if $self->can('_agent_template');
+
+ my $pkey = $self->primary_key;
+ my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
+
+ my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+ my $lh = new File::Temp(
+ TEMPLATE => $tmp_template,
+ DIR => $dir,
+ SUFFIX => '.eps',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+
+ my $agentnum = $self->cust_main->agentnum;
+
+ if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
+ print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
+ or die "can't write temp file: $!\n";
+ } else {
+ print $lh $conf->config_binary('logo.eps', $agentnum)
+ or die "can't write temp file: $!\n";
+ }
+ close $lh;
+ $params{'logo_file'} = $lh->filename;
+
+ if( $conf->exists('invoice-barcode') && $self->can('invoice_barcode') ) {
+ my $png_file = $self->invoice_barcode($dir);
+ my $eps_file = $png_file;
+ $eps_file =~ s/\.png$/.eps/g;
+ $png_file =~ /(barcode.*png)/;
+ $png_file = $1;
+ $eps_file =~ /(barcode.*eps)/;
+ $eps_file = $1;
+
+ my $curr_dir = cwd();
+ chdir($dir);
+ # after painfuly long experimentation, it was determined that sam2p won't
+ # accept : and other chars in the path, no matter how hard I tried to
+ # escape them, hence the chdir (and chdir back, just to be safe)
+ system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
+ or die "sam2p failed: $!\n";
+ unlink($png_file);
+ chdir($curr_dir);
+
+ $params{'barcode_file'} = $eps_file;
+ }
+
+ my @filled_in = $self->print_generic( %params );
+
+ my $fh = new File::Temp( TEMPLATE => $tmp_template,
+ DIR => $dir,
+ SUFFIX => '.tex',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ binmode($fh, ':utf8'); # language support
+ print $fh join('', @filled_in );
+ close $fh;
+
+ $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
+ return ($1, $params{'logo_file'}, $params{'barcode_file'});
+
+}
+
+=item print_generic OPTION => VALUE ...
+
+Internal method - returns a filled-in template for this invoice as a scalar.
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+Non optional options include
+ format - latex, html, template
+
+Optional options include
+
+template - a value used as a suffix for a configuration template
+
+time - a 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.
+
+cid -
+
+unsquelch_cdr - overrides any per customer cdr squelching when true
+
+notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+locale - override customer's locale
+
+=cut
+
+#what's with all the sprintf('%10.2f')'s in here? will it cause any
+# (alignment in text invoice?) problems to change them all to '%.2f' ?
+# yes: fixed width/plain text printing will be borked
+sub print_generic {
+ my( $self, %params ) = @_;
+ my $conf = $self->conf;
+ my $today = $params{today} ? $params{today} : time;
+ warn "$me print_generic called on $self with suffix $params{template}\n"
+ if $DEBUG;
+
+ my $format = $params{format};
+ die "Unknown format: $format"
+ unless $format =~ /^(latex|html|template)$/;
+
+ my $cust_main = $self->cust_main || $self->prospect_main;
+ $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
+ unless $cust_main->payname
+ && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
+
+ my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
+ 'html' => [ '<%=', '%>' ],
+ 'template' => [ '{', '}' ],
+ );
+
+ warn "$me print_generic creating template\n"
+ if $DEBUG > 1;
+
+ #create the template
+ my $template = $params{template} ? $params{template} : $self->_agent_template;
+ my $templatefile = $self->template_conf. $format;
+ $templatefile .= "_$template"
+ if length($template) && $conf->exists($templatefile."_$template");
+ my @invoice_template = map "$_\n", $conf->config($templatefile)
+ or die "cannot load config data $templatefile";
+
+ my $old_latex = '';
+ if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
+ #change this to a die when the old code is removed
+ warn "old-style invoice template $templatefile; ".
+ "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
+ $old_latex = 'true';
+ @invoice_template = _translate_old_latex_format(@invoice_template);
+ }
+
+ warn "$me print_generic creating T:T object\n"
+ if $DEBUG > 1;
+
+ my $text_template = new Text::Template(
+ TYPE => 'ARRAY',
+ SOURCE => \@invoice_template,
+ DELIMITERS => $delimiters{$format},
+ );
+
+ warn "$me print_generic compiling T:T object\n"
+ if $DEBUG > 1;
+
+ $text_template->compile()
+ or die "Can't compile $templatefile: $Text::Template::ERROR\n";
+
+
+ # additional substitution could possibly cause breakage in existing templates
+ my %convert_maps = (
+ 'latex' => {
+ 'notes' => sub { map "$_", @_ },
+ 'footer' => sub { map "$_", @_ },
+ 'smallfooter' => sub { map "$_", @_ },
+ 'returnaddress' => sub { map "$_", @_ },
+ 'coupon' => sub { map "$_", @_ },
+ 'summary' => sub { map "$_", @_ },
+ },
+ 'html' => {
+ 'notes' =>
+ sub {
+ map {
+ s/%%(.*)$/<!-- $1 -->/g;
+ s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
+ s/\\begin\{enumerate\}/<ol>/g;
+ s/\\item / <li>/g;
+ s/\\end\{enumerate\}/<\/ol>/g;
+ s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
+ s/\\\\\*/<br>/g;
+ s/\\dollar ?/\$/g;
+ s/\\#/#/g;
+ s/~/&nbsp;/g;
+ $_;
+ } @_
+ },
+ 'footer' =>
+ sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
+ 'smallfooter' =>
+ sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
+ 'returnaddress' =>
+ sub {
+ map {
+ s/~/&nbsp;/g;
+ s/\\\\\*?\s*$/<BR>/;
+ s/\\hyphenation\{[\w\s\-]+}//;
+ s/\\([&])/$1/g;
+ $_;
+ } @_
+ },
+ 'coupon' => sub { "" },
+ 'summary' => sub { "" },
+ },
+ 'template' => {
+ 'notes' =>
+ sub {
+ map {
+ s/%%.*$//g;
+ s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
+ s/\\begin\{enumerate\}//g;
+ s/\\item / * /g;
+ s/\\end\{enumerate\}//g;
+ s/\\textbf\{(.*)\}/$1/g;
+ s/\\\\\*/ /;
+ s/\\dollar ?/\$/g;
+ $_;
+ } @_
+ },
+ 'footer' =>
+ sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
+ 'smallfooter' =>
+ sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
+ 'returnaddress' =>
+ sub {
+ map {
+ s/~/ /g;
+ s/\\\\\*?\s*$/\n/; # dubious
+ s/\\hyphenation\{[\w\s\-]+}//;
+ $_;
+ } @_
+ },
+ 'coupon' => sub { "" },
+ 'summary' => sub { "" },
+ },
+ );
+
+
+ # hashes for differing output formats
+ my %nbsps = ( 'latex' => '~',
+ 'html' => '', # '&nbps;' would be nice
+ 'template' => '', # not used
+ );
+ my $nbsp = $nbsps{$format};
+
+ my %escape_functions = ( 'latex' => \&_latex_escape,
+ 'html' => \&_html_escape_nbsp,#\&encode_entities,
+ 'template' => sub { shift },
+ );
+ my $escape_function = $escape_functions{$format};
+ my $escape_function_nonbsp = ($format eq 'html')
+ ? \&_html_escape : $escape_function;
+
+ my %date_formats = ( 'latex' => $date_format_long,
+ 'html' => $date_format_long,
+ 'template' => '%s',
+ );
+ $date_formats{'html'} =~ s/ /&nbsp;/g;
+
+ my $date_format = $date_formats{$format};
+
+ my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
+ },
+ 'html' => sub { return '<b>'. shift(). '</b>'
+ },
+ 'template' => sub { shift },
+ );
+ my $embolden_function = $embolden_functions{$format};
+
+ my %newline_tokens = ( 'latex' => '\\\\',
+ 'html' => '<br>',
+ 'template' => "\n",
+ );
+ my $newline_token = $newline_tokens{$format};
+
+ warn "$me generating template variables\n"
+ if $DEBUG > 1;
+
+ # generate template variables
+ my $returnaddress;
+ if (
+ defined( $conf->config_orbase( "invoice_${format}returnaddress",
+ $template
+ )
+ )
+ && length( $conf->config_orbase( "invoice_${format}returnaddress",
+ $template
+ )
+ )
+ ) {
+
+ $returnaddress = join("\n",
+ $conf->config_orbase("invoice_${format}returnaddress", $template)
+ );
+
+ } elsif ( grep /\S/,
+ $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
+
+ my $convert_map = $convert_maps{$format}{'returnaddress'};
+ $returnaddress =
+ join( "\n",
+ &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
+ $template
+ )
+ )
+ );
+ } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
+
+ my $convert_map = $convert_maps{$format}{'returnaddress'};
+ $returnaddress = join( "\n", &$convert_map(
+ map { s/( {2,})/'~' x length($1)/eg;
+ s/$/\\\\\*/;
+ $_
+ }
+ ( $conf->config('company_name', $cust_main->agentnum),
+ $conf->config('company_address', $cust_main->agentnum),
+ )
+ )
+ );
+
+ } else {
+
+ my $warning = "Couldn't find a return address; ".
+ "do you need to set the company_address configuration value?";
+ warn "$warning\n";
+ $returnaddress = $nbsp;
+ #$returnaddress = $warning;
+
+ }
+
+ warn "$me generating invoice data\n"
+ if $DEBUG > 1;
+
+ my $agentnum = $cust_main->agentnum;
+
+ my %invoice_data = (
+
+ #invoice from info
+ 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
+ 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
+ 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
+ 'returnaddress' => $returnaddress,
+ 'agent' => &$escape_function($cust_main->agent->agent),
+
+ #invoice/quotation info
+ 'invnum' => $self->invnum,
+ 'quotationnum' => $self->quotationnum,
+ 'date' => time2str($date_format, $self->_date),
+ 'today' => time2str($date_format_long, $today),
+ 'terms' => $self->terms,
+ 'template' => $template, #params{'template'},
+ 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
+ 'current_charges' => sprintf("%.2f", $self->charged),
+ 'duedate' => $self->due_date2str($rdate_format), #date_format?
+
+ #customer info
+ 'custnum' => $cust_main->display_custnum,
+ 'prospectnum' => $cust_main->prospectnum,
+ 'agent_custid' => &$escape_function($cust_main->agent_custid),
+ ( map { $_ => &$escape_function($cust_main->$_()) } qw(
+ payname company address1 address2 city state zip fax
+ )),
+
+ #global config
+ 'ship_enable' => $conf->exists('invoice-ship_address'),
+ 'unitprices' => $conf->exists('invoice-unitprice'),
+ 'smallernotes' => $conf->exists('invoice-smallernotes'),
+ 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
+ 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
+
+ #layout info -- would be fancy to calc some of this and bury the template
+ # here in the code
+ 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
+ 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
+ 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
+ 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
+ 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
+ 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
+ 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
+ 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
+ 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
+ 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+
+ # better hang on to conf_dir for a while (for old templates)
+ 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+
+ #these are only used when doing paged plaintext
+ 'page' => 1,
+ 'total_pages' => 1,
+
+ );
+
+ #localization
+ my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
+ $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
+ my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
+ # eval to avoid death for unimplemented languages
+ my $dh = eval { Date::Language->new($info{'name'}) } ||
+ Date::Language->new(); # fall back to English
+ # prototype here to silence warnings
+ $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
+ # eventually use this date handle everywhere in here, too
+
+ my $min_sdate = 999999999999;
+ my $max_edate = 0;
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+ $min_sdate = $cust_bill_pkg->sdate
+ if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
+ $max_edate = $cust_bill_pkg->edate
+ if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
+ }
+
+ $invoice_data{'bill_period'} = '';
+ $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
+ . " to " . time2str('%e %h', $max_edate)
+ if ($max_edate != 0 && $min_sdate != 999999999999);
+
+ $invoice_data{finance_section} = '';
+ if ( $conf->config('finance_pkgclass') ) {
+ my $pkg_class =
+ qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
+ $invoice_data{finance_section} = $pkg_class->categoryname;
+ }
+ $invoice_data{finance_amount} = '0.00';
+ $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
+
+ my $countrydefault = $conf->config('countrydefault') || 'US';
+ foreach ( qw( address1 address2 city state zip country fax) ){
+ my $method = 'ship_'.$_;
+ $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+ }
+ foreach ( qw( contact company ) ) { #compatibility
+ $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
+ }
+ $invoice_data{'ship_country'} = ''
+ if ( $invoice_data{'ship_country'} eq $countrydefault );
+
+ $invoice_data{'cid'} = $params{'cid'}
+ if $params{'cid'};
+
+ if ( $cust_main->country eq $countrydefault ) {
+ $invoice_data{'country'} = '';
+ } else {
+ $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
+ }
+
+ my @address = ();
+ $invoice_data{'address'} = \@address;
+ push @address,
+ $cust_main->payname.
+ ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
+ ? " (P.O. #". $cust_main->payinfo. ")"
+ : ''
+ )
+ ;
+ push @address, $cust_main->company
+ if $cust_main->company;
+ push @address, $cust_main->address1;
+ push @address, $cust_main->address2
+ if $cust_main->address2;
+ push @address,
+ $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
+ push @address, $invoice_data{'country'}
+ if $invoice_data{'country'};
+ push @address, ''
+ while (scalar(@address) < 5);
+
+ $invoice_data{'logo_file'} = $params{'logo_file'}
+ if $params{'logo_file'};
+ $invoice_data{'barcode_file'} = $params{'barcode_file'}
+ if $params{'barcode_file'};
+ $invoice_data{'barcode_img'} = $params{'barcode_img'}
+ if $params{'barcode_img'};
+ $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
+ if $params{'barcode_cid'};
+
+ my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+ #my $balance_due = $self->owed + $pr_total - $cr_total;
+ my $balance_due = $self->owed + $pr_total;
+
+ # the customer's current balance as shown on the invoice before this one
+ $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+
+ # the change in balance from that invoice to this one
+ $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
+
+ # the sum of amount owed on all previous invoices
+ $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+
+ # the sum of amount owed on all invoices
+ $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+
+ # info from customer's last invoice before this one, for some
+ # summary formats
+ $invoice_data{'last_bill'} = {};
+ my $last_bill = $pr_cust_bill[-1];
+ if ( $last_bill ) {
+ $invoice_data{'last_bill'} = {
+ '_date' => $last_bill->_date, #unformatted
+ # all we need for now
+ };
+ }
+
+ my $summarypage = '';
+ if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+ $summarypage = 1;
+ }
+ $invoice_data{'summarypage'} = $summarypage;
+
+ warn "$me substituting variables in notes, footer, smallfooter\n"
+ if $DEBUG > 1;
+
+ my $tc = $self->template_conf;
+ my @include = ( [ $tc, 'notes' ],
+ [ 'invoice_', 'footer' ],
+ [ 'invoice_', 'smallfooter', ],
+ );
+ push @include, [ $tc, 'coupon', ]
+ unless $params{'no_coupon'};
+
+ foreach my $i (@include) {
+
+ my($base, $include) = @$i;
+
+ my $inc_file = $conf->key_orbase("$base$format$include", $template);
+ my @inc_src;
+
+ if ( $conf->exists($inc_file, $agentnum)
+ && length( $conf->config($inc_file, $agentnum) ) ) {
+
+ @inc_src = $conf->config($inc_file, $agentnum);
+
+ } else {
+
+ $inc_file = $conf->key_orbase("${base}latex$include", $template);
+
+ my $convert_map = $convert_maps{$format}{$include};
+
+ @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+ s/--\@\]/$delimiters{$format}[1]/g;
+ $_;
+ }
+ &$convert_map( $conf->config($inc_file, $agentnum) );
+
+ }
+
+ my $inc_tt = new Text::Template (
+ TYPE => 'ARRAY',
+ SOURCE => [ map "$_\n", @inc_src ],
+ DELIMITERS => $delimiters{$format},
+ ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+ unless ( $inc_tt->compile() ) {
+ my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+ warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+ die $error;
+ }
+
+ $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+ $invoice_data{$include} =~ s/\n+$//
+ if ($format eq 'latex');
+ }
+
+ # let invoices use either of these as needed
+ $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
+ ? $cust_main->payinfo : '';
+ $invoice_data{'po_line'} =
+ ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+ ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
+ : $nbsp;
+
+ my %money_chars = ( 'latex' => '',
+ 'html' => $conf->config('money_char') || '$',
+ 'template' => '',
+ );
+ my $money_char = $money_chars{$format};
+
+ my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
+ 'html' => $conf->config('money_char') || '$',
+ 'template' => '',
+ );
+ my $other_money_char = $other_money_chars{$format};
+ $invoice_data{'dollar'} = $other_money_char;
+
+ my @detail_items = ();
+ my @total_items = ();
+ my @buf = ();
+ my @sections = ();
+
+ $invoice_data{'detail_items'} = \@detail_items;
+ $invoice_data{'total_items'} = \@total_items;
+ $invoice_data{'buf'} = \@buf;
+ $invoice_data{'sections'} = \@sections;
+
+ warn "$me generating sections\n"
+ if $DEBUG > 1;
+
+ my $previous_section = { 'description' => $self->mt('Previous Charges'),
+ 'subtotal' => $other_money_char.
+ sprintf('%.2f', $pr_total),
+ 'summarized' => '', #why? $summarypage ? 'Y' : '',
+ };
+ $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
+ join(' / ', map { $cust_main->balance_date_range(@$_) }
+ $self->_prior_month30s
+ )
+ if $conf->exists('invoice_include_aging');
+
+ my $taxtotal = 0;
+ my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
+ 'subtotal' => $taxtotal, # adjusted below
+ };
+ my $tax_weight = _pkg_category($tax_section->{description})
+ ? _pkg_category($tax_section->{description})->weight
+ : 0;
+ $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
+ $tax_section->{'sort_weight'} = $tax_weight;
+
+
+ my $adjusttotal = 0;
+ my $adjust_section = { 'description' =>
+ $self->mt('Credits, Payments, and Adjustments'),
+ 'subtotal' => 0, # adjusted below
+ };
+ my $adjust_weight = _pkg_category($adjust_section->{description})
+ ? _pkg_category($adjust_section->{description})->weight
+ : 0;
+ $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
+ $adjust_section->{'sort_weight'} = $adjust_weight;
+
+ my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
+ my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+ $invoice_data{'multisection'} = $multisection;
+ my $late_sections = [];
+ my $extra_sections = [];
+ my $extra_lines = ();
+
+ my $default_section = { 'description' => '',
+ 'subtotal' => '',
+ 'no_subtotal' => 1,
+ };
+
+ if ( $multisection ) {
+ ($extra_sections, $extra_lines) =
+ $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
+ if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
+ && $self->can('_items_extra_usage_sections');
+
+ push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
+
+ push @detail_items, @$extra_lines if $extra_lines;
+ push @sections,
+ $self->_items_sections( $late_sections, # this could stand a refactor
+ $summarypage,
+ $escape_function_nonbsp,
+ $extra_sections,
+ $format, #bah
+ );
+ if ( $conf->exists('svc_phone_sections')
+ && $self->can('_items_svc_phone_sections')
+ )
+ {
+ my ($phone_sections, $phone_lines) =
+ $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
+ push @{$late_sections}, @$phone_sections;
+ push @detail_items, @$phone_lines;
+ }
+ if ( $conf->exists('voip-cust_accountcode_cdr')
+ && $cust_main->accountcode_cdr
+ && $self->can('_items_accountcode_cdr')
+ )
+ {
+ my ($accountcode_section, $accountcode_lines) =
+ $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
+ if ( scalar(@$accountcode_lines) ) {
+ push @{$late_sections}, $accountcode_section;
+ push @detail_items, @$accountcode_lines;
+ }
+ }
+ } else {# not multisection
+ # make a default section
+ push @sections, $default_section;
+ # and calculate the finance charge total, since it won't get done otherwise.
+ # XXX possibly other totals?
+ # XXX possibly finance_pkgclass should not be used in this manner?
+ if ( $conf->exists('finance_pkgclass') ) {
+ my @finance_charges;
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ if ( grep { $_->section eq $invoice_data{finance_section} }
+ $cust_bill_pkg->cust_bill_pkg_display ) {
+ # I think these are always setup fees, but just to be sure...
+ push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
+ }
+ }
+ $invoice_data{finance_amount} =
+ sprintf('%.2f', sum( @finance_charges ) || 0);
+ }
+ }
+
+ unless ( $conf->exists('disable_previous_balance', $agentnum)
+ || $conf->exists('previous_balance-summary_only')
+ || ! $self->can('_items_previous')
+ )
+ {
+
+ warn "$me adding previous balances\n"
+ if $DEBUG > 1;
+
+ foreach my $line_item ( $self->_items_previous ) {
+
+ my $detail = {
+ ext_description => [],
+ };
+ $detail->{'ref'} = $line_item->{'pkgnum'};
+ $detail->{'quantity'} = 1;
+ $detail->{'section'} = $multisection ? $previous_section
+ : $default_section;
+ $detail->{'description'} = &$escape_function($line_item->{'description'});
+ if ( exists $line_item->{'ext_description'} ) {
+ @{$detail->{'ext_description'}} = map {
+ &$escape_function($_);
+ } @{$line_item->{'ext_description'}};
+ }
+ $detail->{'amount'} = ( $old_latex ? '' : $money_char).
+ $line_item->{'amount'};
+ $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+ push @detail_items, $detail;
+ push @buf, [ $detail->{'description'},
+ $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+ ];
+ }
+
+ }
+
+ if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
+ {
+ push @buf, ['','-----------'];
+ push @buf, [ $self->mt('Total Previous Balance'),
+ $money_char. sprintf("%10.2f", $pr_total) ];
+ push @buf, ['',''];
+ }
+
+ if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
+ warn "$me adding DID summary\n"
+ if $DEBUG > 1;
+
+ my ($didsummary,$minutes) = $self->_did_summary;
+ my $didsummary_desc = 'DID Activity Summary (since last invoice)';
+ push @detail_items,
+ { 'description' => $didsummary_desc,
+ 'ext_description' => [ $didsummary, $minutes ],
+ };
+ }
+
+ foreach my $section (@sections, @$late_sections) {
+
+ warn "$me adding section \n". Dumper($section)
+ if $DEBUG > 1;
+
+ # begin some normalization
+ $section->{'subtotal'} = $section->{'amount'}
+ if $multisection
+ && !exists($section->{subtotal})
+ && exists($section->{amount});
+
+ $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
+ if ( $invoice_data{finance_section} &&
+ $section->{'description'} eq $invoice_data{finance_section} );
+
+ $section->{'subtotal'} = $other_money_char.
+ sprintf('%.2f', $section->{'subtotal'})
+ if $multisection;
+
+ # continue some normalization
+ $section->{'amount'} = $section->{'subtotal'}
+ if $multisection;
+
+
+ if ( $section->{'description'} ) {
+ push @buf, ( [ &$escape_function($section->{'description'}), '' ],
+ [ '', '' ],
+ );
+ }
+
+ warn "$me setting options\n"
+ if $DEBUG > 1;
+
+ my $multilocation = scalar($cust_main->cust_location); #too expensive?
+ my %options = ();
+ $options{'section'} = $section if $multisection;
+ $options{'format'} = $format;
+ $options{'escape_function'} = $escape_function;
+ $options{'no_usage'} = 1 unless $unsquelched;
+ $options{'unsquelched'} = $unsquelched;
+ $options{'summary_page'} = $summarypage;
+ $options{'skip_usage'} =
+ scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
+ $options{'multilocation'} = $multilocation;
+ $options{'multisection'} = $multisection;
+
+ warn "$me searching for line items\n"
+ if $DEBUG > 1;
+
+ foreach my $line_item ( $self->_items_pkg(%options) ) {
+
+ warn "$me adding line item $line_item\n"
+ if $DEBUG > 1;
+
+ my $detail = {
+ ext_description => [],
+ };
+ $detail->{'ref'} = $line_item->{'pkgnum'};
+ $detail->{'quantity'} = $line_item->{'quantity'};
+ $detail->{'section'} = $section;
+ $detail->{'description'} = &$escape_function($line_item->{'description'});
+ if ( exists $line_item->{'ext_description'} ) {
+ @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
+ }
+ $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
+ $line_item->{'amount'};
+ $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
+ $line_item->{'unit_amount'};
+ $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+ $detail->{'sdate'} = $line_item->{'sdate'};
+ $detail->{'edate'} = $line_item->{'edate'};
+ $detail->{'seconds'} = $line_item->{'seconds'};
+
+ push @detail_items, $detail;
+ push @buf, ( [ $detail->{'description'},
+ $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+ ],
+ map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
+ );
+ }
+
+ if ( $section->{'description'} ) {
+ push @buf, ( ['','-----------'],
+ [ $section->{'description'}. ' sub-total',
+ $section->{'subtotal'} # already formatted this
+ ],
+ [ '', '' ],
+ [ '', '' ],
+ );
+ }
+
+ }
+
+ $invoice_data{current_less_finance} =
+ sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
+
+ if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
+ || $conf->exists('previous_balance-summary_only') )
+ {
+ unshift @sections, $previous_section if $pr_total;
+ }
+
+ warn "$me adding taxes\n"
+ if $DEBUG > 1;
+
+ foreach my $tax ( $self->_items_tax ) {
+
+ $taxtotal += $tax->{'amount'};
+
+ my $description = &$escape_function( $tax->{'description'} );
+ my $amount = sprintf( '%.2f', $tax->{'amount'} );
+
+ if ( $multisection ) {
+
+ my $money = $old_latex ? '' : $money_char;
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => $description,
+ amount => $money. $amount,
+ product_code => '',
+ section => $tax_section,
+ };
+
+ } else {
+
+ push @total_items, {
+ 'total_item' => $description,
+ 'total_amount' => $other_money_char. $amount,
+ };
+
+ }
+
+ push @buf,[ $description,
+ $money_char. $amount,
+ ];
+
+ }
+
+ if ( $taxtotal ) {
+ my $total = {};
+ $total->{'total_item'} = $self->mt('Sub-total');
+ $total->{'total_amount'} =
+ $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
+
+ if ( $multisection ) {
+ $tax_section->{'subtotal'} = $other_money_char.
+ sprintf('%.2f', $taxtotal);
+ $tax_section->{'pretotal'} = 'New charges sub-total '.
+ $total->{'total_amount'};
+ push @sections, $tax_section if $taxtotal;
+ }else{
+ unshift @total_items, $total;
+ }
+ }
+ $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
+
+ push @buf,['','-----------'];
+ push @buf,[$self->mt(
+ $conf->exists('disable_previous_balance', $agentnum)
+ ? 'Total Charges'
+ : 'Total New Charges'
+ ),
+ $money_char. sprintf("%10.2f",$self->charged) ];
+ push @buf,['',''];
+
+ {
+ my $total = {};
+ my $item = 'Total';
+ $item = $conf->config('previous_balance-exclude_from_total')
+ || 'Total New Charges'
+ if $conf->exists('previous_balance-exclude_from_total');
+ my $amount = $self->charged +
+ ( $conf->exists('disable_previous_balance', $agentnum) ||
+ $conf->exists('previous_balance-exclude_from_total')
+ ? 0
+ : $pr_total
+ );
+ $total->{'total_item'} = &$embolden_function($self->mt($item));
+ $total->{'total_amount'} =
+ &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
+ if ( $multisection ) {
+ if ( $adjust_section->{'sort_weight'} ) {
+ $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
+ $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
+ } else {
+ $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
+ $other_money_char. sprintf('%.2f', $self->charged );
+ }
+ }else{
+ push @total_items, $total;
+ }
+ push @buf,['','-----------'];
+ push @buf,[$item,
+ $money_char.
+ sprintf( '%10.2f', $amount )
+ ];
+ push @buf,['',''];
+ }
+
+ unless ( $conf->exists('disable_previous_balance', $agentnum)
+ || ! $self->can('_items_credits')
+ || ! $self->can('_items_payments')
+ )
+ {
+ #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+
+ # credits
+ my $credittotal = 0;
+ foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
+ my $total;
+ $total->{'total_item'} = &$escape_function($credit->{'description'});
+ $credittotal += $credit->{'amount'};
+ $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+ $adjusttotal += $credit->{'amount'};
+ if ( $multisection ) {
+ my $money = $old_latex ? '' : $money_char;
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => &$escape_function($credit->{'description'}),
+ amount => $money. $credit->{'amount'},
+ product_code => '',
+ section => $adjust_section,
+ };
+ } else {
+ push @total_items, $total;
+ }
+
+ }
+ $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
+
+ #credits (again)
+ foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+ push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+ }
+
+ # payments
+ my $paymenttotal = 0;
+ foreach my $payment ( $self->_items_payments ) {
+ my $total = {};
+ $total->{'total_item'} = &$escape_function($payment->{'description'});
+ $paymenttotal += $payment->{'amount'};
+ $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+ $adjusttotal += $payment->{'amount'};
+ if ( $multisection ) {
+ my $money = $old_latex ? '' : $money_char;
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => &$escape_function($payment->{'description'}),
+ amount => $money. $payment->{'amount'},
+ product_code => '',
+ section => $adjust_section,
+ };
+ }else{
+ push @total_items, $total;
+ }
+ push @buf, [ $payment->{'description'},
+ $money_char. sprintf("%10.2f", $payment->{'amount'}),
+ ];
+ }
+ $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
+
+ if ( $multisection ) {
+ $adjust_section->{'subtotal'} = $other_money_char.
+ sprintf('%.2f', $adjusttotal);
+ push @sections, $adjust_section
+ unless $adjust_section->{sort_weight};
+ }
+
+ # create Balance Due message
+ {
+ my $total;
+ $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+ $total->{'total_amount'} =
+ &$embolden_function(
+ $other_money_char. sprintf('%.2f', $summarypage
+ ? $self->charged +
+ $self->billing_balance
+ : $self->owed + $pr_total
+ )
+ );
+ if ( $multisection && !$adjust_section->{sort_weight} ) {
+ $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+ $total->{'total_amount'};
+ }else{
+ push @total_items, $total;
+ }
+ push @buf,['','-----------'];
+ push @buf,[$self->balance_due_msg, $money_char.
+ sprintf("%10.2f", $balance_due ) ];
+ }
+
+ if ( $conf->exists('previous_balance-show_credit')
+ and $cust_main->balance < 0 ) {
+ my $credit_total = {
+ 'total_item' => &$embolden_function($self->credit_balance_msg),
+ 'total_amount' => &$embolden_function(
+ $other_money_char. sprintf('%.2f', -$cust_main->balance)
+ ),
+ };
+ if ( $multisection ) {
+ $adjust_section->{'posttotal'} .= $newline_token .
+ $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+ }
+ else {
+ push @total_items, $credit_total;
+ }
+ push @buf,['','-----------'];
+ push @buf,[$self->credit_balance_msg, $money_char.
+ sprintf("%10.2f", -$cust_main->balance ) ];
+ }
+ }
+
+ if ( $multisection ) {
+ if ( $conf->exists('svc_phone_sections')
+ && $self->can('_items_svc_phone_sections')
+ )
+ {
+ my $total;
+ $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+ $total->{'total_amount'} =
+ &$embolden_function(
+ $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+ );
+ my $last_section = pop @sections;
+ $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
+ $total->{'total_amount'};
+ push @sections, $last_section;
+ }
+ push @sections, @$late_sections
+ if $unsquelched;
+ }
+
+ # make a discounts-available section, even without multisection
+ if ( $conf->exists('discount-show_available')
+ and my @discounts_avail = $self->_items_discounts_avail ) {
+ my $discount_section = {
+ 'description' => $self->mt('Discounts Available'),
+ 'subtotal' => '',
+ 'no_subtotal' => 1,
+ };
+
+ push @sections, $discount_section;
+ push @detail_items, map { +{
+ 'ref' => '', #should this be something else?
+ 'section' => $discount_section,
+ 'description' => &$escape_function( $_->{description} ),
+ 'amount' => $money_char . &$escape_function( $_->{amount} ),
+ 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
+ } } @discounts_avail;
+ }
+
+ # All sections and items are built; now fill in templates.
+ my @includelist = ();
+ push @includelist, 'summary' if $summarypage;
+ foreach my $include ( @includelist ) {
+
+ my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
+ my @inc_src;
+
+ if ( length( $conf->config($inc_file, $agentnum) ) ) {
+
+ @inc_src = $conf->config($inc_file, $agentnum);
+
+ } else {
+
+ $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+
+ my $convert_map = $convert_maps{$format}{$include};
+
+ @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+ s/--\@\]/$delimiters{$format}[1]/g;
+ $_;
+ }
+ &$convert_map( $conf->config($inc_file, $agentnum) );
+
+ }
+
+ my $inc_tt = new Text::Template (
+ TYPE => 'ARRAY',
+ SOURCE => [ map "$_\n", @inc_src ],
+ DELIMITERS => $delimiters{$format},
+ ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+ unless ( $inc_tt->compile() ) {
+ my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+ warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+ die $error;
+ }
+
+ $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+ $invoice_data{$include} =~ s/\n+$//
+ if ($format eq 'latex');
+ }
+
+ $invoice_lines = 0;
+ my $wasfunc = 0;
+ foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
+ /invoice_lines\((\d*)\)/;
+ $invoice_lines += $1 || scalar(@buf);
+ $wasfunc=1;
+ }
+ die "no invoice_lines() functions in template?"
+ if ( $format eq 'template' && !$wasfunc );
+
+ if ($format eq 'template') {
+
+ if ( $invoice_lines ) {
+ $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
+ $invoice_data{'total_pages'}++
+ if scalar(@buf) % $invoice_lines;
+ }
+
+ #setup subroutine for the template
+ $invoice_data{invoice_lines} = sub {
+ my $lines = shift || scalar(@buf);
+ map {
+ scalar(@buf)
+ ? shift @buf
+ : [ '', '' ];
+ }
+ ( 1 .. $lines );
+ };
+
+ my $lines;
+ my @collect;
+ while (@buf) {
+ push @collect, split("\n",
+ $text_template->fill_in( HASH => \%invoice_data )
+ );
+ $invoice_data{'page'}++;
+ }
+ map "$_\n", @collect;
+
+ } else { # this is where we actually create the invoice
+
+ warn "filling in template for invoice ". $self->invnum. "\n"
+ if $DEBUG;
+ warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
+ if $DEBUG > 1;
+
+ $text_template->fill_in(HASH => \%invoice_data);
+ }
+}
+
+sub notice_name { '('.shift->table.')'; }
+
+sub template_conf { 'invoice_'; }
+
+# helper routine for generating date ranges
+sub _prior_month30s {
+ my $self = shift;
+ my @ranges = (
+ [ 1, 2592000 ], # 0-30 days ago
+ [ 2592000, 5184000 ], # 30-60 days ago
+ [ 5184000, 7776000 ], # 60-90 days ago
+ [ 7776000, 0 ], # 90+ days ago
+ );
+
+ map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
+ $_->[1] ? $self->_date - $_->[1] - 1 : '',
+ ] }
+ @ranges;
+}
+
+=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
+
+Returns an postscript invoice, as a scalar.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<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.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_ps {
+ my $self = shift;
+
+ my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
+ my $ps = generate_ps($file);
+ unlink($logofile);
+ unlink($barcodefile) if $barcodefile;
+
+ $ps;
+}
+
+=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
+
+Returns an PDF invoice, as a scalar.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<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.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_pdf {
+ my $self = shift;
+
+ my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
+ my $pdf = generate_pdf($file);
+ unlink($logofile);
+ unlink($barcodefile) if $barcodefile;
+
+ $pdf;
+}
+
+=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
+
+Returns an HTML invoice, as a scalar.
+
+I<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.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
+when emailing the invoice as part of a multipart/related MIME email.
+
+=cut
+
+sub print_html {
+ my $self = shift;
+ my %params;
+ if ( ref($_[0]) ) {
+ %params = %{ shift() };
+ }else{
+ $params{'time'} = shift;
+ $params{'template'} = shift;
+ $params{'cid'} = shift;
+ }
+
+ $params{'format'} = 'html';
+
+ $self->print_generic( %params );
+}
+
+# quick subroutine for print_latex
+#
+# There are ten characters that LaTeX treats as special characters, which
+# means that they do not simply typeset themselves:
+# # $ % & ~ _ ^ \ { }
+#
+# TeX ignores blanks following an escaped character; if you want a blank (as
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
+
+sub _latex_escape {
+ my $value = shift;
+ $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
+ $value =~ s/([<>])/\$$1\$/g;
+ $value;
+}
+
+sub _html_escape {
+ my $value = shift;
+ encode_entities($value);
+ $value;
+}
+
+sub _html_escape_nbsp {
+ my $value = _html_escape(shift);
+ $value =~ s/ +/&nbsp;/g;
+ $value;
+}
+
+#utility methods for print_*
+
+sub _translate_old_latex_format {
+ warn "_translate_old_latex_format called\n"
+ if $DEBUG;
+
+ my @template = ();
+ while ( @_ ) {
+ my $line = shift;
+
+ if ( $line =~ /^%%Detail\s*$/ ) {
+
+ push @template, q![@--!,
+ q! foreach my $_tr_line (@detail_items) {!,
+ q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
+ q! $_tr_line->{'description'} .= !,
+ q! "\\tabularnewline\n~~".!,
+ q! join( "\\tabularnewline\n~~",!,
+ q! @{$_tr_line->{'ext_description'}}!,
+ q! );!,
+ q! }!;
+
+ while ( ( my $line_item_line = shift )
+ !~ /^%%EndDetail\s*$/ ) {
+ $line_item_line =~ s/'/\\'/g; # nice LTS
+ $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
+ $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
+ push @template, " \$OUT .= '$line_item_line';";
+ }
+
+ push @template, '}',
+ '--@]';
+ #' doh, gvim
+ } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+
+ push @template, '[@--',
+ ' foreach my $_tr_line (@total_items) {';
+
+ while ( ( my $total_item_line = shift )
+ !~ /^%%EndTotalDetails\s*$/ ) {
+ $total_item_line =~ s/'/\\'/g; # nice LTS
+ $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
+ $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
+ push @template, " \$OUT .= '$total_item_line';";
+ }
+
+ push @template, '}',
+ '--@]';
+
+ } else {
+ $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
+ push @template, $line;
+ }
+
+ }
+
+ if ($DEBUG) {
+ warn "$_\n" foreach @template;
+ }
+
+ (@template);
+}
+
+sub terms {
+ my $self = shift;
+ my $conf = $self->conf;
+
+ #check for an invoice-specific override
+ return $self->invoice_terms if $self->invoice_terms;
+
+ #check for a customer- specific override
+ my $cust_main = $self->cust_main;
+ return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
+
+ #use configured default
+ $conf->config('invoice_default_terms') || '';
+}
+
+sub due_date {
+ my $self = shift;
+ my $duedate = '';
+ if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
+ $duedate = $self->_date() + ( $1 * 86400 );
+ }
+ $duedate;
+}
+
+sub due_date2str {
+ my $self = shift;
+ $self->due_date ? time2str(shift, $self->due_date) : '';
+}
+
+sub balance_due_msg {
+ my $self = shift;
+ my $msg = $self->mt('Balance Due');
+ return $msg unless $self->terms;
+ if ( $self->due_date ) {
+ $msg .= ' - ' . $self->mt('Please pay by'). ' '.
+ $self->due_date2str($date_format);
+ } elsif ( $self->terms ) {
+ $msg .= ' - '. $self->terms;
+ }
+ $msg;
+}
+
+sub balance_due_date {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $duedate = '';
+ if ( $conf->exists('invoice_default_terms')
+ && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
+ $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
+ }
+ $duedate;
+}
+
+sub credit_balance_msg {
+ my $self = shift;
+ $self->mt('Credit Balance Remaining')
+}
+
+=item _date_pretty
+
+Returns a string with the date, for example: "3/20/2008"
+
+=cut
+
+sub _date_pretty {
+ my $self = shift;
+ time2str($date_format, $self->_date);
+}
+
+=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
+
+Generate section information for all items appearing on this invoice.
+This will only be called for multi-section invoices.
+
+For each line item (L<FS::cust_bill_pkg> record), this will fetch all
+related display records (L<FS::cust_bill_pkg_display>) and organize
+them into two groups ("early" and "late" according to whether they come
+before or after the total), then into sections. A subtotal is calculated
+for each section.
+
+Section descriptions are returned in sort weight order. Each consists
+of a hash containing:
+
+description: the package category name, escaped
+subtotal: the total charges in that section
+tax_section: a flag indicating that the section contains only tax charges
+summarized: same as tax_section, for some reason
+sort_weight: the package category's sort weight
+
+If 'condense' is set on the display record, it also contains everything
+returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
+coderefs to generate parts of the invoice. This is not advised.
+
+Arguments:
+
+LATE: an arrayref to push the "late" section hashes onto. The "early"
+group is simply returned from the method.
+
+SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
+Turning this on has the following effects:
+- Ignores display items with the 'summary' flag.
+- Combines all items into the "early" group.
+- Creates sections for all non-disabled package categories, even if they
+have no charges on this invoice, as well as a section with no name.
+
+ESCAPE: an escape function to use for section titles.
+
+EXTRA_SECTIONS: an arrayref of additional sections to return after the
+sorted list. If there are any of these, section subtotals exclude
+usage charges.
+
+FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
+passed through to C<_condense_section()>.
+
+=cut
+
+use vars qw(%pkg_category_cache);
+sub _items_sections {
+ my $self = shift;
+ my $late = shift;
+ my $summarypage = shift;
+ my $escape = shift;
+ my $extra_sections = shift;
+ my $format = shift;
+
+ my %subtotal = ();
+ my %late_subtotal = ();
+ my %not_tax = ();
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+ {
+
+ my $usage = $cust_bill_pkg->usage;
+
+ foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
+ next if ( $display->summary && $summarypage );
+
+ my $section = $display->section;
+ my $type = $display->type;
+
+ $not_tax{$section} = 1
+ unless $cust_bill_pkg->pkgnum == 0;
+
+ if ( $display->post_total && !$summarypage ) {
+ if (! $type || $type eq 'S') {
+ $late_subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero;
+ }
+
+ if (! $type) {
+ $late_subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
+ }
+
+ if ($type && $type eq 'R') {
+ $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
+ }
+
+ if ($type && $type eq 'U') {
+ $late_subtotal{$section} += $usage
+ unless scalar(@$extra_sections);
+ }
+
+ } else {
+
+ next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
+ if (! $type || $type eq 'S') {
+ $subtotal{$section} += $cust_bill_pkg->setup
+ if $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero;
+ }
+
+ if (! $type) {
+ $subtotal{$section} += $cust_bill_pkg->recur
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
+ }
+
+ if ($type && $type eq 'R') {
+ $subtotal{$section} += $cust_bill_pkg->recur - $usage
+ if $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->recur_show_zero;
+ }
+
+ if ($type && $type eq 'U') {
+ $subtotal{$section} += $usage
+ unless scalar(@$extra_sections);
+ }
+
+ }
+
+ }
+
+ }
+
+ %pkg_category_cache = ();
+
+ push @$late, map { { 'description' => &{$escape}($_),
+ 'subtotal' => $late_subtotal{$_},
+ 'post_total' => 1,
+ 'sort_weight' => ( _pkg_category($_)
+ ? _pkg_category($_)->weight
+ : 0
+ ),
+ ((_pkg_category($_) && _pkg_category($_)->condense)
+ ? $self->_condense_section($format)
+ : ()
+ ),
+ } }
+ sort _sectionsort keys %late_subtotal;
+
+ my @sections;
+ if ( $summarypage ) {
+ @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
+ map { $_->categoryname } qsearch('pkg_category', {});
+ push @sections, '' if exists($subtotal{''});
+ } else {
+ @sections = keys %subtotal;
+ }
+
+ my @early = map { { 'description' => &{$escape}($_),
+ 'subtotal' => $subtotal{$_},
+ 'summarized' => $not_tax{$_} ? '' : 'Y',
+ 'tax_section' => $not_tax{$_} ? '' : 'Y',
+ 'sort_weight' => ( _pkg_category($_)
+ ? _pkg_category($_)->weight
+ : 0
+ ),
+ ((_pkg_category($_) && _pkg_category($_)->condense)
+ ? $self->_condense_section($format)
+ : ()
+ ),
+ }
+ } @sections;
+ push @early, @$extra_sections if $extra_sections;
+
+ sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
+
+}
+
+#helper subs for above
+
+sub _sectionsort {
+ _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+}
+
+sub _pkg_category {
+ my $categoryname = shift;
+ $pkg_category_cache{$categoryname} ||=
+ qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
+}
+
+my %condensed_format = (
+ 'label' => [ qw( Description Qty Amount ) ],
+ 'fields' => [
+ sub { shift->{description} },
+ sub { shift->{quantity} },
+ sub { my($href, %opt) = @_;
+ ($opt{dollar} || ''). $href->{amount};
+ },
+ ],
+ 'align' => [ qw( l r r ) ],
+ 'span' => [ qw( 5 1 1 ) ], # unitprices?
+ 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
+);
+
+sub _condense_section {
+ my ( $self, $format ) = ( shift, shift );
+ ( 'condensed' => 1,
+ map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ );
+}
+
+sub _condensed_generator_defaults {
+ my ( $self, $format ) = ( shift, shift );
+ return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
+}
+
+my %html_align = (
+ 'c' => 'center',
+ 'l' => 'left',
+ 'r' => 'right',
+);
+
+sub _condensed_header_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
+ $suffix = "\\\\\n\\hline";
+ $separator = "&\n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ } elsif ( $format eq 'html' ) {
+ $prefix = '<th></th>';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<th align="$html_align{$a}">$d</th>!;
+ };
+ }
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
+ }
+
+ $prefix. join($separator, @result). $suffix;
+ };
+
+}
+
+sub _condensed_description_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ my $money_char = '$';
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
+ $suffix = '\\\\';
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ $money_char = '\\dollar';
+ }elsif ( $format eq 'html' ) {
+ $prefix = '"><td align="center"></td>';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}">$d</td>!;
+ };
+ #$money_char = $conf->config('money_char') || '$';
+ $money_char = ''; # this is madness
+ }
+
+ sub {
+ #my @args = @_;
+ my $href = shift;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ my $dollar = '';
+ $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+sub _condensed_total_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ # my $r = &{$f->{fields}->[$i]}(@args);
+ # $r .= ' Total' unless $i;
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+=item total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class. FORMAT is either html or latex
+
+=cut
+
+# should not be used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub _condensed_total_line_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+# sub _items { # seems to be unused
+# my $self = shift;
+#
+# #my @display = scalar(@_)
+# # ? @_
+# # : qw( _items_previous _items_pkg );
+# # #: qw( _items_pkg );
+# # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+# my @display = qw( _items_previous _items_pkg );
+#
+# my @b = ();
+# foreach my $display ( @display ) {
+# push @b, $self->$display(@_);
+# }
+# @b;
+# }
+
+=item _items_pkg [ OPTIONS ]
+
+Return line item hashes for each package item on this invoice. Nearly
+equivalent to
+
+$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
+
+The only OPTIONS accepted is 'section', which may point to a hashref
+with a key named 'condensed', which may have a true value. If it
+does, this method tries to merge identical items into items with
+'quantity' equal to the number of items (not the sum of their
+separate quantities, for some reason).
+
+=cut
+
+sub _items_pkg {
+ my $self = shift;
+ my %options = @_;
+
+ warn "$me _items_pkg searching for all package line items\n"
+ if $DEBUG > 1;
+
+ my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+
+ warn "$me _items_pkg filtering line items\n"
+ if $DEBUG > 1;
+ my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+ if ($options{section} && $options{section}->{condensed}) {
+
+ warn "$me _items_pkg condensing section\n"
+ if $DEBUG > 1;
+
+ my %itemshash = ();
+ local $Storable::canonical = 1;
+ foreach ( @items ) {
+ my $item = { %$_ };
+ delete $item->{ref};
+ delete $item->{ext_description};
+ my $key = freeze($item);
+ $itemshash{$key} ||= 0;
+ $itemshash{$key} ++; # += $item->{quantity};
+ }
+ @items = sort { $a->{description} cmp $b->{description} }
+ map { my $i = thaw($_);
+ $i->{quantity} = $itemshash{$_};
+ $i->{amount} =
+ sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
+ $i;
+ }
+ keys %itemshash;
+ }
+
+ warn "$me _items_pkg returning ". scalar(@items). " items\n"
+ if $DEBUG > 1;
+
+ @items;
+}
+
+sub _taxsort {
+ return 0 unless $a->itemdesc cmp $b->itemdesc;
+ return -1 if $b->itemdesc eq 'Tax';
+ return 1 if $a->itemdesc eq 'Tax';
+ return -1 if $b->itemdesc eq 'Other surcharges';
+ return 1 if $a->itemdesc eq 'Other surcharges';
+ $a->itemdesc cmp $b->itemdesc;
+}
+
+sub _items_tax {
+ my $self = shift;
+ my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
+ $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
+
+Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
+list of hashrefs describing the line items they generate on the invoice.
+
+OPTIONS may include:
+
+format: the invoice format.
+
+escape_function: the function used to escape strings.
+
+DEPRECATED? (expensive, mostly unused?)
+format_function: the function used to format CDRs.
+
+section: a hashref containing 'description'; if this is present,
+cust_bill_pkg_display records not belonging to this section are
+ignored.
+
+multisection: a flag indicating that this is a multisection invoice,
+which does something complicated.
+
+multilocation: a flag to display the location label for the package.
+
+Returns a list of hashrefs, each of which may contain:
+
+pkgnum, description, amount, unit_amount, quantity, _is_setup, and
+ext_description, which is an arrayref of detail lines to show below
+the package line.
+
+=cut
+
+sub _items_cust_bill_pkg {
+ my $self = shift;
+ my $conf = $self->conf;
+ my $cust_bill_pkgs = shift;
+ my %opt = @_;
+
+ my $format = $opt{format} || '';
+ my $escape_function = $opt{escape_function} || sub { shift };
+ my $format_function = $opt{format_function} || '';
+ my $no_usage = $opt{no_usage} || '';
+ my $unsquelched = $opt{unsquelched} || ''; #unused
+ my $section = $opt{section}->{description} if $opt{section};
+ my $summary_page = $opt{summary_page} || ''; #unused
+ my $multilocation = $opt{multilocation} || '';
+ my $multisection = $opt{multisection} || '';
+ my $discount_show_always = 0;
+
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+ my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+
+ my @b = ();
+ my ($s, $r, $u) = ( undef, undef, undef );
+ foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
+ {
+
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+ if ( $_ && !$cust_bill_pkg->hidden ) {
+ $_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{amount} =~ s/^\-0\.00$/0.00/;
+ $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+ push @b, { %$_ }
+ if $_->{amount} != 0
+ || $discount_show_always
+ || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ ;
+ $_ = undef;
+ }
+ }
+
+ my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
+
+ warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
+ $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
+ if $DEBUG > 1;
+
+ foreach my $display ( grep { defined($section)
+ ? $_->section eq $section
+ : 1
+ }
+ #grep { !$_->summary || !$summary_page } # bunk!
+ grep { !$_->summary || $multisection }
+ @cust_bill_pkg_display
+ )
+ {
+
+ warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
+ $display->billpkgdisplaynum. "\n"
+ if $DEBUG > 1;
+
+ my $type = $display->type;
+
+ my $desc = $cust_bill_pkg->desc;
+ $desc = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
+
+ my %details_opt = ( 'format' => $format,
+ 'escape_function' => $escape_function,
+ 'format_function' => $format_function,
+ 'no_usage' => $opt{'no_usage'},
+ );
+
+ if ( $cust_bill_pkg->pkgnum > 0 ) {
+
+ warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
+ if $DEBUG > 1;
+
+ my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+ # start/end dates for invoice formats that do nonstandard
+ # things with them
+ my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
+
+ if ( (!$type || $type eq 'S')
+ && ( $cust_bill_pkg->setup != 0
+ || $cust_bill_pkg->setup_show_zero
+ )
+ )
+ {
+
+ warn "$me _items_cust_bill_pkg adding setup\n"
+ if $DEBUG > 1;
+
+ my $description = $desc;
+ $description .= ' Setup'
+ if $cust_bill_pkg->recur != 0
+ || $discount_show_always
+ || $cust_bill_pkg->recur_show_zero;
+
+ my @d = ();
+ unless ( $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->hidden )
+ {
+
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short($self->_date, undef, 'I')
+ unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($loc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($loc) > $maxlength;
+ push @d, &{$escape_function}($loc);
+ }
+
+ } #unless hiding service details
+
+ push @d, $cust_bill_pkg->details(%details_opt)
+ if $cust_bill_pkg->recur == 0;
+
+ if ( $cust_bill_pkg->hidden ) {
+ $s->{amount} += $cust_bill_pkg->setup;
+ $s->{unit_amount} += $cust_bill_pkg->unitsetup;
+ push @{ $s->{ext_description} }, @d;
+ } else {
+ $s = {
+ _is_setup => 1,
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $cust_bill_pkg->setup,
+ setup_show_zero => $cust_bill_pkg->setup_show_zero,
+ unit_amount => $cust_bill_pkg->unitsetup,
+ quantity => $cust_bill_pkg->quantity,
+ ext_description => \@d,
+ };
+ };
+
+ }
+
+ if ( ( !$type || $type eq 'R' || $type eq 'U' )
+ && (
+ $cust_bill_pkg->recur != 0
+ || $cust_bill_pkg->setup == 0
+ || $discount_show_always
+ || $cust_bill_pkg->recur_show_zero
+ )
+ )
+ {
+
+ warn "$me _items_cust_bill_pkg adding recur/usage\n"
+ if $DEBUG > 1;
+
+ my $is_summary = $display->summary;
+ my $description = ($is_summary && $type && $type eq 'U')
+ ? "Usage charges" : $desc;
+
+ #pry be a bit more efficient to look some of this conf stuff up
+ # outside the loop
+ unless (
+ $conf->exists('disable_line_item_date_ranges')
+ || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
+ ) {
+ my $time_period;
+ my $date_style = $conf->config( 'cust_bill-line_item-date_style',
+ $cust_main->agentnum
+ );
+ if ( defined($date_style) && $date_style eq 'month_of' ) {
+ $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
+ } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
+ my $desc = $conf->config( 'cust_bill-line_item-date_description',
+ $cust_main->agentnum
+ );
+ $desc .= ' ' unless $desc =~ /\s$/;
+ $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
+ } else {
+ $time_period = time2str($date_format, $cust_bill_pkg->sdate).
+ " - ". time2str($date_format, $cust_bill_pkg->edate);
+ }
+ $description .= " ($time_period)";
+ }
+
+ my @d = ();
+ my @seconds = (); # for display of usage info
+
+ #at least until cust_bill_pkg has "past" ranges in addition to
+ #the "future" sdate/edate ones... see #3032
+ my @dates = ( $self->_date );
+ my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
+ push @dates, $prev->sdate if $prev;
+ push @dates, undef if !$prev;
+
+ unless ( $cust_pkg->part_pkg->hide_svc_detail
+ || $cust_bill_pkg->itemdesc
+ || $cust_bill_pkg->hidden
+ || $is_summary && $type && $type eq 'U' )
+ {
+
+ warn "$me _items_cust_bill_pkg adding service details\n"
+ if $DEBUG > 1;
+
+ push @d, map &{$escape_function}($_),
+ $cust_pkg->h_labels_short(@dates, 'I')
+ #$cust_bill_pkg->edate,
+ #$cust_bill_pkg->sdate)
+ unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+ warn "$me _items_cust_bill_pkg done adding service details\n"
+ if $DEBUG > 1;
+
+ if ( $multilocation ) {
+ my $loc = $cust_pkg->location_label;
+ $loc = substr($loc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($loc) > $maxlength;
+ push @d, &{$escape_function}($loc);
+ }
+
+ # Display of seconds_since_sqlradacct:
+ # On the invoice, when processing @detail_items, look for a field
+ # named 'seconds'. This will contain total seconds for each
+ # service, in the same order as @ext_description. For services
+ # that don't support this it will show undef.
+ if ( $conf->exists('svc_acct-usage_seconds')
+ and ! $cust_bill_pkg->pkgpart_override ) {
+ foreach my $cust_svc (
+ $cust_pkg->h_cust_svc(@dates, 'I')
+ ) {
+
+ # eval because not having any part_export_usage exports
+ # is a fatal error, last_bill/_date because that's how
+ # sqlradius_hour billing does it
+ my $sec = eval {
+ $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
+ };
+ push @seconds, $sec;
+ }
+ } #if svc_acct-usage_seconds
+
+ }
+
+ unless ( $is_summary ) {
+ warn "$me _items_cust_bill_pkg adding details\n"
+ if $DEBUG > 1;
+
+ #instead of omitting details entirely in this case (unwanted side
+ # effects), just omit CDRs
+ $details_opt{'no_usage'} = 1
+ if $type && $type eq 'R';
+
+ push @d, $cust_bill_pkg->details(%details_opt);
+ }
+
+ warn "$me _items_cust_bill_pkg calculating amount\n"
+ if $DEBUG > 1;
+
+ my $amount = 0;
+ if (!$type) {
+ $amount = $cust_bill_pkg->recur;
+ } elsif ($type eq 'R') {
+ $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+ } elsif ($type eq 'U') {
+ $amount = $cust_bill_pkg->usage;
+ }
+
+ if ( !$type || $type eq 'R' ) {
+
+ warn "$me _items_cust_bill_pkg adding recur\n"
+ if $DEBUG > 1;
+
+ if ( $cust_bill_pkg->hidden ) {
+ $r->{amount} += $amount;
+ $r->{unit_amount} += $cust_bill_pkg->unitrecur;
+ push @{ $r->{ext_description} }, @d;
+ } else {
+ $r = {
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $amount,
+ recur_show_zero => $cust_bill_pkg->recur_show_zero,
+ unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ %item_dates,
+ ext_description => \@d,
+ };
+ $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
+ }
+
+ } else { # $type eq 'U'
+
+ warn "$me _items_cust_bill_pkg adding usage\n"
+ if $DEBUG > 1;
+
+ if ( $cust_bill_pkg->hidden ) {
+ $u->{amount} += $amount;
+ $u->{unit_amount} += $cust_bill_pkg->unitrecur;
+ push @{ $u->{ext_description} }, @d;
+ } else {
+ $u = {
+ description => $description,
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ amount => $amount,
+ recur_show_zero => $cust_bill_pkg->recur_show_zero,
+ unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ %item_dates,
+ ext_description => \@d,
+ };
+ }
+ }
+
+ } # recurring or usage with recurring charge
+
+ } else { #pkgnum tax or one-shot line item (??)
+
+ warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
+ if $DEBUG > 1;
+
+ if ( $cust_bill_pkg->setup != 0 ) {
+ push @b, {
+ 'description' => $desc,
+ 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
+ };
+ }
+ if ( $cust_bill_pkg->recur != 0 ) {
+ push @b, {
+ 'description' => "$desc (".
+ time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+ time2str($date_format, $cust_bill_pkg->edate). ')',
+ 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
+ };
+ }
+
+ }
+
+ }
+
+ $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
+ && $conf->exists('discount-show-always'));
+
+ }
+
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+ if ( $_ ) {
+ $_->{amount} = sprintf( "%.2f", $_->{amount} ),
+ $_->{amount} =~ s/^\-0\.00$/0.00/;
+ $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+ push @b, { %$_ }
+ if $_->{amount} != 0
+ || $discount_show_always
+ || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+ || ( $_->{_is_setup} && $_->{setup_show_zero} )
+ }
+ }
+
+ warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
+ if $DEBUG > 1;
+
+ @b;
+
+}
+
+=item _items_discounts_avail
+
+Returns an array of line item hashrefs representing available term discounts
+for this invoice. This makes the same assumptions that apply to term
+discounts in general: that the package is billed monthly, at a flat rate,
+with no usage charges. A prorated first month will be handled, as will
+a setup fee if the discount is allowed to apply to setup fees.
+
+=cut
+
+sub _items_discounts_avail {
+ my $self = shift;
+
+ #maybe move this method from cust_bill when quotations support discount_plans
+ return () unless $self->can('discount_plans');
+ my %plans = $self->discount_plans;
+
+ my $list_pkgnums = 0; # if any packages are not eligible for all discounts
+ $list_pkgnums = grep { $_->list_pkgnums } values %plans;
+
+ map {
+ my $months = $_;
+ my $plan = $plans{$months};
+
+ my $term_total = sprintf('%.2f', $plan->discounted_total);
+ my $percent = sprintf('%.0f',
+ 100 * (1 - $term_total / $plan->base_total) );
+ my $permonth = sprintf('%.2f', $term_total / $months);
+ my $detail = $self->mt('discount on item'). ' '.
+ join(', ', map { "#$_" } $plan->pkgnums)
+ if $list_pkgnums;
+
+ # discounts for non-integer months don't work anyway
+ $months = sprintf("%d", $months);
+
+ +{
+ description => $self->mt('Save [_1]% by paying for [_2] months',
+ $percent, $months),
+ amount => $self->mt('[_1] ([_2] per month)',
+ $term_total, $money_char.$permonth),
+ ext_description => ($detail || ''),
+ }
+ } #map
+ sort { $b <=> $a } keys %plans;
+
+}
+
+1;
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 8d4b346..417b202 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -4,6 +4,7 @@ use strict;
use vars qw( @ISA @EXPORT_OK $DEBUG );
use Exporter;
use Tie::IxHash;
+use File::Slurp;
use FS::UID qw( dbh driver_name );
use FS::Conf;
use FS::Record qw(qsearchs qsearch str2time_sql);
@@ -63,7 +64,12 @@ sub upgrade_config {
upgrade_overlimit_groups($conf);
map { upgrade_overlimit_groups($conf,$_->agentnum) } qsearch('agent', {});
-
+
+ my $DIST_CONF = '/usr/local/etc/freeside/default_conf/';#DIST_CONF in Makefile
+ $conf->set($_, scalar(read_file( "$DIST_CONF/$_" )) )
+ foreach grep { ! $conf->exists($_) && -s "$DIST_CONF/$_" }
+ qw( quotation_html quotation_latex quotation_latexnotes );
+
}
sub upgrade_overlimit_groups {
diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm
index 1e65ca3..e6266b4 100644
--- a/FS/FS/access_right.pm
+++ b/FS/FS/access_right.pm
@@ -192,6 +192,7 @@ sub _upgrade_data { # class method
'Cancel customer package immediately' => 'Un-cancel customer package',
'Suspend customer package' => 'Suspend customer',
'Unsuspend customer package' => 'Unsuspend customer',
+ 'New prospect' => 'Generate quotation',
'List services' => [ 'Services: Accounts',
'Services: Domains',
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 498025f..83748be 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -1,26 +1,20 @@
package FS::cust_bill;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
use strict;
-use vars qw( @ISA $DEBUG $me
- $money_char $date_format $rdate_format $date_format_long );
+use vars qw( $DEBUG $me $date_format );
# but NOT $conf
-use vars qw( $invoice_lines @buf ); #yuck
use Fcntl qw(:flock); #for spool_csv
use Cwd;
-use List::Util qw(min max sum);
+use List::Util qw(min max);
use Date::Format;
-use Date::Language;
-use Text::Template 1.20;
use File::Temp 0.14;
-use String::ShellQuote;
use HTML::Entities;
-use Locale::Country;
use Storable qw( freeze thaw );
use GD::Barcode;
use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
+use FS::Misc qw( send_email send_fax do_print );
use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main_Mixin;
use FS::cust_main;
use FS::cust_statement;
use FS::cust_bill_pkg;
@@ -46,18 +40,13 @@ use FS::cust_credit_bill_pkg;
use FS::discount_plan;
use FS::L10N;
-@ISA = qw( FS::cust_main_Mixin FS::Record );
-
$DEBUG = 0;
$me = '[FS::cust_bill]';
#ask FS::UID to run this stuff for us later
FS::UID->install_callback( sub {
my $conf = new FS::Conf; #global
- $money_char = $conf->config('money_char') || '$';
$date_format = $conf->config('date_format') || '%x'; #/YY
- $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
- $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
} );
=head1 NAME
@@ -161,6 +150,7 @@ Invoices are normally created by calling the bill method of a customer object
=cut
sub table { 'cust_bill'; }
+sub notice_name { 'Invoice'; }
sub cust_linked { $_[0]->cust_main_custnum; }
sub cust_unlinked_msg {
@@ -2325,143 +2315,6 @@ sub _agent_invoice_from {
$self->cust_main->agent_invoice_from;
}
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Returns an text invoice, as a list of lines.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time>, if specified, is 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.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_text {
- my $self = shift;
- my( $today, $template, %opt );
- if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
- } else {
- ( $today, $template, %opt ) = @_;
- }
-
- my %params = ( 'format' => 'template' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
-
- $self->print_generic( %params );
-}
-
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Internal method - returns a filename of a filled-in LaTeX template for this
-invoice (Note: add ".tex" to get the actual filename), and a filename of
-an associated logo (with the .eps extension included).
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time>, if specified, is 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.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_latex {
- my $self = shift;
- my $conf = $self->conf;
- my( $today, $template, %opt );
- if ( ref($_[0]) ) {
- %opt = %{ shift() };
- $today = delete($opt{'time'}) || '';
- $template = delete($opt{template}) || '';
- } else {
- ( $today, $template, %opt ) = @_;
- }
-
- my %params = ( 'format' => 'latex' );
- $params{'time'} = $today if $today;
- $params{'template'} = $template if $template;
- $params{$_} = $opt{$_}
- foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
-
- $template ||= $self->_agent_template;
-
- my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
- my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
- DIR => $dir,
- SUFFIX => '.eps',
- UNLINK => 0,
- ) or die "can't open temp file: $!\n";
-
- my $agentnum = $self->cust_main->agentnum;
-
- if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
- print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
- or die "can't write temp file: $!\n";
- } else {
- print $lh $conf->config_binary('logo.eps', $agentnum)
- or die "can't write temp file: $!\n";
- }
- close $lh;
- $params{'logo_file'} = $lh->filename;
-
- if($conf->exists('invoice-barcode')){
- my $png_file = $self->invoice_barcode($dir);
- my $eps_file = $png_file;
- $eps_file =~ s/\.png$/.eps/g;
- $png_file =~ /(barcode.*png)/;
- $png_file = $1;
- $eps_file =~ /(barcode.*eps)/;
- $eps_file = $1;
-
- my $curr_dir = cwd();
- chdir($dir);
- # after painfuly long experimentation, it was determined that sam2p won't
- # accept : and other chars in the path, no matter how hard I tried to
- # escape them, hence the chdir (and chdir back, just to be safe)
- system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
- or die "sam2p failed: $!\n";
- unlink($png_file);
- chdir($curr_dir);
-
- $params{'barcode_file'} = $eps_file;
- }
-
- my @filled_in = $self->print_generic( %params );
-
- my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
- DIR => $dir,
- SUFFIX => '.tex',
- UNLINK => 0,
- ) or die "can't open temp file: $!\n";
- binmode($fh, ':utf8'); # language support
- print $fh join('', @filled_in );
- close $fh;
-
- $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
- return ($1, $params{'logo_file'}, $params{'barcode_file'});
-
-}
-
=item invoice_barcode DIR_OR_FALSE
Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
@@ -2491,1373 +2344,6 @@ sub invoice_barcode {
return $gd->png;
}
-=item print_generic OPTION => VALUE ...
-
-Internal method - returns a filled-in template for this invoice as a scalar.
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Non optional options include
- format - latex, html, template
-
-Optional options include
-
-template - a value used as a suffix for a configuration template
-
-time - a 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.
-
-cid -
-
-unsquelch_cdr - overrides any per customer cdr squelching when true
-
-notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-locale - override customer's locale
-
-=cut
-
-#what's with all the sprintf('%10.2f')'s in here? will it cause any
-# (alignment in text invoice?) problems to change them all to '%.2f' ?
-# yes: fixed width/plain text printing will be borked
-sub print_generic {
- my( $self, %params ) = @_;
- my $conf = $self->conf;
- my $today = $params{today} ? $params{today} : time;
- warn "$me print_generic called on $self with suffix $params{template}\n"
- if $DEBUG;
-
- my $format = $params{format};
- die "Unknown format: $format"
- unless $format =~ /^(latex|html|template)$/;
-
- my $cust_main = $self->cust_main;
- $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
- unless $cust_main->payname
- && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
-
- my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
- 'html' => [ '<%=', '%>' ],
- 'template' => [ '{', '}' ],
- );
-
- warn "$me print_generic creating template\n"
- if $DEBUG > 1;
-
- #create the template
- my $template = $params{template} ? $params{template} : $self->_agent_template;
- my $templatefile = "invoice_$format";
- $templatefile .= "_$template"
- if length($template) && $conf->exists($templatefile."_$template");
- my @invoice_template = map "$_\n", $conf->config($templatefile)
- or die "cannot load config data $templatefile";
-
- my $old_latex = '';
- if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
- #change this to a die when the old code is removed
- warn "old-style invoice template $templatefile; ".
- "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
- $old_latex = 'true';
- @invoice_template = _translate_old_latex_format(@invoice_template);
- }
-
- warn "$me print_generic creating T:T object\n"
- if $DEBUG > 1;
-
- my $text_template = new Text::Template(
- TYPE => 'ARRAY',
- SOURCE => \@invoice_template,
- DELIMITERS => $delimiters{$format},
- );
-
- warn "$me print_generic compiling T:T object\n"
- if $DEBUG > 1;
-
- $text_template->compile()
- or die "Can't compile $templatefile: $Text::Template::ERROR\n";
-
-
- # additional substitution could possibly cause breakage in existing templates
- my %convert_maps = (
- 'latex' => {
- 'notes' => sub { map "$_", @_ },
- 'footer' => sub { map "$_", @_ },
- 'smallfooter' => sub { map "$_", @_ },
- 'returnaddress' => sub { map "$_", @_ },
- 'coupon' => sub { map "$_", @_ },
- 'summary' => sub { map "$_", @_ },
- },
- 'html' => {
- 'notes' =>
- sub {
- map {
- s/%%(.*)$/<!-- $1 -->/g;
- s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
- s/\\begin\{enumerate\}/<ol>/g;
- s/\\item / <li>/g;
- s/\\end\{enumerate\}/<\/ol>/g;
- s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
- s/\\\\\*/<br>/g;
- s/\\dollar ?/\$/g;
- s/\\#/#/g;
- s/~/&nbsp;/g;
- $_;
- } @_
- },
- 'footer' =>
- sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
- 'smallfooter' =>
- sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
- 'returnaddress' =>
- sub {
- map {
- s/~/&nbsp;/g;
- s/\\\\\*?\s*$/<BR>/;
- s/\\hyphenation\{[\w\s\-]+}//;
- s/\\([&])/$1/g;
- $_;
- } @_
- },
- 'coupon' => sub { "" },
- 'summary' => sub { "" },
- },
- 'template' => {
- 'notes' =>
- sub {
- map {
- s/%%.*$//g;
- s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
- s/\\begin\{enumerate\}//g;
- s/\\item / * /g;
- s/\\end\{enumerate\}//g;
- s/\\textbf\{(.*)\}/$1/g;
- s/\\\\\*/ /;
- s/\\dollar ?/\$/g;
- $_;
- } @_
- },
- 'footer' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
- 'smallfooter' =>
- sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
- 'returnaddress' =>
- sub {
- map {
- s/~/ /g;
- s/\\\\\*?\s*$/\n/; # dubious
- s/\\hyphenation\{[\w\s\-]+}//;
- $_;
- } @_
- },
- 'coupon' => sub { "" },
- 'summary' => sub { "" },
- },
- );
-
-
- # hashes for differing output formats
- my %nbsps = ( 'latex' => '~',
- 'html' => '', # '&nbps;' would be nice
- 'template' => '', # not used
- );
- my $nbsp = $nbsps{$format};
-
- my %escape_functions = ( 'latex' => \&_latex_escape,
- 'html' => \&_html_escape_nbsp,#\&encode_entities,
- 'template' => sub { shift },
- );
- my $escape_function = $escape_functions{$format};
- my $escape_function_nonbsp = ($format eq 'html')
- ? \&_html_escape : $escape_function;
-
- my %date_formats = ( 'latex' => $date_format_long,
- 'html' => $date_format_long,
- 'template' => '%s',
- );
- $date_formats{'html'} =~ s/ /&nbsp;/g;
-
- my $date_format = $date_formats{$format};
-
- my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
- },
- 'html' => sub { return '<b>'. shift(). '</b>'
- },
- 'template' => sub { shift },
- );
- my $embolden_function = $embolden_functions{$format};
-
- my %newline_tokens = ( 'latex' => '\\\\',
- 'html' => '<br>',
- 'template' => "\n",
- );
- my $newline_token = $newline_tokens{$format};
-
- warn "$me generating template variables\n"
- if $DEBUG > 1;
-
- # generate template variables
- my $returnaddress;
- if (
- defined( $conf->config_orbase( "invoice_${format}returnaddress",
- $template
- )
- )
- && length( $conf->config_orbase( "invoice_${format}returnaddress",
- $template
- )
- )
- ) {
-
- $returnaddress = join("\n",
- $conf->config_orbase("invoice_${format}returnaddress", $template)
- );
-
- } elsif ( grep /\S/,
- $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
-
- my $convert_map = $convert_maps{$format}{'returnaddress'};
- $returnaddress =
- join( "\n",
- &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
- $template
- )
- )
- );
- } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
-
- my $convert_map = $convert_maps{$format}{'returnaddress'};
- $returnaddress = join( "\n", &$convert_map(
- map { s/( {2,})/'~' x length($1)/eg;
- s/$/\\\\\*/;
- $_
- }
- ( $conf->config('company_name', $self->cust_main->agentnum),
- $conf->config('company_address', $self->cust_main->agentnum),
- )
- )
- );
-
- } else {
-
- my $warning = "Couldn't find a return address; ".
- "do you need to set the company_address configuration value?";
- warn "$warning\n";
- $returnaddress = $nbsp;
- #$returnaddress = $warning;
-
- }
-
- warn "$me generating invoice data\n"
- if $DEBUG > 1;
-
- my $agentnum = $self->cust_main->agentnum;
-
- my %invoice_data = (
-
- #invoice from info
- 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
- 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
- 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
- 'returnaddress' => $returnaddress,
- 'agent' => &$escape_function($cust_main->agent->agent),
-
- #invoice info
- 'invnum' => $self->invnum,
- 'date' => time2str($date_format, $self->_date),
- 'today' => time2str($date_format_long, $today),
- 'terms' => $self->terms,
- 'template' => $template, #params{'template'},
- 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
- 'current_charges' => sprintf("%.2f", $self->charged),
- 'duedate' => $self->due_date2str($rdate_format), #date_format?
-
- #customer info
- 'custnum' => $cust_main->display_custnum,
- 'agent_custid' => &$escape_function($cust_main->agent_custid),
- ( map { $_ => &$escape_function($cust_main->$_()) } qw(
- payname company address1 address2 city state zip fax
- )),
-
- #global config
- 'ship_enable' => $conf->exists('invoice-ship_address'),
- 'unitprices' => $conf->exists('invoice-unitprice'),
- 'smallernotes' => $conf->exists('invoice-smallernotes'),
- 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
- 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
-
- #layout info -- would be fancy to calc some of this and bury the template
- # here in the code
- 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
- 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
- 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
- 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
- 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
- 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
- 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
- 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
- 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
- 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
-
- # better hang on to conf_dir for a while (for old templates)
- 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
-
- #these are only used when doing paged plaintext
- 'page' => 1,
- 'total_pages' => 1,
-
- );
-
- #localization
- my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
- $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
- my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
- # eval to avoid death for unimplemented languages
- my $dh = eval { Date::Language->new($info{'name'}) } ||
- Date::Language->new(); # fall back to English
- # prototype here to silence warnings
- $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
- # eventually use this date handle everywhere in here, too
-
- my $min_sdate = 999999999999;
- my $max_edate = 0;
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
- next unless $cust_bill_pkg->pkgnum > 0;
- $min_sdate = $cust_bill_pkg->sdate
- if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
- $max_edate = $cust_bill_pkg->edate
- if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
- }
-
- $invoice_data{'bill_period'} = '';
- $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
- . " to " . time2str('%e %h', $max_edate)
- if ($max_edate != 0 && $min_sdate != 999999999999);
-
- $invoice_data{finance_section} = '';
- if ( $conf->config('finance_pkgclass') ) {
- my $pkg_class =
- qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
- $invoice_data{finance_section} = $pkg_class->categoryname;
- }
- $invoice_data{finance_amount} = '0.00';
- $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
-
- my $countrydefault = $conf->config('countrydefault') || 'US';
- foreach ( qw( address1 address2 city state zip country fax) ){
- my $method = 'ship_'.$_;
- $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
- }
- foreach ( qw( contact company ) ) { #compatibility
- $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
- }
- $invoice_data{'ship_country'} = ''
- if ( $invoice_data{'ship_country'} eq $countrydefault );
-
- $invoice_data{'cid'} = $params{'cid'}
- if $params{'cid'};
-
- if ( $cust_main->country eq $countrydefault ) {
- $invoice_data{'country'} = '';
- } else {
- $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
- }
-
- my @address = ();
- $invoice_data{'address'} = \@address;
- push @address,
- $cust_main->payname.
- ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
- ? " (P.O. #". $cust_main->payinfo. ")"
- : ''
- )
- ;
- push @address, $cust_main->company
- if $cust_main->company;
- push @address, $cust_main->address1;
- push @address, $cust_main->address2
- if $cust_main->address2;
- push @address,
- $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
- push @address, $invoice_data{'country'}
- if $invoice_data{'country'};
- push @address, ''
- while (scalar(@address) < 5);
-
- $invoice_data{'logo_file'} = $params{'logo_file'}
- if $params{'logo_file'};
- $invoice_data{'barcode_file'} = $params{'barcode_file'}
- if $params{'barcode_file'};
- $invoice_data{'barcode_img'} = $params{'barcode_img'}
- if $params{'barcode_img'};
- $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
- if $params{'barcode_cid'};
-
- my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
- #my $balance_due = $self->owed + $pr_total - $cr_total;
- my $balance_due = $self->owed + $pr_total;
-
- # the customer's current balance as shown on the invoice before this one
- $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
-
- # the change in balance from that invoice to this one
- $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
-
- # the sum of amount owed on all previous invoices
- $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
-
- # the sum of amount owed on all invoices
- $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
-
- # info from customer's last invoice before this one, for some
- # summary formats
- $invoice_data{'last_bill'} = {};
- my $last_bill = $pr_cust_bill[-1];
- if ( $last_bill ) {
- $invoice_data{'last_bill'} = {
- '_date' => $last_bill->_date, #unformatted
- # all we need for now
- };
- }
-
- my $summarypage = '';
- if ( $conf->exists('invoice_usesummary', $agentnum) ) {
- $summarypage = 1;
- }
- $invoice_data{'summarypage'} = $summarypage;
-
- warn "$me substituting variables in notes, footer, smallfooter\n"
- if $DEBUG > 1;
-
- my @include = (qw( notes footer smallfooter ));
- push @include, 'coupon' unless $params{'no_coupon'};
- foreach my $include (@include) {
-
- my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
- my @inc_src;
-
- if ( $conf->exists($inc_file, $agentnum)
- && length( $conf->config($inc_file, $agentnum) ) ) {
-
- @inc_src = $conf->config($inc_file, $agentnum);
-
- } else {
-
- $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
- my $convert_map = $convert_maps{$format}{$include};
-
- @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
- s/--\@\]/$delimiters{$format}[1]/g;
- $_;
- }
- &$convert_map( $conf->config($inc_file, $agentnum) );
-
- }
-
- my $inc_tt = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @inc_src ],
- DELIMITERS => $delimiters{$format},
- ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
-
- unless ( $inc_tt->compile() ) {
- my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
- warn $error. "Template:\n". join('', map "$_\n", @inc_src);
- die $error;
- }
-
- $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
-
- $invoice_data{$include} =~ s/\n+$//
- if ($format eq 'latex');
- }
-
- # let invoices use either of these as needed
- $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
- ? $cust_main->payinfo : '';
- $invoice_data{'po_line'} =
- ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
- ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
- : $nbsp;
-
- my %money_chars = ( 'latex' => '',
- 'html' => $conf->config('money_char') || '$',
- 'template' => '',
- );
- my $money_char = $money_chars{$format};
-
- my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
- 'html' => $conf->config('money_char') || '$',
- 'template' => '',
- );
- my $other_money_char = $other_money_chars{$format};
- $invoice_data{'dollar'} = $other_money_char;
-
- my @detail_items = ();
- my @total_items = ();
- my @buf = ();
- my @sections = ();
-
- $invoice_data{'detail_items'} = \@detail_items;
- $invoice_data{'total_items'} = \@total_items;
- $invoice_data{'buf'} = \@buf;
- $invoice_data{'sections'} = \@sections;
-
- warn "$me generating sections\n"
- if $DEBUG > 1;
-
- my $previous_section = { 'description' => $self->mt('Previous Charges'),
- 'subtotal' => $other_money_char.
- sprintf('%.2f', $pr_total),
- 'summarized' => '', #why? $summarypage ? 'Y' : '',
- };
- $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
- join(' / ', map { $cust_main->balance_date_range(@$_) }
- $self->_prior_month30s
- )
- if $conf->exists('invoice_include_aging');
-
- my $taxtotal = 0;
- my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
- 'subtotal' => $taxtotal, # adjusted below
- };
- my $tax_weight = _pkg_category($tax_section->{description})
- ? _pkg_category($tax_section->{description})->weight
- : 0;
- $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
- $tax_section->{'sort_weight'} = $tax_weight;
-
-
- my $adjusttotal = 0;
- my $adjust_section = { 'description' =>
- $self->mt('Credits, Payments, and Adjustments'),
- 'subtotal' => 0, # adjusted below
- };
- my $adjust_weight = _pkg_category($adjust_section->{description})
- ? _pkg_category($adjust_section->{description})->weight
- : 0;
- $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
- $adjust_section->{'sort_weight'} = $adjust_weight;
-
- my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
- my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
- $invoice_data{'multisection'} = $multisection;
- my $late_sections = [];
- my $extra_sections = [];
- my $extra_lines = ();
-
- my $default_section = { 'description' => '',
- 'subtotal' => '',
- 'no_subtotal' => 1,
- };
-
- if ( $multisection ) {
- ($extra_sections, $extra_lines) =
- $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
- if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
-
- push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
-
- push @detail_items, @$extra_lines if $extra_lines;
- push @sections,
- $self->_items_sections( $late_sections, # this could stand a refactor
- $summarypage,
- $escape_function_nonbsp,
- $extra_sections,
- $format, #bah
- );
- if ($conf->exists('svc_phone_sections')) {
- my ($phone_sections, $phone_lines) =
- $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
- push @{$late_sections}, @$phone_sections;
- push @detail_items, @$phone_lines;
- }
- if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
- my ($accountcode_section, $accountcode_lines) =
- $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
- if ( scalar(@$accountcode_lines) ) {
- push @{$late_sections}, $accountcode_section;
- push @detail_items, @$accountcode_lines;
- }
- }
- } else {# not multisection
- # make a default section
- push @sections, $default_section;
- # and calculate the finance charge total, since it won't get done otherwise.
- # XXX possibly other totals?
- # XXX possibly finance_pkgclass should not be used in this manner?
- if ( $conf->exists('finance_pkgclass') ) {
- my @finance_charges;
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
- if ( grep { $_->section eq $invoice_data{finance_section} }
- $cust_bill_pkg->cust_bill_pkg_display ) {
- # I think these are always setup fees, but just to be sure...
- push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
- }
- }
- $invoice_data{finance_amount} =
- sprintf('%.2f', sum( @finance_charges ) || 0);
- }
- }
-
- unless ( $conf->exists('disable_previous_balance', $agentnum)
- || $conf->exists('previous_balance-summary_only')
- )
- {
-
- warn "$me adding previous balances\n"
- if $DEBUG > 1;
-
- foreach my $line_item ( $self->_items_previous ) {
-
- my $detail = {
- ext_description => [],
- };
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'quantity'} = 1;
- $detail->{'section'} = $multisection ? $previous_section
- : $default_section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = map {
- &$escape_function($_);
- } @{$line_item->{'ext_description'}};
- }
- $detail->{'amount'} = ( $old_latex ? '' : $money_char).
- $line_item->{'amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
- push @detail_items, $detail;
- push @buf, [ $detail->{'description'},
- $money_char. sprintf("%10.2f", $line_item->{'amount'}),
- ];
- }
-
- }
-
- if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
- {
- push @buf, ['','-----------'];
- push @buf, [ $self->mt('Total Previous Balance'),
- $money_char. sprintf("%10.2f", $pr_total) ];
- push @buf, ['',''];
- }
-
- if ( $conf->exists('svc_phone-did-summary') ) {
- warn "$me adding DID summary\n"
- if $DEBUG > 1;
-
- my ($didsummary,$minutes) = $self->_did_summary;
- my $didsummary_desc = 'DID Activity Summary (since last invoice)';
- push @detail_items,
- { 'description' => $didsummary_desc,
- 'ext_description' => [ $didsummary, $minutes ],
- };
- }
-
- foreach my $section (@sections, @$late_sections) {
-
- warn "$me adding section \n". Dumper($section)
- if $DEBUG > 1;
-
- # begin some normalization
- $section->{'subtotal'} = $section->{'amount'}
- if $multisection
- && !exists($section->{subtotal})
- && exists($section->{amount});
-
- $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
- if ( $invoice_data{finance_section} &&
- $section->{'description'} eq $invoice_data{finance_section} );
-
- $section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $section->{'subtotal'})
- if $multisection;
-
- # continue some normalization
- $section->{'amount'} = $section->{'subtotal'}
- if $multisection;
-
-
- if ( $section->{'description'} ) {
- push @buf, ( [ &$escape_function($section->{'description'}), '' ],
- [ '', '' ],
- );
- }
-
- warn "$me setting options\n"
- if $DEBUG > 1;
-
- my $multilocation = scalar($cust_main->cust_location); #too expensive?
- my %options = ();
- $options{'section'} = $section if $multisection;
- $options{'format'} = $format;
- $options{'escape_function'} = $escape_function;
- $options{'no_usage'} = 1 unless $unsquelched;
- $options{'unsquelched'} = $unsquelched;
- $options{'summary_page'} = $summarypage;
- $options{'skip_usage'} =
- scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
- $options{'multilocation'} = $multilocation;
- $options{'multisection'} = $multisection;
-
- warn "$me searching for line items\n"
- if $DEBUG > 1;
-
- foreach my $line_item ( $self->_items_pkg(%options) ) {
-
- warn "$me adding line item $line_item\n"
- if $DEBUG > 1;
-
- my $detail = {
- ext_description => [],
- };
- $detail->{'ref'} = $line_item->{'pkgnum'};
- $detail->{'quantity'} = $line_item->{'quantity'};
- $detail->{'section'} = $section;
- $detail->{'description'} = &$escape_function($line_item->{'description'});
- if ( exists $line_item->{'ext_description'} ) {
- @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
- }
- $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
- $line_item->{'amount'};
- $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
- $line_item->{'unit_amount'};
- $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
- $detail->{'sdate'} = $line_item->{'sdate'};
- $detail->{'edate'} = $line_item->{'edate'};
- $detail->{'seconds'} = $line_item->{'seconds'};
-
- push @detail_items, $detail;
- push @buf, ( [ $detail->{'description'},
- $money_char. sprintf("%10.2f", $line_item->{'amount'}),
- ],
- map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
- );
- }
-
- if ( $section->{'description'} ) {
- push @buf, ( ['','-----------'],
- [ $section->{'description'}. ' sub-total',
- $section->{'subtotal'} # already formatted this
- ],
- [ '', '' ],
- [ '', '' ],
- );
- }
-
- }
-
- $invoice_data{current_less_finance} =
- sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
-
- if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
- || $conf->exists('previous_balance-summary_only') )
- {
- unshift @sections, $previous_section if $pr_total;
- }
-
- warn "$me adding taxes\n"
- if $DEBUG > 1;
-
- foreach my $tax ( $self->_items_tax ) {
-
- $taxtotal += $tax->{'amount'};
-
- my $description = &$escape_function( $tax->{'description'} );
- my $amount = sprintf( '%.2f', $tax->{'amount'} );
-
- if ( $multisection ) {
-
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => $description,
- amount => $money. $amount,
- product_code => '',
- section => $tax_section,
- };
-
- } else {
-
- push @total_items, {
- 'total_item' => $description,
- 'total_amount' => $other_money_char. $amount,
- };
-
- }
-
- push @buf,[ $description,
- $money_char. $amount,
- ];
-
- }
-
- if ( $taxtotal ) {
- my $total = {};
- $total->{'total_item'} = $self->mt('Sub-total');
- $total->{'total_amount'} =
- $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-
- if ( $multisection ) {
- $tax_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $taxtotal);
- $tax_section->{'pretotal'} = 'New charges sub-total '.
- $total->{'total_amount'};
- push @sections, $tax_section if $taxtotal;
- }else{
- unshift @total_items, $total;
- }
- }
- $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
-
- push @buf,['','-----------'];
- push @buf,[$self->mt(
- $conf->exists('disable_previous_balance', $agentnum)
- ? 'Total Charges'
- : 'Total New Charges'
- ),
- $money_char. sprintf("%10.2f",$self->charged) ];
- push @buf,['',''];
-
- {
- my $total = {};
- my $item = 'Total';
- $item = $conf->config('previous_balance-exclude_from_total')
- || 'Total New Charges'
- if $conf->exists('previous_balance-exclude_from_total');
- my $amount = $self->charged +
- ( $conf->exists('disable_previous_balance', $agentnum) ||
- $conf->exists('previous_balance-exclude_from_total')
- ? 0
- : $pr_total
- );
- $total->{'total_item'} = &$embolden_function($self->mt($item));
- $total->{'total_amount'} =
- &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
- if ( $multisection ) {
- if ( $adjust_section->{'sort_weight'} ) {
- $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
- $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
- } else {
- $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
- $other_money_char. sprintf('%.2f', $self->charged );
- }
- }else{
- push @total_items, $total;
- }
- push @buf,['','-----------'];
- push @buf,[$item,
- $money_char.
- sprintf( '%10.2f', $amount )
- ];
- push @buf,['',''];
- }
-
- unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
- #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-
- # credits
- my $credittotal = 0;
- foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
-
- my $total;
- $total->{'total_item'} = &$escape_function($credit->{'description'});
- $credittotal += $credit->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
- $adjusttotal += $credit->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($credit->{'description'}),
- amount => $money. $credit->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- } else {
- push @total_items, $total;
- }
-
- }
- $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
- #credits (again)
- foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
- push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
- }
-
- # payments
- my $paymenttotal = 0;
- foreach my $payment ( $self->_items_payments ) {
- my $total = {};
- $total->{'total_item'} = &$escape_function($payment->{'description'});
- $paymenttotal += $payment->{'amount'};
- $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
- $adjusttotal += $payment->{'amount'};
- if ( $multisection ) {
- my $money = $old_latex ? '' : $money_char;
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => &$escape_function($payment->{'description'}),
- amount => $money. $payment->{'amount'},
- product_code => '',
- section => $adjust_section,
- };
- }else{
- push @total_items, $total;
- }
- push @buf, [ $payment->{'description'},
- $money_char. sprintf("%10.2f", $payment->{'amount'}),
- ];
- }
- $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-
- if ( $multisection ) {
- $adjust_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $adjusttotal);
- push @sections, $adjust_section
- unless $adjust_section->{sort_weight};
- }
-
- # create Balance Due message
- {
- my $total;
- $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char. sprintf('%.2f', $summarypage
- ? $self->charged +
- $self->billing_balance
- : $self->owed + $pr_total
- )
- );
- if ( $multisection && !$adjust_section->{sort_weight} ) {
- $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
- $total->{'total_amount'};
- }else{
- push @total_items, $total;
- }
- push @buf,['','-----------'];
- push @buf,[$self->balance_due_msg, $money_char.
- sprintf("%10.2f", $balance_due ) ];
- }
-
- if ( $conf->exists('previous_balance-show_credit')
- and $cust_main->balance < 0 ) {
- my $credit_total = {
- 'total_item' => &$embolden_function($self->credit_balance_msg),
- 'total_amount' => &$embolden_function(
- $other_money_char. sprintf('%.2f', -$cust_main->balance)
- ),
- };
- if ( $multisection ) {
- $adjust_section->{'posttotal'} .= $newline_token .
- $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
- }
- else {
- push @total_items, $credit_total;
- }
- push @buf,['','-----------'];
- push @buf,[$self->credit_balance_msg, $money_char.
- sprintf("%10.2f", -$cust_main->balance ) ];
- }
- }
-
- if ( $multisection ) {
- if ($conf->exists('svc_phone_sections')) {
- my $total;
- $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
- $total->{'total_amount'} =
- &$embolden_function(
- $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
- );
- my $last_section = pop @sections;
- $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
- $total->{'total_amount'};
- push @sections, $last_section;
- }
- push @sections, @$late_sections
- if $unsquelched;
- }
-
- # make a discounts-available section, even without multisection
- if ( $conf->exists('discount-show_available')
- and my @discounts_avail = $self->_items_discounts_avail ) {
- my $discount_section = {
- 'description' => $self->mt('Discounts Available'),
- 'subtotal' => '',
- 'no_subtotal' => 1,
- };
-
- push @sections, $discount_section;
- push @detail_items, map { +{
- 'ref' => '', #should this be something else?
- 'section' => $discount_section,
- 'description' => &$escape_function( $_->{description} ),
- 'amount' => $money_char . &$escape_function( $_->{amount} ),
- 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
- } } @discounts_avail;
- }
-
- # All sections and items are built; now fill in templates.
- my @includelist = ();
- push @includelist, 'summary' if $summarypage;
- foreach my $include ( @includelist ) {
-
- my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
- my @inc_src;
-
- if ( length( $conf->config($inc_file, $agentnum) ) ) {
-
- @inc_src = $conf->config($inc_file, $agentnum);
-
- } else {
-
- $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
- my $convert_map = $convert_maps{$format}{$include};
-
- @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
- s/--\@\]/$delimiters{$format}[1]/g;
- $_;
- }
- &$convert_map( $conf->config($inc_file, $agentnum) );
-
- }
-
- my $inc_tt = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @inc_src ],
- DELIMITERS => $delimiters{$format},
- ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
-
- unless ( $inc_tt->compile() ) {
- my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
- warn $error. "Template:\n". join('', map "$_\n", @inc_src);
- die $error;
- }
-
- $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
-
- $invoice_data{$include} =~ s/\n+$//
- if ($format eq 'latex');
- }
-
- $invoice_lines = 0;
- my $wasfunc = 0;
- foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
- /invoice_lines\((\d*)\)/;
- $invoice_lines += $1 || scalar(@buf);
- $wasfunc=1;
- }
- die "no invoice_lines() functions in template?"
- if ( $format eq 'template' && !$wasfunc );
-
- if ($format eq 'template') {
-
- if ( $invoice_lines ) {
- $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
- $invoice_data{'total_pages'}++
- if scalar(@buf) % $invoice_lines;
- }
-
- #setup subroutine for the template
- $invoice_data{invoice_lines} = sub {
- my $lines = shift || scalar(@buf);
- map {
- scalar(@buf)
- ? shift @buf
- : [ '', '' ];
- }
- ( 1 .. $lines );
- };
-
- my $lines;
- my @collect;
- while (@buf) {
- push @collect, split("\n",
- $text_template->fill_in( HASH => \%invoice_data )
- );
- $invoice_data{'page'}++;
- }
- map "$_\n", @collect;
- }else{
- # this is where we actually create the invoice
- warn "filling in template for invoice ". $self->invnum. "\n"
- if $DEBUG;
- warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
- if $DEBUG > 1;
-
- $text_template->fill_in(HASH => \%invoice_data);
- }
-}
-
-# helper routine for generating date ranges
-sub _prior_month30s {
- my $self = shift;
- my @ranges = (
- [ 1, 2592000 ], # 0-30 days ago
- [ 2592000, 5184000 ], # 30-60 days ago
- [ 5184000, 7776000 ], # 60-90 days ago
- [ 7776000, 0 ], # 90+ days ago
- );
-
- map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
- $_->[1] ? $self->_date - $_->[1] - 1 : '',
- ] }
- @ranges;
-}
-
-=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an postscript invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<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.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_ps {
- my $self = shift;
-
- my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
- my $ps = generate_ps($file);
- unlink($logofile);
- unlink($barcodefile) if $barcodefile;
-
- $ps;
-}
-
-=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an PDF invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<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.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_pdf {
- my $self = shift;
-
- my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
- my $pdf = generate_pdf($file);
- unlink($logofile);
- unlink($barcodefile) if $barcodefile;
-
- $pdf;
-}
-
-=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
-
-Returns an HTML invoice, as a scalar.
-
-I<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.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
-when emailing the invoice as part of a multipart/related MIME email.
-
-=cut
-
-sub print_html {
- my $self = shift;
- my %params;
- if ( ref($_[0]) ) {
- %params = %{ shift() };
- }else{
- $params{'time'} = shift;
- $params{'template'} = shift;
- $params{'cid'} = shift;
- }
-
- $params{'format'} = 'html';
-
- $self->print_generic( %params );
-}
-
-# quick subroutine for print_latex
-#
-# There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves:
-# # $ % & ~ _ ^ \ { }
-#
-# TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
-
-sub _latex_escape {
- my $value = shift;
- $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
- $value =~ s/([<>])/\$$1\$/g;
- $value;
-}
-
-sub _html_escape {
- my $value = shift;
- encode_entities($value);
- $value;
-}
-
-sub _html_escape_nbsp {
- my $value = _html_escape(shift);
- $value =~ s/ +/&nbsp;/g;
- $value;
-}
-
-#utility methods for print_*
-
-sub _translate_old_latex_format {
- warn "_translate_old_latex_format called\n"
- if $DEBUG;
-
- my @template = ();
- while ( @_ ) {
- my $line = shift;
-
- if ( $line =~ /^%%Detail\s*$/ ) {
-
- push @template, q![@--!,
- q! foreach my $_tr_line (@detail_items) {!,
- q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
- q! $_tr_line->{'description'} .= !,
- q! "\\tabularnewline\n~~".!,
- q! join( "\\tabularnewline\n~~",!,
- q! @{$_tr_line->{'ext_description'}}!,
- q! );!,
- q! }!;
-
- while ( ( my $line_item_line = shift )
- !~ /^%%EndDetail\s*$/ ) {
- $line_item_line =~ s/'/\\'/g; # nice LTS
- $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
- $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
- push @template, " \$OUT .= '$line_item_line';";
- }
-
- push @template, '}',
- '--@]';
- #' doh, gvim
- } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
-
- push @template, '[@--',
- ' foreach my $_tr_line (@total_items) {';
-
- while ( ( my $total_item_line = shift )
- !~ /^%%EndTotalDetails\s*$/ ) {
- $total_item_line =~ s/'/\\'/g; # nice LTS
- $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
- $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
- push @template, " \$OUT .= '$total_item_line';";
- }
-
- push @template, '}',
- '--@]';
-
- } else {
- $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
- push @template, $line;
- }
-
- }
-
- if ($DEBUG) {
- warn "$_\n" foreach @template;
- }
-
- (@template);
-}
-
-sub terms {
- my $self = shift;
- my $conf = $self->conf;
-
- #check for an invoice-specific override
- return $self->invoice_terms if $self->invoice_terms;
-
- #check for a customer- specific override
- my $cust_main = $self->cust_main;
- return $cust_main->invoice_terms if $cust_main->invoice_terms;
-
- #use configured default
- $conf->config('invoice_default_terms') || '';
-}
-
-sub due_date {
- my $self = shift;
- my $duedate = '';
- if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = $self->_date() + ( $1 * 86400 );
- }
- $duedate;
-}
-
-sub due_date2str {
- my $self = shift;
- $self->due_date ? time2str(shift, $self->due_date) : '';
-}
-
-sub balance_due_msg {
- my $self = shift;
- my $msg = $self->mt('Balance Due');
- return $msg unless $self->terms;
- if ( $self->due_date ) {
- $msg .= ' - ' . $self->mt('Please pay by'). ' '.
- $self->due_date2str($date_format);
- } elsif ( $self->terms ) {
- $msg .= ' - '. $self->terms;
- }
- $msg;
-}
-
-sub balance_due_date {
- my $self = shift;
- my $conf = $self->conf;
- my $duedate = '';
- if ( $conf->exists('invoice_default_terms')
- && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
- $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
- }
- $duedate;
-}
-
-sub credit_balance_msg {
- my $self = shift;
- $self->mt('Credit Balance Remaining')
-}
-
=item invnum_date_pretty
Returns a string with the invoice number and date, for example:
@@ -3870,426 +2356,6 @@ sub invnum_date_pretty {
$self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
}
-=item _date_pretty
-
-Returns a string with the date, for example: "3/20/2008"
-
-=cut
-
-sub _date_pretty {
- my $self = shift;
- time2str($date_format, $self->_date);
-}
-
-=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
-
-Generate section information for all items appearing on this invoice.
-This will only be called for multi-section invoices.
-
-For each line item (L<FS::cust_bill_pkg> record), this will fetch all
-related display records (L<FS::cust_bill_pkg_display>) and organize
-them into two groups ("early" and "late" according to whether they come
-before or after the total), then into sections. A subtotal is calculated
-for each section.
-
-Section descriptions are returned in sort weight order. Each consists
-of a hash containing:
-
-description: the package category name, escaped
-subtotal: the total charges in that section
-tax_section: a flag indicating that the section contains only tax charges
-summarized: same as tax_section, for some reason
-sort_weight: the package category's sort weight
-
-If 'condense' is set on the display record, it also contains everything
-returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
-coderefs to generate parts of the invoice. This is not advised.
-
-Arguments:
-
-LATE: an arrayref to push the "late" section hashes onto. The "early"
-group is simply returned from the method.
-
-SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
-Turning this on has the following effects:
-- Ignores display items with the 'summary' flag.
-- Combines all items into the "early" group.
-- Creates sections for all non-disabled package categories, even if they
-have no charges on this invoice, as well as a section with no name.
-
-ESCAPE: an escape function to use for section titles.
-
-EXTRA_SECTIONS: an arrayref of additional sections to return after the
-sorted list. If there are any of these, section subtotals exclude
-usage charges.
-
-FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
-passed through to C<_condense_section()>.
-
-=cut
-
-use vars qw(%pkg_category_cache);
-sub _items_sections {
- my $self = shift;
- my $late = shift;
- my $summarypage = shift;
- my $escape = shift;
- my $extra_sections = shift;
- my $format = shift;
-
- my %subtotal = ();
- my %late_subtotal = ();
- my %not_tax = ();
-
- foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
- {
-
- my $usage = $cust_bill_pkg->usage;
-
- foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
- next if ( $display->summary && $summarypage );
-
- my $section = $display->section;
- my $type = $display->type;
-
- $not_tax{$section} = 1
- unless $cust_bill_pkg->pkgnum == 0;
-
- if ( $display->post_total && !$summarypage ) {
- if (! $type || $type eq 'S') {
- $late_subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0
- || $cust_bill_pkg->setup_show_zero;
- }
-
- if (! $type) {
- $late_subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0
- || $cust_bill_pkg->recur_show_zero;
- }
-
- if ($type && $type eq 'R') {
- $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0
- || $cust_bill_pkg->recur_show_zero;
- }
-
- if ($type && $type eq 'U') {
- $late_subtotal{$section} += $usage
- unless scalar(@$extra_sections);
- }
-
- } else {
-
- next if $cust_bill_pkg->pkgnum == 0 && ! $section;
-
- if (! $type || $type eq 'S') {
- $subtotal{$section} += $cust_bill_pkg->setup
- if $cust_bill_pkg->setup != 0
- || $cust_bill_pkg->setup_show_zero;
- }
-
- if (! $type) {
- $subtotal{$section} += $cust_bill_pkg->recur
- if $cust_bill_pkg->recur != 0
- || $cust_bill_pkg->recur_show_zero;
- }
-
- if ($type && $type eq 'R') {
- $subtotal{$section} += $cust_bill_pkg->recur - $usage
- if $cust_bill_pkg->recur != 0
- || $cust_bill_pkg->recur_show_zero;
- }
-
- if ($type && $type eq 'U') {
- $subtotal{$section} += $usage
- unless scalar(@$extra_sections);
- }
-
- }
-
- }
-
- }
-
- %pkg_category_cache = ();
-
- push @$late, map { { 'description' => &{$escape}($_),
- 'subtotal' => $late_subtotal{$_},
- 'post_total' => 1,
- 'sort_weight' => ( _pkg_category($_)
- ? _pkg_category($_)->weight
- : 0
- ),
- ((_pkg_category($_) && _pkg_category($_)->condense)
- ? $self->_condense_section($format)
- : ()
- ),
- } }
- sort _sectionsort keys %late_subtotal;
-
- my @sections;
- if ( $summarypage ) {
- @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
- map { $_->categoryname } qsearch('pkg_category', {});
- push @sections, '' if exists($subtotal{''});
- } else {
- @sections = keys %subtotal;
- }
-
- my @early = map { { 'description' => &{$escape}($_),
- 'subtotal' => $subtotal{$_},
- 'summarized' => $not_tax{$_} ? '' : 'Y',
- 'tax_section' => $not_tax{$_} ? '' : 'Y',
- 'sort_weight' => ( _pkg_category($_)
- ? _pkg_category($_)->weight
- : 0
- ),
- ((_pkg_category($_) && _pkg_category($_)->condense)
- ? $self->_condense_section($format)
- : ()
- ),
- }
- } @sections;
- push @early, @$extra_sections if $extra_sections;
-
- sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
-
-}
-
-#helper subs for above
-
-sub _sectionsort {
- _pkg_category($a)->weight <=> _pkg_category($b)->weight;
-}
-
-sub _pkg_category {
- my $categoryname = shift;
- $pkg_category_cache{$categoryname} ||=
- qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
-}
-
-my %condensed_format = (
- 'label' => [ qw( Description Qty Amount ) ],
- 'fields' => [
- sub { shift->{description} },
- sub { shift->{quantity} },
- sub { my($href, %opt) = @_;
- ($opt{dollar} || ''). $href->{amount};
- },
- ],
- 'align' => [ qw( l r r ) ],
- 'span' => [ qw( 5 1 1 ) ], # unitprices?
- 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
-);
-
-sub _condense_section {
- my ( $self, $format ) = ( shift, shift );
- ( 'condensed' => 1,
- map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
- qw( description_generator
- header_generator
- total_generator
- total_line_generator
- )
- );
-}
-
-sub _condensed_generator_defaults {
- my ( $self, $format ) = ( shift, shift );
- return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
-}
-
-my %html_align = (
- 'c' => 'center',
- 'l' => 'left',
- 'r' => 'right',
-);
-
-sub _condensed_header_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
-
- if ($format eq 'latex') {
- $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
- $suffix = "\\\\\n\\hline";
- $separator = "&\n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
- };
- } elsif ( $format eq 'html' ) {
- $prefix = '<th></th>';
- $suffix = '';
- $separator = '';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<th align="$html_align{$a}">$d</th>!;
- };
- }
-
- sub {
- my @args = @_;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
- }
-
- $prefix. join($separator, @result). $suffix;
- };
-
-}
-
-sub _condensed_description_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
-
- my $money_char = '$';
- if ($format eq 'latex') {
- $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
- $suffix = '\\\\';
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
- };
- $money_char = '\\dollar';
- }elsif ( $format eq 'html' ) {
- $prefix = '"><td align="center"></td>';
- $suffix = '';
- $separator = '';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}">$d</td>!;
- };
- #$money_char = $conf->config('money_char') || '$';
- $money_char = ''; # this is madness
- }
-
- sub {
- #my @args = @_;
- my $href = shift;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- my $dollar = '';
- $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
- push @result,
- &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
- map { $f->{$_}->[$i] } qw(align span width)
- );
- }
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
-}
-
-sub _condensed_total_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
- my $style = '';
-
- if ($format eq 'latex') {
- $prefix = "& ";
- $suffix = "\\\\\n";
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
- };
- }elsif ( $format eq 'html' ) {
- $prefix = '';
- $suffix = '';
- $separator = '';
- $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
- };
- }
-
-
- sub {
- my @args = @_;
- my @result = ();
-
- # my $r = &{$f->{fields}->[$i]}(@args);
- # $r .= ' Total' unless $i;
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
- map { $f->{$_}->[$i] } qw(align span width)
- );
- }
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
-}
-
-=item total_line_generator FORMAT
-
-Returns a coderef used for generation of invoice total line items for this
-usage_class. FORMAT is either html or latex
-
-=cut
-
-# should not be used: will have issues with hash element names (description vs
-# total_item and amount vs total_amount -- another array of functions?
-
-sub _condensed_total_line_generator {
- my ( $self, $format ) = ( shift, shift );
-
- my ( $f, $prefix, $suffix, $separator, $column ) =
- _condensed_generator_defaults($format);
- my $style = '';
-
- if ($format eq 'latex') {
- $prefix = "& ";
- $suffix = "\\\\\n";
- $separator = " & \n";
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
- };
- }elsif ( $format eq 'html' ) {
- $prefix = '';
- $suffix = '';
- $separator = '';
- $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
- $column =
- sub { my ($d,$a,$s,$w) = @_;
- return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
- };
- }
-
-
- sub {
- my @args = @_;
- my @result = ();
-
- foreach (my $i = 0; $f->{label}->[$i]; $i++) {
- push @result,
- &{$column}( &{$f->{fields}->[$i]}(@args),
- map { $f->{$_}->[$i] } qw(align span width)
- );
- }
-
- $prefix. join( $separator, @result ). $suffix;
- };
-
-}
-
#sub _items_extra_usage_sections {
# my $self = shift;
# my $escape = shift;
@@ -4795,23 +2861,6 @@ sub _items_svc_phone_sections {
}
-sub _items { # seems to be unused
- my $self = shift;
-
- #my @display = scalar(@_)
- # ? @_
- # : qw( _items_previous _items_pkg );
- # #: qw( _items_pkg );
- # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
- my @display = qw( _items_previous _items_pkg );
-
- my @b = ();
- foreach my $display ( @display ) {
- push @b, $self->$display(@_);
- }
- @b;
-}
-
sub _items_previous {
my $self = shift;
my $conf = $self->conf;
@@ -4845,475 +2894,6 @@ sub _items_previous {
#};
}
-=item _items_pkg [ OPTIONS ]
-
-Return line item hashes for each package item on this invoice. Nearly
-equivalent to
-
-$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
-
-The only OPTIONS accepted is 'section', which may point to a hashref
-with a key named 'condensed', which may have a true value. If it
-does, this method tries to merge identical items into items with
-'quantity' equal to the number of items (not the sum of their
-separate quantities, for some reason).
-
-=cut
-
-sub _items_pkg {
- my $self = shift;
- my %options = @_;
-
- warn "$me _items_pkg searching for all package line items\n"
- if $DEBUG > 1;
-
- my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
-
- warn "$me _items_pkg filtering line items\n"
- if $DEBUG > 1;
- my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-
- if ($options{section} && $options{section}->{condensed}) {
-
- warn "$me _items_pkg condensing section\n"
- if $DEBUG > 1;
-
- my %itemshash = ();
- local $Storable::canonical = 1;
- foreach ( @items ) {
- my $item = { %$_ };
- delete $item->{ref};
- delete $item->{ext_description};
- my $key = freeze($item);
- $itemshash{$key} ||= 0;
- $itemshash{$key} ++; # += $item->{quantity};
- }
- @items = sort { $a->{description} cmp $b->{description} }
- map { my $i = thaw($_);
- $i->{quantity} = $itemshash{$_};
- $i->{amount} =
- sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
- $i;
- }
- keys %itemshash;
- }
-
- warn "$me _items_pkg returning ". scalar(@items). " items\n"
- if $DEBUG > 1;
-
- @items;
-}
-
-sub _taxsort {
- return 0 unless $a->itemdesc cmp $b->itemdesc;
- return -1 if $b->itemdesc eq 'Tax';
- return 1 if $a->itemdesc eq 'Tax';
- return -1 if $b->itemdesc eq 'Other surcharges';
- return 1 if $a->itemdesc eq 'Other surcharges';
- $a->itemdesc cmp $b->itemdesc;
-}
-
-sub _items_tax {
- my $self = shift;
- my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
- $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-}
-
-=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
-
-Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
-list of hashrefs describing the line items they generate on the invoice.
-
-OPTIONS may include:
-
-format: the invoice format.
-
-escape_function: the function used to escape strings.
-
-DEPRECATED? (expensive, mostly unused?)
-format_function: the function used to format CDRs.
-
-section: a hashref containing 'description'; if this is present,
-cust_bill_pkg_display records not belonging to this section are
-ignored.
-
-multisection: a flag indicating that this is a multisection invoice,
-which does something complicated.
-
-multilocation: a flag to display the location label for the package.
-
-Returns a list of hashrefs, each of which may contain:
-
-pkgnum, description, amount, unit_amount, quantity, _is_setup, and
-ext_description, which is an arrayref of detail lines to show below
-the package line.
-
-=cut
-
-sub _items_cust_bill_pkg {
- my $self = shift;
- my $conf = $self->conf;
- my $cust_bill_pkgs = shift;
- my %opt = @_;
-
- my $format = $opt{format} || '';
- my $escape_function = $opt{escape_function} || sub { shift };
- my $format_function = $opt{format_function} || '';
- my $no_usage = $opt{no_usage} || '';
- my $unsquelched = $opt{unsquelched} || ''; #unused
- my $section = $opt{section}->{description} if $opt{section};
- my $summary_page = $opt{summary_page} || ''; #unused
- my $multilocation = $opt{multilocation} || '';
- my $multisection = $opt{multisection} || '';
- my $discount_show_always = 0;
-
- my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
-
- my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
-
- my @b = ();
- my ($s, $r, $u) = ( undef, undef, undef );
- foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
- {
-
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
- if ( $_ && !$cust_bill_pkg->hidden ) {
- $_->{amount} = sprintf( "%.2f", $_->{amount} ),
- $_->{amount} =~ s/^\-0\.00$/0.00/;
- $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ }
- if $_->{amount} != 0
- || $discount_show_always
- || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
- || ( $_->{_is_setup} && $_->{setup_show_zero} )
- ;
- $_ = undef;
- }
- }
-
- my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
-
- warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
- $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
- if $DEBUG > 1;
-
- foreach my $display ( grep { defined($section)
- ? $_->section eq $section
- : 1
- }
- #grep { !$_->summary || !$summary_page } # bunk!
- grep { !$_->summary || $multisection }
- @cust_bill_pkg_display
- )
- {
-
- warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
- $display->billpkgdisplaynum. "\n"
- if $DEBUG > 1;
-
- my $type = $display->type;
-
- my $desc = $cust_bill_pkg->desc;
- $desc = substr($desc, 0, $maxlength). '...'
- if $format eq 'latex' && length($desc) > $maxlength;
-
- my %details_opt = ( 'format' => $format,
- 'escape_function' => $escape_function,
- 'format_function' => $format_function,
- 'no_usage' => $opt{'no_usage'},
- );
-
- if ( $cust_bill_pkg->pkgnum > 0 ) {
-
- warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
- if $DEBUG > 1;
-
- my $cust_pkg = $cust_bill_pkg->cust_pkg;
-
- # start/end dates for invoice formats that do nonstandard
- # things with them
- my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
-
- if ( (!$type || $type eq 'S')
- && ( $cust_bill_pkg->setup != 0
- || $cust_bill_pkg->setup_show_zero
- )
- )
- {
-
- warn "$me _items_cust_bill_pkg adding setup\n"
- if $DEBUG > 1;
-
- my $description = $desc;
- $description .= ' Setup'
- if $cust_bill_pkg->recur != 0
- || $discount_show_always
- || $cust_bill_pkg->recur_show_zero;
-
- my @d = ();
- unless ( $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->hidden )
- {
-
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short($self->_date, undef, 'I')
- unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
-
- if ( $multilocation ) {
- my $loc = $cust_pkg->location_label;
- $loc = substr($loc, 0, $maxlength). '...'
- if $format eq 'latex' && length($loc) > $maxlength;
- push @d, &{$escape_function}($loc);
- }
-
- } #unless hiding service details
-
- push @d, $cust_bill_pkg->details(%details_opt)
- if $cust_bill_pkg->recur == 0;
-
- if ( $cust_bill_pkg->hidden ) {
- $s->{amount} += $cust_bill_pkg->setup;
- $s->{unit_amount} += $cust_bill_pkg->unitsetup;
- push @{ $s->{ext_description} }, @d;
- } else {
- $s = {
- _is_setup => 1,
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $cust_bill_pkg->setup,
- setup_show_zero => $cust_bill_pkg->setup_show_zero,
- unit_amount => $cust_bill_pkg->unitsetup,
- quantity => $cust_bill_pkg->quantity,
- ext_description => \@d,
- };
- };
-
- }
-
- if ( ( !$type || $type eq 'R' || $type eq 'U' )
- && (
- $cust_bill_pkg->recur != 0
- || $cust_bill_pkg->setup == 0
- || $discount_show_always
- || $cust_bill_pkg->recur_show_zero
- )
- )
- {
-
- warn "$me _items_cust_bill_pkg adding recur/usage\n"
- if $DEBUG > 1;
-
- my $is_summary = $display->summary;
- my $description = ($is_summary && $type && $type eq 'U')
- ? "Usage charges" : $desc;
-
- #pry be a bit more efficient to look some of this conf stuff up
- # outside the loop
- unless (
- $conf->exists('disable_line_item_date_ranges')
- || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
- ) {
- my $time_period;
- my $date_style = $conf->config( 'cust_bill-line_item-date_style',
- $cust_main->agentnum
- );
- if ( defined($date_style) && $date_style eq 'month_of' ) {
- $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
- } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
- my $desc = $conf->config( 'cust_bill-line_item-date_description',
- $cust_main->agentnum
- );
- $desc .= ' ' unless $desc =~ /\s$/;
- $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
- } else {
- $time_period = time2str($date_format, $cust_bill_pkg->sdate).
- " - ". time2str($date_format, $cust_bill_pkg->edate);
- }
- $description .= " ($time_period)";
- }
-
- my @d = ();
- my @seconds = (); # for display of usage info
-
- #at least until cust_bill_pkg has "past" ranges in addition to
- #the "future" sdate/edate ones... see #3032
- my @dates = ( $self->_date );
- my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
- push @dates, $prev->sdate if $prev;
- push @dates, undef if !$prev;
-
- unless ( $cust_pkg->part_pkg->hide_svc_detail
- || $cust_bill_pkg->itemdesc
- || $cust_bill_pkg->hidden
- || $is_summary && $type && $type eq 'U' )
- {
-
- warn "$me _items_cust_bill_pkg adding service details\n"
- if $DEBUG > 1;
-
- push @d, map &{$escape_function}($_),
- $cust_pkg->h_labels_short(@dates, 'I')
- #$cust_bill_pkg->edate,
- #$cust_bill_pkg->sdate)
- unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
-
- warn "$me _items_cust_bill_pkg done adding service details\n"
- if $DEBUG > 1;
-
- if ( $multilocation ) {
- my $loc = $cust_pkg->location_label;
- $loc = substr($loc, 0, $maxlength). '...'
- if $format eq 'latex' && length($loc) > $maxlength;
- push @d, &{$escape_function}($loc);
- }
-
- # Display of seconds_since_sqlradacct:
- # On the invoice, when processing @detail_items, look for a field
- # named 'seconds'. This will contain total seconds for each
- # service, in the same order as @ext_description. For services
- # that don't support this it will show undef.
- if ( $conf->exists('svc_acct-usage_seconds')
- and ! $cust_bill_pkg->pkgpart_override ) {
- foreach my $cust_svc (
- $cust_pkg->h_cust_svc(@dates, 'I')
- ) {
-
- # eval because not having any part_export_usage exports
- # is a fatal error, last_bill/_date because that's how
- # sqlradius_hour billing does it
- my $sec = eval {
- $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
- };
- push @seconds, $sec;
- }
- } #if svc_acct-usage_seconds
-
- }
-
- unless ( $is_summary ) {
- warn "$me _items_cust_bill_pkg adding details\n"
- if $DEBUG > 1;
-
- #instead of omitting details entirely in this case (unwanted side
- # effects), just omit CDRs
- $details_opt{'no_usage'} = 1
- if $type && $type eq 'R';
-
- push @d, $cust_bill_pkg->details(%details_opt);
- }
-
- warn "$me _items_cust_bill_pkg calculating amount\n"
- if $DEBUG > 1;
-
- my $amount = 0;
- if (!$type) {
- $amount = $cust_bill_pkg->recur;
- } elsif ($type eq 'R') {
- $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
- } elsif ($type eq 'U') {
- $amount = $cust_bill_pkg->usage;
- }
-
- if ( !$type || $type eq 'R' ) {
-
- warn "$me _items_cust_bill_pkg adding recur\n"
- if $DEBUG > 1;
-
- if ( $cust_bill_pkg->hidden ) {
- $r->{amount} += $amount;
- $r->{unit_amount} += $cust_bill_pkg->unitrecur;
- push @{ $r->{ext_description} }, @d;
- } else {
- $r = {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $amount,
- recur_show_zero => $cust_bill_pkg->recur_show_zero,
- unit_amount => $cust_bill_pkg->unitrecur,
- quantity => $cust_bill_pkg->quantity,
- %item_dates,
- ext_description => \@d,
- };
- $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
- }
-
- } else { # $type eq 'U'
-
- warn "$me _items_cust_bill_pkg adding usage\n"
- if $DEBUG > 1;
-
- if ( $cust_bill_pkg->hidden ) {
- $u->{amount} += $amount;
- $u->{unit_amount} += $cust_bill_pkg->unitrecur;
- push @{ $u->{ext_description} }, @d;
- } else {
- $u = {
- description => $description,
- #pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_bill_pkg->pkgnum,
- amount => $amount,
- recur_show_zero => $cust_bill_pkg->recur_show_zero,
- unit_amount => $cust_bill_pkg->unitrecur,
- quantity => $cust_bill_pkg->quantity,
- %item_dates,
- ext_description => \@d,
- };
- }
- }
-
- } # recurring or usage with recurring charge
-
- } else { #pkgnum tax or one-shot line item (??)
-
- warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
- if $DEBUG > 1;
-
- if ( $cust_bill_pkg->setup != 0 ) {
- push @b, {
- 'description' => $desc,
- 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
- };
- }
- if ( $cust_bill_pkg->recur != 0 ) {
- push @b, {
- 'description' => "$desc (".
- time2str($date_format, $cust_bill_pkg->sdate). ' - '.
- time2str($date_format, $cust_bill_pkg->edate). ')',
- 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
- };
- }
-
- }
-
- }
-
- $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
- && $conf->exists('discount-show-always'));
-
- }
-
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
- if ( $_ ) {
- $_->{amount} = sprintf( "%.2f", $_->{amount} ),
- $_->{amount} =~ s/^\-0\.00$/0.00/;
- $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
- push @b, { %$_ }
- if $_->{amount} != 0
- || $discount_show_always
- || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
- || ( $_->{_is_setup} && $_->{setup_show_zero} )
- }
- }
-
- warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
- if $DEBUG > 1;
-
- @b;
-
-}
-
sub _items_credits {
my( $self, %opt ) = @_;
my $trim_len = $opt{'trim_len'} || 60;
@@ -5362,51 +2942,6 @@ sub _items_payments {
}
-=item _items_discounts_avail
-
-Returns an array of line item hashrefs representing available term discounts
-for this invoice. This makes the same assumptions that apply to term
-discounts in general: that the package is billed monthly, at a flat rate,
-with no usage charges. A prorated first month will be handled, as will
-a setup fee if the discount is allowed to apply to setup fees.
-
-=cut
-
-sub _items_discounts_avail {
- my $self = shift;
- my $list_pkgnums = 0; # if any packages are not eligible for all discounts
-
- my %plans = $self->discount_plans;
-
- $list_pkgnums = grep { $_->list_pkgnums } values %plans;
-
- map {
- my $months = $_;
- my $plan = $plans{$months};
-
- my $term_total = sprintf('%.2f', $plan->discounted_total);
- my $percent = sprintf('%.0f',
- 100 * (1 - $term_total / $plan->base_total) );
- my $permonth = sprintf('%.2f', $term_total / $months);
- my $detail = $self->mt('discount on item'). ' '.
- join(', ', map { "#$_" } $plan->pkgnums)
- if $list_pkgnums;
-
- # discounts for non-integer months don't work anyway
- $months = sprintf("%d", $months);
-
- +{
- description => $self->mt('Save [_1]% by paying for [_2] months',
- $percent, $months),
- amount => $self->mt('[_1] ([_2] per month)',
- $term_total, $money_char.$permonth),
- ext_description => ($detail || ''),
- }
- } #map
- sort { $b <=> $a } keys %plans;
-
-}
-
=item call_details [ OPTION => VALUE ... ]
Returns an array of CSV strings representing the call details for this invoice
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index b382232..82b09b6 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -8,7 +8,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status
FS::cust_main::Billing_Discount
FS::cust_main::Location
FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
- FS::geocode_Mixin
+ FS::geocode_Mixin FS::Quotable_Mixin
FS::o2m_Common
FS::Record
);
diff --git a/FS/FS/prospect_main.pm b/FS/FS/prospect_main.pm
index 6adc852..b5d51d3 100644
--- a/FS/FS/prospect_main.pm
+++ b/FS/FS/prospect_main.pm
@@ -1,10 +1,10 @@
package FS::prospect_main;
use strict;
-use base qw( FS::o2m_Common FS::Record );
+use base qw( FS::Quotable_Mixin FS::o2m_Common FS::Record );
use vars qw( $DEBUG );
use Scalar::Util qw( blessed );
-use FS::Record qw( dbh qsearch ); #qsearchs );
+use FS::Record qw( dbh qsearch qsearchs );
use FS::agent;
use FS::cust_location;
use FS::contact;
@@ -213,6 +213,9 @@ sub check {
=item name
+Returns a name for this prospect, as a string (company name for commercial
+prospects, contact name for residential prospects).
+
=cut
sub name {
@@ -259,6 +262,16 @@ sub qual {
qsearch( 'qual', { 'prospectnum' => $self->prospectnum } );
}
+=item agent
+
+Returns the agent (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent {
+ my $self = shift;
+ qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
+}
=item search HASHREF
diff --git a/FS/FS/quotation.pm b/FS/FS/quotation.pm
index 4202335..0cfb11e 100644
--- a/FS/FS/quotation.pm
+++ b/FS/FS/quotation.pm
@@ -1,10 +1,12 @@
package FS::quotation;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
use strict;
-use base qw( FS::otaker_Mixin FS::Record );
-use FS::Record; # qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs );
+use FS::CurrentUser;
use FS::cust_main;
use FS::prospect_main;
+use FS::quotation_pkg;
=head1 NAME
@@ -73,6 +75,8 @@ points to. You can ask the object for a copy with the I<hash> method.
=cut
sub table { 'quotation'; }
+sub notice_name { 'Quotation'; }
+sub template_conf { 'quotation_'; }
=item insert
@@ -111,7 +115,7 @@ sub check {
$self->_date(time) unless $self->_date;
- #XXX set usernum
+ $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
$self->SUPER::check;
}
@@ -134,6 +138,16 @@ sub cust_main {
qsearchs('cust_main', { 'custnum' => $self->custnum } );
}
+=item cust_bill_pkg
+
+=cut
+
+sub cust_bill_pkg {
+ my $self = shift;
+ #actually quotation_pkg objects
+ qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
+}
+
=back
=head1 BUGS
diff --git a/FS/MANIFEST b/FS/MANIFEST
index e7aba20..590874d 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -641,3 +641,11 @@ FS/part_svc_class.pm
t/part_svc_class.t
FS/ftp_target.pm
t/ftp_target.t
+FS/quotation.pm
+t/quotation.t
+FS/quotation_pkg.pm
+t/quotation_pkg.t
+FS/quotation_pkg_discount.pm
+t/quotation_pkg_discount.t
+FS/Quotable_Mixin.pm
+t/Quotable_Mixin.t
diff --git a/conf/quotation_html b/conf/quotation_html
new file mode 100644
index 0000000..1dfb944
--- /dev/null
+++ b/conf/quotation_html
@@ -0,0 +1,266 @@
+<STYLE TYPE="text/css">
+.invoice { font-family: sans-serif; font-size: 10pt }
+.invoice_header { font-size: 10pt }
+.invoice_headerright TH { border-top: 2px solid #000000; border-bottom: 2px solid #000000 }
+.invoice_headerright TD { font-size: 10pt; empty-cells: show }
+.invoice_summary TH { border-bottom: 2px solid #000000 }
+.invoice_summary TD { font-size: 10pt; empty-cells: show }
+.invoice_longtable table { cellspacing: none }
+.invoice_longtable TH { border-top: 2px solid #000000; border-bottom: 1px solid #000000; padding-left: none; padding-right: none; font-size: 10pt }
+.invoice_desc TD { border-top: 2px solid #000000; font-weight: bold; font-size: 10pt }
+.invoice_desc_more TD { font-weight: bold; font-size: 10pt }
+.invoice_extdesc TD { font-size: 8pt }
+.invoice_totaldesc TD { font-size: 10pt; empty-cells: show }
+.allcaps { text-transform:uppercase }
+</STYLE>
+
+<table class="invoice" bgcolor="#ffffff" WIDTH=625 CELLSPACING=8><tr><td>
+
+ <table class="invoice_header" width="100%">
+ <tr>
+ <td><img src="<%= $cid ? "cid:$cid" : "cust_bill-logo.cgi?invnum=$invnum;template=$template" %>"></td>
+ <td align="left"><%= $returnaddress %></td>
+ <td align="right">
+ <table CLASS="invoice_headerright" cellspacing=0>
+ <tr>
+ <td align="center">
+ <%= emt('Quotation date') %><BR>
+ <B><%= $date %></B>
+ </td>
+ <td>
+ </td>
+ <td align="center">
+ <%= emt('Quotation #') %><BR>
+ <B><%= $quotationnum %></B>
+ </td>
+ <td>
+ </td>
+ <td align="center">
+ <%= $custnum ? emt('Customer #') : $prospectnum ? emt('Prospect #') : '' %><BR>
+ <B><%= $custnum || $prospectnum %></B>
+ </td>
+ </tr>
+ <tr>
+ <th>&nbsp;</th>
+ <th colspan=3 align="center" class="allcaps">
+ <FONT SIZE="+3"><%= substr(emt($notice_name),0,1) %></FONT><FONT SIZE="+2"><%= substr(emt($notice_name),1) %></FONT>
+ </th>
+ <th>&nbsp;</th>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ </td>
+ <td align="left">
+ <b><%= $name %></b><BR>
+ <%= join('<BR>', grep length($_), $company,
+ $address1,
+ $address2,
+ "$city,&nbsp;$state&nbsp;&nbsp;$zip",
+ $country,
+ )
+ %>
+ </td>
+ <%= $ship_enable ? ('<td align="left">'.
+ join('<BR>',grep length($_), '<b>'.emt('Service Address').'</b>',
+ $ship_company,
+ $ship_address1,
+ $ship_address2,
+ "$ship_city,&nbsp;$ship_state&nbsp;$ship_zip",
+ $ship_country,
+ ' ',
+ ' ',
+ ).
+ '</td><tr><td></td><td></td>'
+ )
+ : ''
+ %>
+ <td align="right">
+ <%=
+ if($barcode_cid) {
+ $OUT .= qq! <img src="cid:$barcode_cid"><br> !;
+ }
+ elsif($barcode_img) {
+ $OUT .= qq! <img src="cust_bill-barcode.cgi?invnum=$invnum;template=$template"><br> !;
+ }
+ %>
+ <%= $terms ? emt('Terms') . ": $terms" : '' %><BR>
+ <%= $po_line %>
+ </td>
+ </tr>
+
+ </table>
+ <%= $summary %>
+ <%=
+ my $notfirst = 0;
+ my $columncount = $unitprices ? 5 : 3;
+ foreach my $section ( grep { !$summary || $_->{description} ne $finance_section } @sections ) {
+ if ($section->{'pretotal'} && !$summary) {
+ $OUT .= '</table>' if $notfirst;
+ $OUT .=
+ '<table width="100%"><tr><td>'.
+ '<p align="right"><b><font size="+1">'.
+ uc(substr($section->{'pretotal'},0,1)).
+ '</font><font size="+0">'. uc(substr($section->{'pretotal'},1)).
+ '</font></b>'.
+ '<p>'.
+ '</td></tr>';
+ }
+ unless ($section->{'summarized'}) {
+ $OUT .= '</table>' if ( $notfirst || $section->{'pretotal'} && !$summary );
+ $OUT .= '<table><tr><td>';
+ my $sectionhead = $section->{'description'} || emt('Charges');
+ $OUT .=
+ '<p class="allcaps"><b><font size="+1">'. substr($sectionhead,0,1).
+ '</font><font size="+0">'. substr($sectionhead,1).
+ '</font></b>'.
+ '<p>'.
+ '</td></tr></table>';
+
+ $OUT .=
+ '<table class="invoice_longtable" CELLSPACING=0 WIDTH="100%">'.
+ '<tr>';
+
+ if ($section->{header_generator}) {
+ my $header = &{$section->{header_generator}}();
+ $OUT .= $header;
+ $columncount = scalar(my @array = split /<\/th><th/i, $header);
+ } else {
+ $OUT .= '<th align="center">' . emt('Ref') . '</th>'.
+ '<th align="left">' . emt('Description') . '</th>'.
+ ( $unitprices
+ ? '<th align="left">' . emt('Unit Price') . '</th>'.
+ '<th align="left">' . emt('Quantity') . '</th>'
+ : '' ).
+ '<th align="right">' . emt('Amount') . '</th>';
+ }
+ $OUT .= '</tr>';
+
+ my $lastref = 0;
+ foreach my $line (
+ grep { ( scalar(@sections) > 1
+ ? $section->{'description'} eq $_->{'section'}->{'description'}
+ : 1
+ ) }
+ @detail_items )
+ {
+ $OUT .=
+ '<tr class="invoice_desc';
+ if ( $section->{description_generator} ) {
+ $OUT .= &{$section->{description_generator}}($line);
+ } else {
+ $OUT .= ( ($line->{'ref'} && $line->{'ref'} ne $lastref) ? '' : '_more' ).
+ '">'.
+ '<td align="center">'.
+ ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
+ '<td align="left">'. $line->{'description'}. '</td>'.
+ ( $unitprices
+ ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
+ '<td align="left">'. $line->{'quantity'}. '</td>'
+ : ''
+ ).
+
+ '<td align="right">'. $line->{'amount'}. '</td>';
+ }
+ $OUT .= '</tr>';
+ $lastref = $line->{'ref'};
+ if ( @{$line->{'ext_description'} } ) {
+ unless ( $section->{description_generator} ) {
+ $OUT .= '<tr class="invoice_extdesc"><td></td><td';
+ $OUT .= $unitprices ? ' colspan=3' : '';
+ $OUT .= '><table width="100%">';
+ }
+ foreach my $ext_desc ( @{$line->{'ext_description'} } ) {
+ $OUT .=
+ '<tr class="invoice_extdesc">'.
+ ( $section->{'description_generator'} ? '<td></td>' : '' ).
+ '<td align="left" '.
+ ( $ext_desc =~ /<\/?TD>/i ? '' : 'colspan=99' ). '>'.
+ '&nbsp;&nbsp;'. $ext_desc.
+ '</td>'.
+ '</tr>'
+ }
+ unless ( $section->{description_generator} ) {
+ $OUT .= '</table></td><td></td>';
+ }
+ $OUT .= '</tr>';
+ }
+ }
+
+
+ if ($section->{'description'} || $multisection and !$section->{no_subtotal}) {
+ my $style = 'border-top: 3px solid #000000;'.
+ 'border-bottom: 3px solid #000000;';
+ $OUT .=
+ '<tr class="invoice_totaldesc">'.
+ qq(<td style="$style">&nbsp;</td>);
+ if ($section->{total_generator}) {
+ $OUT .= &{$section->{total_generator}}($section);
+ } else {
+ $OUT .= qq(<td align="left" style="$style").
+ ( $unitprices ? ' colspan=3>' : '>' ).
+ $section->{'description'}. ' ' . emt('Total') . '</td>'.
+ qq(<td align="right" style="$style">).
+ $section->{'subtotal'}. '</td>';
+ }
+ $OUT .= '</tr>';
+ }
+ }
+ if ($section->{'posttotal'}) {
+ $OUT .= '<tr><td align="right" colspan='. $columncount. '>';
+ $OUT .=
+ '<p><font size="+1">'. $section->{'posttotal'}.
+ '</font>'.
+ '<p>';
+ $OUT .= '</td></tr>';
+ }
+
+ $notfirst++;
+
+ }
+
+ my $style = 'border-top: 3px solid #000000;';
+ my $linenum = 0;
+
+ foreach my $line ( @total_items ) {
+
+ $style .= 'border-bottom: 3px solid #000000;'
+ if ++$linenum == scalar(@total_items) - ( $balance_due_below_line ? 1 : 0 );
+
+ $OUT .=
+ '<tr class="invoice_totaldesc">';
+ if ($section->{total_line_generator}) {
+ $OUT .= &{$section->{total_line_generator}}($line);
+ } else {
+ $OUT .= qq(<td style="$style">&nbsp;</td>).
+ qq(<td align="left" style="$style" colspan=").
+ ( $columncount - 2 ). '">'.
+ $line->{'total_item'}. '</td>'.
+ qq(<td align="right" style="$style">).
+ $line->{'total_amount'}. '</td>';
+ }
+ $OUT .= '</tr>';
+
+ $style='';
+
+ }
+
+ %>
+ </table>
+ <br><br>
+
+<%= length($summary)
+ ? ''
+ : ( $smallernotes
+ ? '<FONT SIZE="-1">'.$notes.'</FONT>'
+ : $notes
+ )
+%>
+
+ <hr NOSHADE SIZE=2 COLOR="#000000">
+ <p align="center" <%= $smallerfooter ? 'STYLE="font-size:75%;"' : '' %>><%= $footer %>
+
+</td></tr></table>
diff --git a/conf/quotation_latex b/conf/quotation_latex
new file mode 100644
index 0000000..772c2eb
--- /dev/null
+++ b/conf/quotation_latex
@@ -0,0 +1,362 @@
+%% file: Standard Multipage.tex
+%% Purpose: Multipage bill template for e-Bills
+%%
+%% Created by Mark Asplen-Taylor
+%% Asplen Management Ltd
+%% www.asplen.co.uk
+%%
+%% Modified for Freeside by Kristian Hoffman
+%%
+%% Changes
+%% 0.1 4/12/00 Created
+%% 0.2 18/10/01 More fields added
+%% 1.0 16/11/01 RELEASED
+%% 1.2 16/10/02 Invoice number added
+%% 1.3 2/12/02 Logo graphic added
+%% 1.4 7/2/03 Multipage headers/footers added
+%% n/a forked for Freeside; checked into CVS
+%%
+
+\documentclass[letterpaper]{article}
+
+\usepackage{fancyhdr,lastpage,ifthen,array,fslongtable,afterpage,caption,multirow,bigstrut}
+\usepackage{graphicx} % required for logo graphic
+\usepackage[utf8]{inputenc} % multilanguage support
+\usepackage[T1]{fontenc}
+
+\addtolength{\voffset}{-0.0cm} % top margin to top of header
+\addtolength{\hoffset}{-0.6cm} % left margin on page
+\addtolength{\topmargin}{[@-- defined($topmargin) ? $topmargin : '-1.25cm' --@]}
+\setlength{\headheight}{2.0cm} % height of header
+\setlength{\headsep}{[@-- defined($headsep) ? $headsep : '1.0cm' --@]}
+\setlength{\footskip}{1.0cm} % bottom of footer from bottom of text
+
+%\addtolength{\textwidth}{2.1in} % width of text
+\setlength{\textwidth}{19.5cm}
+\setlength{\textheight}{[@-- defined($textheight) ? $textheight : '19.5cm' --@]}
+\setlength{\oddsidemargin}{-0.9cm} % odd page left margin
+\setlength{\evensidemargin}{-0.9cm} % even page left margin
+
+\LTchunksize=40
+
+\renewcommand{\headrulewidth}{0pt}
+\renewcommand{\footrulewidth}{1pt}
+
+\renewcommand{\footrule}{
+[@--
+ $coupon ? '\ifthenelse{\equal{\thepage}{1}}' : '';
+--@]
+ {
+ }
+ {
+ \vbox to 0pt{\rule{\headwidth}{\footrulewidth}\vss}
+ }
+}
+
+\newcommand{\extracouponspace}{[@-- defined($extracouponspace) ? $extracouponspace : '3.6cm' --@]}
+
+% Adjust the inset of the mailing address
+\newcommand{\addressinset}[1][]{\hspace{1.0cm}}
+
+% Adjust the inset of the return address and logo
+\newcommand{\returninset}[1][]{\hspace{-0.25cm}}
+
+% New command for address lines i.e. skip them if blank
+\newcommand{\addressline}[1]{\ifthenelse{\equal{#1}{}}{}{#1\\}}
+
+% Inserts dollar symbol
+\newcommand{\dollar}[1][]{\symbol{36}}
+
+% Remove plain style header/footer
+\fancypagestyle{plain}{
+ \fancyhead{}
+}
+\fancyhf{}
+
+% Define fancy header/footer for first and subsequent pages
+\fancyfoot[C]{
+ \ifthenelse{\equal{\thepage}{1}}
+ { % First page
+[@--
+ if ($coupon) {
+ $OUT .= '\vspace{-\extracouponspace}';
+ $OUT .= '\rule[0.5em]{\textwidth}{\footrulewidth}\\\\';
+ $OUT .= $coupon;
+ $OUT .= '\vspace{'. $couponfootsep. '}' if defined($couponfootsep);
+ }
+ '';
+--@] [@-- $smallerfooter ? '\scriptsize{' : '\small{' --@]
+[@-- $footer --@]
+ }[@-- $coupon ? '\vspace{\extracouponspace}' : '' --@]
+ }
+ { % ... pages
+ [@-- $smallerfooter ? '\scriptsize{' : '\small{' --@]
+[@-- $smallfooter --@]
+ }
+ }
+}
+
+\fancyfoot[R]{
+ \ifthenelse{\equal{\thepage}{1}}
+ { % First page
+ }
+ { % ... pages
+ \small{\thepage\ of \pageref{LastPage}}
+ }
+}
+
+\fancyhead[L]{
+ \ifthenelse{\equal{\thepage}{1}}
+ { % First page
+ \returninset
+ \makebox{
+ \begin{tabular}{ll}
+ \includegraphics{[@-- $logo_file --@]} & [@-- $verticalreturnaddress ? '\\\\' : '' --@]
+ \begin{minipage}[b]{5.5cm}
+[@-- $returnaddress --@]
+ \end{minipage}\\
+ \end{tabular}
+ }
+ }
+ { % ... pages
+ %\includegraphics{[@-- $logo_file --@]} % Uncomment if you want the logo on all pages.
+ }
+}
+
+\fancyhead[R]{
+ \ifthenelse{\equal{\thepage}{1}}
+ { % First page
+ \begin{tabular}{ccc}
+ [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\
+ \vspace{0.2cm}
+ \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]} \\\hline
+ \rule{0pt}{5ex} &~~ \huge{\textsc{[@-- emt($notice_name) --@]}} & \\
+ \vspace{-0.2cm}
+ & & \\\hline
+ \end{tabular}
+ }
+ { % ... pages
+ \small{
+ \begin{tabular}{lll}
+ [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\
+ \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]}\\
+ \end{tabular}
+ }
+ }
+}
+
+\pagestyle{fancy}
+
+
+%% Font options are:
+%% bch Bitsream Charter
+%% put Utopia
+%% phv Adobe Helvetica
+%% pnc New Century Schoolbook
+%% ptm Times
+%% pcr Courier
+
+\renewcommand{\familydefault}{phv}
+
+
+% Commands for freeside table header...
+
+\newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }
+\newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }
+\newcommand{\FSunitcolumns}{ [@--
+ $unitprices
+ ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&'
+ : '' --@] }
+
+\newcommand{\FShead}{
+ \hline
+ \rule{0pt}{2.5ex}
+ \makebox[1.4cm]{\textbf{Ref}} &
+ \multicolumn{\FSdescriptioncolumncount}{l}{\makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}}}&
+ \FSunitcolumns
+ \makebox[1.6cm][r]{\textbf{[@-- emt('Amount') --@]}} \\
+ \hline
+}
+
+% ...description...
+\newcommand{\FSdesc}[5]{
+ \multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &
+ \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &
+[@-- $unitprices ? ' \multicolumn{1}{l}{\textbf{#3}} &'."\n".
+ ' \multicolumn{1}{r}{\textbf{#4}} &'."\n"
+ : ''
+--@]
+ \multicolumn{1}{r}{\textbf{\dollar #5}}\\
+}
+% ...extended description...
+\newcommand{\FSextdesc}[1]{
+ \multicolumn{1}{l}{\rule{0pt}{1.0ex}} &
+%% \multicolumn{2}{l}{\small{~-~#1}}\\
+#1\\
+}
+% ...and total line items.
+\newcommand{\FStotaldesc}[2]{
+ & \multicolumn{6}{l}{#1} & #2\\
+}
+
+
+\begin{document}
+% Headers and footers defined for the first page
+\addressinset \rule{0.5cm}{0cm}
+\makebox{
+\begin{minipage}[t]{7.0cm}
+\vspace{[@-- defined($addresssep) ? $addresssep : '0.25cm' --@]}
+\textbf{[@-- $payname --@]}\\
+\addressline{[@-- $company --@]}
+\addressline{[@-- $address1 --@]}
+\addressline{[@-- $address2 --@]}
+\addressline{[@-- $city --@], [@-- $state --@]~~[@-- $zip --@]}
+\addressline{[@-- $country --@]}
+\end{minipage}}
+\hfill
+\makebox{
+\begin{minipage}[t]{6.4cm}
+[@--
+ if ($ship_enable) {
+ $OUT .= '\textbf{' . emt('Service Address') . '}\\\\';
+ $OUT .= "\\addressline{$ship_company}";
+ $OUT .= "\\addressline{$ship_address1}";
+ $OUT .= "\\addressline{$ship_address2}";
+ $OUT .= "\\addressline{$ship_city, $ship_state~~$ship_zip}";
+ $OUT .= "\\addressline{$ship_country}";
+ $OUT .= '~\\\\';
+ }else{
+ $OUT .= '';
+ }
+--@]
+\begin{flushright}
+[@-- $terms ? emt('Terms') .": $terms" : '' --@]\\
+[@-- $po_line --@]\\
+\end{flushright}
+\end{minipage}}
+\vspace{1.5cm}
+%
+[@-- $summary --@]
+%
+\section*{}
+[@--
+ foreach my $section ( grep { !$summary || $_->{description} ne $finance_section } @sections ) {
+ if ($section->{'pretotal'} && !$summary) {
+ $OUT .= '\begin{flushright}';
+ $OUT .= '\large\textsc{'. $section->{'pretotal'}. '}\\\\';
+ $OUT .= '\\end{flushright}';
+ }
+ $OUT .= '\pagebreak' if $section->{'post_total'};
+ unless ($section->{'summarized'} ) {
+ $OUT .= '\captionsetup{singlelinecheck=false,justification=raggedright,font={Large,sc,bf}}';
+ $OUT .= '\ifthenelse{\equal{\thepage}{1}}{\setlength{\LTextracouponspace}{\extracouponspace}}{\setlength{\LTextracouponspace}{0pt}}'
+ if $coupon;
+ $OUT .= '\begin{longtable}{cllllllr}';
+ $OUT .= '\caption*{ ';
+ $OUT .= ($section->{'description'}) ? $section->{'description'}: emt('Charges');
+ $OUT .= '}\\\\';
+ if ($section->{header_generator}) {
+ $OUT .= &{$section->{header_generator}}();
+ } else {
+ $OUT .= '\FShead';
+ }
+ $OUT .= '\endfirsthead';
+ $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued from previous page').'}\\\\';
+ if ($section->{header_generator}) {
+ $OUT .= &{$section->{header_generator}}();
+ } else {
+ $OUT .= '\FShead';
+ }
+ $OUT .= '\endhead';
+ $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued on next page...').'}\\\\';
+ $OUT .= '\endfoot';
+ $OUT .= '\hline';
+
+ if (scalar(@sections) > 1 and !$section->{no_subtotal}) {
+ if ($section->{total_generator}) {
+ $OUT .= &{$section->{total_generator}}($section);
+ } else {
+ $OUT .= '\FStotaldesc{' . $section->{'description'} . ' Total}' .
+ '{' . $section->{'subtotal'} . '}' . "\n";
+ }
+ }
+
+ #if ($section == $sections[$#sections]) {
+ foreach my $line (grep {$_->{section}->{description} eq $section->{description}} @total_items) {
+ if ($section->{total_line_generator}) {
+ $OUT .= &{$section->{total_line_generator}}($line);
+ } else {
+ $OUT .= '\FStotaldesc{' . $line->{'total_item'} . '}' .
+ '{' . $line->{'total_amount'} . '}' . "\n";
+ }
+ }
+ #}
+
+ $OUT .= '\hline';
+ $OUT .= '\endlastfoot';
+
+ my $lastref = 0;
+ foreach my $line (
+ grep { ( scalar( @sections ) > 1
+ ? $section->{'description'} eq $_->{'section'}->{'description'}
+ : 1
+ ) }
+ @detail_items )
+ {
+ my $ext_description = $line->{'ext_description'};
+
+ # Don't break-up small packages.
+ my $rowbreak = @$ext_description < 5 ? '*' : '';
+
+ $OUT .= "\\hline\n" if ($line->{'ref'} && $line->{'ref'} ne $lastref);
+ if ($section->{description_generator}) {
+ $OUT .= &{$section->{description_generator}}($line);
+ } else {
+ $OUT .= '\FSdesc'.
+ '{' . ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ) . '}'.
+ '{' . $line->{'description'} . '}' .
+ '{' . ( $unitprices ? $line->{'unit_amount'} : '' ) . '}'.
+ '{' . ( $unitprices ? $line->{'quantity'} : '' ) . '}' .
+ '{' . $line->{'amount'} . "}${rowbreak}\n";
+ }
+ $lastref = $line->{'ref'};
+
+ foreach my $ext_desc (@$ext_description) {
+ if ($section->{extended_description_generator}) {
+ $OUT .= &{$section->{extended_description_generator}}($ext_desc);
+ } else {
+ if ( $ext_desc !~ /[^\\]&/ ) {
+ $ext_desc = substr($ext_desc, 0, 80) . '...'
+ if (length($ext_desc) > 80);
+ $ext_desc = '\multicolumn{6}{l}{\small{~~~'. $ext_desc. '}}';
+ }else{
+ $ext_desc = "~~~$ext_desc";
+ }
+ $OUT .= '\FSextdesc{' . $ext_desc . '}' . "${rowbreak}\n";
+ }
+ }
+
+ }
+
+ $OUT .= '\end{longtable}';
+ }
+ if ($section->{'posttotal'}) {
+ $OUT .= '\begin{flushright}';
+ $OUT .= '\normalfont\large\bfseries\textsc{'. $section->{'posttotal'}. '}\\\\';
+ $OUT .= '\\end{flushright}';
+ }
+ }
+
+--@]
+\vfill
+\begin{minipage}[t]{\textwidth}
+ [@-- length($summary)
+ ? ''
+ : ( $smallernotes
+ ? '\scriptsize{ '.$notes.' }'
+ : $notes
+ )
+ --@]
+ [@-- $coupon ? '\ifthenelse{\equal{\thepage}{1}}{\rule{0pt}{\extracouponspace}}{}' : '' --@]
+\end{minipage}
+\end{document}
diff --git a/conf/quotation_latexnotes b/conf/quotation_latexnotes
new file mode 100644
index 0000000..58fd68a
--- /dev/null
+++ b/conf/quotation_latexnotes
@@ -0,0 +1,8 @@
+%%
+%% Add any quotation notes in here
+%%
+\section*{\textsc{Notes}}
+\begin{enumerate}
+\item Thank you for your interest in our services.
+\item If you have any questions please email or telephone.
+\end{enumerate}
diff --git a/httemplate/edit/process/quotation.html b/httemplate/edit/process/quotation.html
index 7671c36..a695665 100644
--- a/httemplate/edit/process/quotation.html
+++ b/httemplate/edit/process/quotation.html
@@ -1,6 +1,6 @@
<% include( 'elements/process.html',
'table' => 'quotation',
- 'redirect' => $p.'view/quotation.html?',
+ 'redirect' => popurl(3).'view/quotation.html?',
)
%>
<%init>
diff --git a/httemplate/edit/quotation.html b/httemplate/edit/quotation.html
index f706425..8b60623 100644
--- a/httemplate/edit/quotation.html
+++ b/httemplate/edit/quotation.html
@@ -3,8 +3,23 @@
'table' => 'quotation',
'labels' => {
'quotationnum' => 'Quotation number',
+ 'prospectnum' => 'Prospect',
+ 'custnum' => 'Customer',
+ '_date' => 'Date',
+ 'disabled' => 'Disabled',
},
+ 'fields' => [
+ { field=>'prospectnum', type=>'fixed-prospect_main' },
+ { field=>'custnum', type=>'fixed-cust_main' },
+ { field=>'_date', type=>'fixed-date' },
+ { field=>'disabled', type=>'checkbox', value=>'Y'},
+ ],
#XXX some way to disable the "view all"
+ 'new_callback' => sub { my( $cgi, $quotation) = @_;
+ $quotation->$_( $cgi->param($_) )
+ foreach qw( prospectnum custnum );
+ $quotation->_date(time);
+ },
)
%>
<%init>
diff --git a/httemplate/elements/tr-fixed-cust_main.html b/httemplate/elements/tr-fixed-cust_main.html
new file mode 100644
index 0000000..00bcb66
--- /dev/null
+++ b/httemplate/elements/tr-fixed-cust_main.html
@@ -0,0 +1,15 @@
+% if ( $cust_main ) {
+ <% include('tr-fixed.html', %opt ) %>
+% }
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $cust_main = $value ? qsearchs('cust_main', {custnum=>$value} )
+ : '';
+
+$opt{'formatted_value'} = $cust_main->name if $cust_main;
+
+</%init>
diff --git a/httemplate/elements/tr-fixed-date.html b/httemplate/elements/tr-fixed-date.html
new file mode 100644
index 0000000..716e5ce
--- /dev/null
+++ b/httemplate/elements/tr-fixed-date.html
@@ -0,0 +1,13 @@
+<% include('tr-fixed.html', %opt ) %>
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $conf = new FS::Conf;
+my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
+$opt{'formatted_value'} = time2str($date_format, $value);
+
+</%init>
diff --git a/httemplate/elements/tr-fixed-prospect_main.html b/httemplate/elements/tr-fixed-prospect_main.html
new file mode 100644
index 0000000..8da0ffb
--- /dev/null
+++ b/httemplate/elements/tr-fixed-prospect_main.html
@@ -0,0 +1,15 @@
+% if ( $prospect_main ) {
+ <% include('tr-fixed.html', %opt ) %>
+% }
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $prospect_main = $value ? qsearchs('prospect_main', {prospectnum=>$value} )
+ : '';
+
+$opt{'formatted_value'} = $prospect_main->name if $prospect_main;
+
+</%init>
diff --git a/httemplate/view/prospect_main.html b/httemplate/view/prospect_main.html
index 9e85348..801d64b 100644
--- a/httemplate/view/prospect_main.html
+++ b/httemplate/view/prospect_main.html
@@ -64,6 +64,31 @@
<BR>
+% if ( $curuser->access_right('Generate quotation') ) {
+ <FONT CLASS="fsinnerbox-title"><% mt( 'Quotations' ) |h %></FONT>
+ <A HREF="<%$p%>edit/quotation.html?prospectnum=<% $prospectnum %>">New quotation</A>
+% my @quotations = $prospect_main->quotation;
+% if ( @quotations ) {
+ <& /elements/table-grid.html &>
+% my $bgcolor1 = '#eeeeee';
+% my $bgcolor2 = '#ffffff';
+% my $bgcolor = '';
+ <TR>
+ <TH CLASS="grid" BGCOLOR="#cccccc">#</TH>
+ <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Date') |h %></TH>
+ </TR>
+% foreach my $quotation (@quotations) {
+ <TR>
+ <TD CLASS="grid" BGCOLOR="#cccccc"><% $quotation->quotationnum %></TD>
+ <TD CLASS="grid" BGCOLOR="#cccccc"><% time2str($date_format, $quotation->_date) |h %></TD>
+ </TR>
+% }
+ </TABLE>
+% }
+ <BR><BR>
+% }
+
+
% if ( $curuser->access_right('Qualify service') ) {
<% include( '/elements/popup_link-prospect_main.html',
'action' => $p. 'misc/qual.html',
@@ -80,6 +105,7 @@
<BR><BR>
% }
+<!--
<% ntable("#cccccc") %>
<TR>
@@ -87,6 +113,7 @@
</TR>
</TABLE>
+-->
<%init>
@@ -95,6 +122,10 @@ my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
unless $curuser->access_right('View prospect');
+my $conf = new FS::Conf;
+
+my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
my $prospectnum;
if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
$prospectnum = $1;
diff --git a/httemplate/view/quotation.html b/httemplate/view/quotation.html
index 2c2c6b7..866ade2 100755
--- a/httemplate/view/quotation.html
+++ b/httemplate/view/quotation.html
@@ -22,21 +22,20 @@ XXX resending quotations
% }
-XXX view typset quotation
+</%doc>
-% if ( $conf->exists('invoice_latex') ) {
+% if ( $conf->exists('quotation_latex') ) {
- <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $link %>"><% mt('View typeset invoice PDF') |h %></A>
+ <A HREF="<% $p %>view/quotation-pdf.cgi?<% $link %>"><% mt('View typeset quotation PDF') |h %></A>
<BR><BR>
% }
-XXX actually show the quotation
-
-% if ( $conf->exists('invoice_html') ) {
- <% join('', $cust_bill->print_html(\%opt) ) %>
+% if ( $conf->exists('quotation_html') ) {
+ <% join('', $quotation->print_html() ) %>
% } else {
- <PRE><% join('', $cust_bill->print_text(\%opt) ) %></PRE>
-% }
+% die "quotation_html config missing";
+% }
+% #plaintext quotations? <PRE><% join('', $quotation->print_text() ) %></PRE>
</%doc>
@@ -56,7 +55,7 @@ if ( $query =~ /^(\d+)$/ ) {
$quotationnum = $cgi->param('quotationnum');
}
-#my $conf = new FS::Conf;
+my $conf = new FS::Conf;
my $quotation = qsearchs({
'select' => 'quotation.*',
@@ -67,6 +66,7 @@ my $quotation = qsearchs({
});
die "Quotation #$quotationnum not found!" unless $quotation;
+my $menubar;
if ( my $custnum = $quotation->custnum ) {
my $display_custnum = $quotation->cust_main->display_custnum;
$menubar = menubar(
@@ -78,4 +78,9 @@ if ( my $custnum = $quotation->custnum ) {
);
}
+my $link = "quotationnum=$quotationnum";
+#$link .= ';template='. uri_escape($template) if $template;
+#$link .= ';notice_name='. $notice_name if $notice_name;
+
+
</%init>