diff options
author | Ivan Kohler <ivan@freeside.biz> | 2012-07-03 18:51:51 -0700 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2012-07-03 18:51:51 -0700 |
commit | 81978af92ecdaaefeff5156d9ab3b4f99586df1c (patch) | |
tree | 19a4e6bbcc6648c506c48e304418001294ed7485 | |
parent | 7abce2207dbee012fd442940dc42070f45ef8a16 (diff) |
quotations, RT#16996
-rw-r--r-- | FS/FS/AccessRight.pm | 1 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 26 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 46 | ||||
-rw-r--r-- | FS/FS/Template_Mixin.pm | 2525 | ||||
-rw-r--r-- | FS/FS/Upgrade.pm | 8 | ||||
-rw-r--r-- | FS/FS/access_right.pm | 1 | ||||
-rw-r--r-- | FS/FS/cust_bill.pm | 2475 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 2 | ||||
-rw-r--r-- | FS/FS/prospect_main.pm | 17 | ||||
-rw-r--r-- | FS/FS/quotation.pm | 20 | ||||
-rw-r--r-- | FS/MANIFEST | 8 | ||||
-rw-r--r-- | conf/quotation_html | 266 | ||||
-rw-r--r-- | conf/quotation_latex | 362 | ||||
-rw-r--r-- | conf/quotation_latexnotes | 8 | ||||
-rw-r--r-- | httemplate/edit/process/quotation.html | 2 | ||||
-rw-r--r-- | httemplate/edit/quotation.html | 15 | ||||
-rw-r--r-- | httemplate/elements/tr-fixed-cust_main.html | 15 | ||||
-rw-r--r-- | httemplate/elements/tr-fixed-date.html | 13 | ||||
-rw-r--r-- | httemplate/elements/tr-fixed-prospect_main.html | 15 | ||||
-rw-r--r-- | httemplate/view/prospect_main.html | 31 | ||||
-rwxr-xr-x | httemplate/view/quotation.html | 25 |
21 files changed, 3390 insertions, 2491 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index eb9974adf..4de29481d 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 8c9d56fef..7e641308b 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 a90c73a95..797b70549 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 000000000..19b452e3b --- /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/~/ /g; + $_; + } @_ + }, + 'footer' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ }, + 'smallfooter' => + sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ }, + 'returnaddress' => + sub { + map { + s/~/ /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/ / /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/ +/ /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 8d4b34601..417b2026c 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 1e65ca354..e6266b49b 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 498025f7b..83748be1b 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/~/ /g; - $_; - } @_ - }, - 'footer' => - sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ }, - 'smallfooter' => - sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ }, - 'returnaddress' => - sub { - map { - s/~/ /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/ / /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/ +/ /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 b382232b2..82b09b61f 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 6adc852f8..b5d51d333 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 420233550..0cfb11e2f 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 e7aba204d..590874d46 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 000000000..1dfb94434 --- /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> </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> </th> + </tr> + </table> + </td> + </tr> + + <tr> + <td> + </td> + <td align="left"> + <b><%= $name %></b><BR> + <%= join('<BR>', grep length($_), $company, + $address1, + $address2, + "$city, $state $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, $ship_state $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' ). '>'. + ' '. $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"> </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"> </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 000000000..772c2eb95 --- /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 000000000..58fd68a29 --- /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 7671c3694..a69566581 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 f70642544..8b6062355 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 000000000..00bcb66d8 --- /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 000000000..716e5ceb8 --- /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 000000000..8da0ffb84 --- /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 9e85348af..801d64bda 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 2c2c6b7ca..866ade2c2 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> |