+
+ 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";
+ 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,
+it is taken as the temp directory where the PNG file will be generated and the
+PNG file name is returned. Otherwise, the PNG image itself is returned.
+
+=cut
+
+sub invoice_barcode {
+ my ($self, $dir) = (shift,shift);
+
+ my $gdbar = new GD::Barcode('Code39',$self->invnum);
+ die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
+ my $gd = $gdbar->plot(Height => 30);
+
+ if($dir) {
+ my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
+ DIR => $dir,
+ SUFFIX => '.png',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ print $bh $gd->png or die "cannot write barcode to file: $!\n";
+ my $png_file = $bh->filename;
+ close $bh;
+ return $png_file;
+ }
+ 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)
+
+=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 (dot matrix) text printing will be borked
+sub print_generic {
+
+ my( $self, %params ) = @_;
+ 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);
+ 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",
+ '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,
+
+ );
+
+ 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';
+ my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+ foreach ( qw( contact company address1 address2 city state zip country fax) ){
+ my $method = $prefix.$_;
+ $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+ }
+ $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;
+ $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+ $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
+ $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+ $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+
+ 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;
+
+ foreach my $include (qw( notes footer smallfooter coupon )) {
+
+ 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');
+ }
+
+ $invoice_data{'po_line'} =
+ ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+ ? &$escape_function("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' => 'Previous Charges',
+ 'subtotal' => $other_money_char.
+ sprintf('%.2f', $pr_total),
+ 'summarized' => $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' => 'Taxes, Surcharges, and Fees',
+ 'subtotal' => $taxtotal, # adjusted below
+ 'summarized' => $summarypage ? 'Y' : '',
+ };
+ my $tax_weight = _pkg_category($tax_section->{description})
+ ? _pkg_category($tax_section->{description})->weight
+ : 0;
+ $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
+ $tax_section->{'sort_weight'} = $tax_weight;
+
+
+ my $adjusttotal = 0;
+ my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
+ 'subtotal' => 0, # adjusted below
+ 'summarized' => $summarypage ? 'Y' : '',
+ };
+ my $adjust_weight = _pkg_category($adjust_section->{description})
+ ? _pkg_category($adjust_section->{description})->weight
+ : 0;
+ $adjust_section->{'summarized'} = $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 = ();
+ 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;
+ }
+ }else{
+ push @sections, { 'description' => '', 'subtotal' => '' };
+ }
+
+ unless ( $conf->exists('disable_previous_balance')
+ || $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'} = $previous_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') ) {
+ push @buf, ['','-----------'];
+ push @buf, [ '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 (Past 30 days)';
+ push @detail_items,
+ { 'description' => $didsummary_desc,
+ 'ext_description' => [ $didsummary, $minutes ],
+ }
+ if !$multisection;
+ }
+
+ 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{'format_function'} = sub { () } 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';
+
+ 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',
+ $money_char. sprintf("%10.2f", $section->{'subtotal'})
+ ],
+ [ '', '' ],
+ [ '', '' ],
+ );
+ }
+
+ }
+
+ $invoice_data{current_less_finance} =
+ sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
+
+ if ( $multisection && !$conf->exists('disable_previous_balance')
+ || $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'} = '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,[( $conf->exists('disable_previous_balance')
+ ? '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') ||
+ $conf->exists('previous_balance-exclude_from_total')
+ ? 0
+ : $pr_total
+ );
+ $total->{'total_item'} = &$embolden_function($item);
+ $total->{'total_amount'} =
+ &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
+ if ( $multisection ) {
+ if ( $adjust_section->{'sort_weight'} ) {
+ $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
+ sprintf("%.2f", ($self->billing_balance || 0) );
+ } else {
+ $adjust_section->{'pretotal'} = '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') ) {
+ #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};
+ }
+
+ {
+ 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;
+ }
+
+ 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');
+ }
+