1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use List::Util qw(sum);
11 use Text::Template 1.20;
16 use FS::Record qw( qsearch qsearchs );
17 use FS::Misc qw( generate_ps generate_pdf );
23 $me = '[FS::Template_Mixin]';
24 FS::UID->install_callback( sub {
25 my $conf = new FS::Conf; #global
26 $money_char = $conf->config('money_char') || '$';
27 $date_format = $conf->config('date_format') || '%x'; #/YY
28 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
29 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
32 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
34 Returns an text invoice, as a list of lines.
36 Options can be passed as a hashref (recommended) or as a list of time, template
37 and then any key/value pairs for any other options.
39 I<time>, if specified, is used to control the printing of overdue messages. The
40 default is now. It isn't the date of the invoice; that's the `_date' field.
41 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
42 L<Time::Local> and L<Date::Parse> for conversion functions.
44 I<template>, if specified, is the name of a suffix for alternate invoices.
46 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
52 my( $today, $template, %opt );
55 $today = delete($opt{'time'}) || '';
56 $template = delete($opt{template}) || '';
58 ( $today, $template, %opt ) = @_;
61 my %params = ( 'format' => 'template' );
62 $params{'time'} = $today if $today;
63 $params{'template'} = $template if $template;
64 $params{$_} = $opt{$_}
65 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
67 $self->print_generic( %params );
70 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
72 Internal method - returns a filename of a filled-in LaTeX template for this
73 invoice (Note: add ".tex" to get the actual filename), and a filename of
74 an associated logo (with the .eps extension included).
76 See print_ps and print_pdf for methods that return PostScript and PDF output.
78 Options can be passed as a hashref (recommended) or as a list of time, template
79 and then any key/value pairs for any other options.
81 I<time>, if specified, is used to control the printing of overdue messages. The
82 default is now. It isn't the date of the invoice; that's the `_date' field.
83 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
84 L<Time::Local> and L<Date::Parse> for conversion functions.
86 I<template>, if specified, is the name of a suffix for alternate invoices.
88 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
94 my $conf = $self->conf;
95 my( $today, $template, %opt );
98 $today = delete($opt{'time'}) || '';
99 $template = delete($opt{template}) || '';
101 ( $today, $template, %opt ) = @_;
104 my %params = ( 'format' => 'latex' );
105 $params{'time'} = $today if $today;
106 $params{'template'} = $template if $template;
107 $params{$_} = $opt{$_}
108 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
110 $template ||= $self->_agent_template
111 if $self->can('_agent_template');
113 my $pkey = $self->primary_key;
114 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
116 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
117 my $lh = new File::Temp(
118 TEMPLATE => $tmp_template,
122 ) or die "can't open temp file: $!\n";
124 my $agentnum = $self->cust_main->agentnum;
126 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
127 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
128 or die "can't write temp file: $!\n";
130 print $lh $conf->config_binary('logo.eps', $agentnum)
131 or die "can't write temp file: $!\n";
134 $params{'logo_file'} = $lh->filename;
136 if( $conf->exists('invoice-barcode') && $self->can('invoice_barcode') ) {
137 my $png_file = $self->invoice_barcode($dir);
138 my $eps_file = $png_file;
139 $eps_file =~ s/\.png$/.eps/g;
140 $png_file =~ /(barcode.*png)/;
142 $eps_file =~ /(barcode.*eps)/;
145 my $curr_dir = cwd();
147 # after painfuly long experimentation, it was determined that sam2p won't
148 # accept : and other chars in the path, no matter how hard I tried to
149 # escape them, hence the chdir (and chdir back, just to be safe)
150 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
151 or die "sam2p failed: $!\n";
155 $params{'barcode_file'} = $eps_file;
158 my @filled_in = $self->print_generic( %params );
160 my $fh = new File::Temp( TEMPLATE => $tmp_template,
164 ) or die "can't open temp file: $!\n";
165 binmode($fh, ':utf8'); # language support
166 print $fh join('', @filled_in );
169 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
170 return ($1, $params{'logo_file'}, $params{'barcode_file'});
174 =item print_generic OPTION => VALUE ...
176 Internal method - returns a filled-in template for this invoice as a scalar.
178 See print_ps and print_pdf for methods that return PostScript and PDF output.
180 Non optional options include
181 format - latex, html, template
183 Optional options include
185 template - a value used as a suffix for a configuration template
187 time - a value used to control the printing of overdue messages. The
188 default is now. It isn't the date of the invoice; that's the `_date' field.
189 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
190 L<Time::Local> and L<Date::Parse> for conversion functions.
194 unsquelch_cdr - overrides any per customer cdr squelching when true
196 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
198 locale - override customer's locale
202 #what's with all the sprintf('%10.2f')'s in here? will it cause any
203 # (alignment in text invoice?) problems to change them all to '%.2f' ?
204 # yes: fixed width/plain text printing will be borked
206 my( $self, %params ) = @_;
207 my $conf = $self->conf;
208 my $today = $params{today} ? $params{today} : time;
209 warn "$me print_generic called on $self with suffix $params{template}\n"
212 my $format = $params{format};
213 die "Unknown format: $format"
214 unless $format =~ /^(latex|html|template)$/;
216 my $cust_main = $self->cust_main || $self->prospect_main;
217 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
218 unless $cust_main->payname
219 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
221 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
222 'html' => [ '<%=', '%>' ],
223 'template' => [ '{', '}' ],
226 warn "$me print_generic creating template\n"
230 my $template = $params{template} ? $params{template} : $self->_agent_template;
231 my $templatefile = $self->template_conf. $format;
232 $templatefile .= "_$template"
233 if length($template) && $conf->exists($templatefile."_$template");
234 my @invoice_template = map "$_\n", $conf->config($templatefile)
235 or die "cannot load config data $templatefile";
238 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
239 #change this to a die when the old code is removed
240 warn "old-style invoice template $templatefile; ".
241 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
243 @invoice_template = _translate_old_latex_format(@invoice_template);
246 warn "$me print_generic creating T:T object\n"
249 my $text_template = new Text::Template(
251 SOURCE => \@invoice_template,
252 DELIMITERS => $delimiters{$format},
255 warn "$me print_generic compiling T:T object\n"
258 $text_template->compile()
259 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
262 # additional substitution could possibly cause breakage in existing templates
265 'notes' => sub { map "$_", @_ },
266 'footer' => sub { map "$_", @_ },
267 'smallfooter' => sub { map "$_", @_ },
268 'returnaddress' => sub { map "$_", @_ },
269 'coupon' => sub { map "$_", @_ },
270 'summary' => sub { map "$_", @_ },
276 s/%%(.*)$/<!-- $1 -->/g;
277 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
278 s/\\begin\{enumerate\}/<ol>/g;
280 s/\\end\{enumerate\}/<\/ol>/g;
281 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
290 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
292 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
298 s/\\hyphenation\{[\w\s\-]+}//;
303 'coupon' => sub { "" },
304 'summary' => sub { "" },
311 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
312 s/\\begin\{enumerate\}//g;
314 s/\\end\{enumerate\}//g;
315 s/\\textbf\{(.*)\}/$1/g;
322 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
324 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
329 s/\\\\\*?\s*$/\n/; # dubious
330 s/\\hyphenation\{[\w\s\-]+}//;
334 'coupon' => sub { "" },
335 'summary' => sub { "" },
340 # hashes for differing output formats
341 my %nbsps = ( 'latex' => '~',
342 'html' => '', # '&nbps;' would be nice
343 'template' => '', # not used
345 my $nbsp = $nbsps{$format};
347 my %escape_functions = ( 'latex' => \&_latex_escape,
348 'html' => \&_html_escape_nbsp,#\&encode_entities,
349 'template' => sub { shift },
351 my $escape_function = $escape_functions{$format};
352 my $escape_function_nonbsp = ($format eq 'html')
353 ? \&_html_escape : $escape_function;
355 my %date_formats = ( 'latex' => $date_format_long,
356 'html' => $date_format_long,
359 $date_formats{'html'} =~ s/ / /g;
361 my $date_format = $date_formats{$format};
363 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
365 'html' => sub { return '<b>'. shift(). '</b>'
367 'template' => sub { shift },
369 my $embolden_function = $embolden_functions{$format};
371 my %newline_tokens = ( 'latex' => '\\\\',
375 my $newline_token = $newline_tokens{$format};
377 warn "$me generating template variables\n"
380 # generate template variables
383 defined( $conf->config_orbase( "invoice_${format}returnaddress",
387 && length( $conf->config_orbase( "invoice_${format}returnaddress",
393 $returnaddress = join("\n",
394 $conf->config_orbase("invoice_${format}returnaddress", $template)
398 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
400 my $convert_map = $convert_maps{$format}{'returnaddress'};
403 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
408 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
410 my $convert_map = $convert_maps{$format}{'returnaddress'};
411 $returnaddress = join( "\n", &$convert_map(
412 map { s/( {2,})/'~' x length($1)/eg;
416 ( $conf->config('company_name', $cust_main->agentnum),
417 $conf->config('company_address', $cust_main->agentnum),
424 my $warning = "Couldn't find a return address; ".
425 "do you need to set the company_address configuration value?";
427 $returnaddress = $nbsp;
428 #$returnaddress = $warning;
432 warn "$me generating invoice data\n"
435 my $agentnum = $cust_main->agentnum;
440 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
441 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
442 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
443 'returnaddress' => $returnaddress,
444 'agent' => &$escape_function($cust_main->agent->agent),
446 #invoice/quotation info
447 'invnum' => $self->invnum,
448 'quotationnum' => $self->quotationnum,
449 'date' => time2str($date_format, $self->_date),
450 'today' => time2str($date_format_long, $today),
451 'terms' => $self->terms,
452 'template' => $template, #params{'template'},
453 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
454 'current_charges' => sprintf("%.2f", $self->charged),
455 'duedate' => $self->due_date2str($rdate_format), #date_format?
458 'custnum' => $cust_main->display_custnum,
459 'prospectnum' => $cust_main->prospectnum,
460 'agent_custid' => &$escape_function($cust_main->agent_custid),
461 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
462 payname company address1 address2 city state zip fax
466 'ship_enable' => $conf->exists('invoice-ship_address'),
467 'unitprices' => $conf->exists('invoice-unitprice'),
468 'smallernotes' => $conf->exists('invoice-smallernotes'),
469 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
470 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
472 #layout info -- would be fancy to calc some of this and bury the template
474 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
475 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
476 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
477 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
478 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
479 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
480 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
481 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
482 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
483 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
485 # better hang on to conf_dir for a while (for old templates)
486 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
488 #these are only used when doing paged plaintext
495 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
496 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
497 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
498 # eval to avoid death for unimplemented languages
499 my $dh = eval { Date::Language->new($info{'name'}) } ||
500 Date::Language->new(); # fall back to English
501 # prototype here to silence warnings
502 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
503 # eventually use this date handle everywhere in here, too
505 my $min_sdate = 999999999999;
507 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
508 next unless $cust_bill_pkg->pkgnum > 0;
509 $min_sdate = $cust_bill_pkg->sdate
510 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
511 $max_edate = $cust_bill_pkg->edate
512 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
515 $invoice_data{'bill_period'} = '';
516 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
517 . " to " . time2str('%e %h', $max_edate)
518 if ($max_edate != 0 && $min_sdate != 999999999999);
520 $invoice_data{finance_section} = '';
521 if ( $conf->config('finance_pkgclass') ) {
523 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
524 $invoice_data{finance_section} = $pkg_class->categoryname;
526 $invoice_data{finance_amount} = '0.00';
527 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
529 my $countrydefault = $conf->config('countrydefault') || 'US';
530 foreach ( qw( address1 address2 city state zip country fax) ){
531 my $method = 'ship_'.$_;
532 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
534 foreach ( qw( contact company ) ) { #compatibility
535 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
537 $invoice_data{'ship_country'} = ''
538 if ( $invoice_data{'ship_country'} eq $countrydefault );
540 $invoice_data{'cid'} = $params{'cid'}
543 if ( $cust_main->country eq $countrydefault ) {
544 $invoice_data{'country'} = '';
546 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
550 $invoice_data{'address'} = \@address;
553 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
554 ? " (P.O. #". $cust_main->payinfo. ")"
558 push @address, $cust_main->company
559 if $cust_main->company;
560 push @address, $cust_main->address1;
561 push @address, $cust_main->address2
562 if $cust_main->address2;
564 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
565 push @address, $invoice_data{'country'}
566 if $invoice_data{'country'};
568 while (scalar(@address) < 5);
570 $invoice_data{'logo_file'} = $params{'logo_file'}
571 if $params{'logo_file'};
572 $invoice_data{'barcode_file'} = $params{'barcode_file'}
573 if $params{'barcode_file'};
574 $invoice_data{'barcode_img'} = $params{'barcode_img'}
575 if $params{'barcode_img'};
576 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
577 if $params{'barcode_cid'};
579 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
580 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
581 #my $balance_due = $self->owed + $pr_total - $cr_total;
582 my $balance_due = $self->owed + $pr_total;
584 # the customer's current balance as shown on the invoice before this one
585 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
587 # the change in balance from that invoice to this one
588 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
590 # the sum of amount owed on all previous invoices
591 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
593 # the sum of amount owed on all invoices
594 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
596 # info from customer's last invoice before this one, for some
598 $invoice_data{'last_bill'} = {};
599 my $last_bill = $pr_cust_bill[-1];
601 $invoice_data{'last_bill'} = {
602 '_date' => $last_bill->_date, #unformatted
603 # all we need for now
607 my $summarypage = '';
608 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
611 $invoice_data{'summarypage'} = $summarypage;
613 warn "$me substituting variables in notes, footer, smallfooter\n"
616 my $tc = $self->template_conf;
617 my @include = ( [ $tc, 'notes' ],
618 [ 'invoice_', 'footer' ],
619 [ 'invoice_', 'smallfooter', ],
621 push @include, [ $tc, 'coupon', ]
622 unless $params{'no_coupon'};
624 foreach my $i (@include) {
626 my($base, $include) = @$i;
628 my $inc_file = $conf->key_orbase("$base$format$include", $template);
631 if ( $conf->exists($inc_file, $agentnum)
632 && length( $conf->config($inc_file, $agentnum) ) ) {
634 @inc_src = $conf->config($inc_file, $agentnum);
638 $inc_file = $conf->key_orbase("${base}latex$include", $template);
640 my $convert_map = $convert_maps{$format}{$include};
642 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
643 s/--\@\]/$delimiters{$format}[1]/g;
646 &$convert_map( $conf->config($inc_file, $agentnum) );
650 my $inc_tt = new Text::Template (
652 SOURCE => [ map "$_\n", @inc_src ],
653 DELIMITERS => $delimiters{$format},
654 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
656 unless ( $inc_tt->compile() ) {
657 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
658 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
662 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
664 $invoice_data{$include} =~ s/\n+$//
665 if ($format eq 'latex');
668 # let invoices use either of these as needed
669 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
670 ? $cust_main->payinfo : '';
671 $invoice_data{'po_line'} =
672 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
673 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
676 my %money_chars = ( 'latex' => '',
677 'html' => $conf->config('money_char') || '$',
680 my $money_char = $money_chars{$format};
682 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
683 'html' => $conf->config('money_char') || '$',
686 my $other_money_char = $other_money_chars{$format};
687 $invoice_data{'dollar'} = $other_money_char;
689 my @detail_items = ();
690 my @total_items = ();
694 $invoice_data{'detail_items'} = \@detail_items;
695 $invoice_data{'total_items'} = \@total_items;
696 $invoice_data{'buf'} = \@buf;
697 $invoice_data{'sections'} = \@sections;
699 warn "$me generating sections\n"
702 my $previous_section = { 'description' => $self->mt('Previous Charges'),
703 'subtotal' => $other_money_char.
704 sprintf('%.2f', $pr_total),
705 'summarized' => '', #why? $summarypage ? 'Y' : '',
707 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
708 join(' / ', map { $cust_main->balance_date_range(@$_) }
709 $self->_prior_month30s
711 if $conf->exists('invoice_include_aging');
714 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
715 'subtotal' => $taxtotal, # adjusted below
717 my $tax_weight = _pkg_category($tax_section->{description})
718 ? _pkg_category($tax_section->{description})->weight
720 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
721 $tax_section->{'sort_weight'} = $tax_weight;
725 my $adjust_section = { 'description' =>
726 $self->mt('Credits, Payments, and Adjustments'),
727 'subtotal' => 0, # adjusted below
729 my $adjust_weight = _pkg_category($adjust_section->{description})
730 ? _pkg_category($adjust_section->{description})->weight
732 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
733 $adjust_section->{'sort_weight'} = $adjust_weight;
735 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
736 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
737 $invoice_data{'multisection'} = $multisection;
738 my $late_sections = [];
739 my $extra_sections = [];
740 my $extra_lines = ();
742 my $default_section = { 'description' => '',
747 if ( $multisection ) {
748 ($extra_sections, $extra_lines) =
749 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
750 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
751 && $self->can('_items_extra_usage_sections');
753 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
755 push @detail_items, @$extra_lines if $extra_lines;
757 $self->_items_sections( $late_sections, # this could stand a refactor
759 $escape_function_nonbsp,
763 if ( $conf->exists('svc_phone_sections')
764 && $self->can('_items_svc_phone_sections')
767 my ($phone_sections, $phone_lines) =
768 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
769 push @{$late_sections}, @$phone_sections;
770 push @detail_items, @$phone_lines;
772 if ( $conf->exists('voip-cust_accountcode_cdr')
773 && $cust_main->accountcode_cdr
774 && $self->can('_items_accountcode_cdr')
777 my ($accountcode_section, $accountcode_lines) =
778 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
779 if ( scalar(@$accountcode_lines) ) {
780 push @{$late_sections}, $accountcode_section;
781 push @detail_items, @$accountcode_lines;
784 } else {# not multisection
785 # make a default section
786 push @sections, $default_section;
787 # and calculate the finance charge total, since it won't get done otherwise.
788 # XXX possibly other totals?
789 # XXX possibly finance_pkgclass should not be used in this manner?
790 if ( $conf->exists('finance_pkgclass') ) {
792 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
793 if ( grep { $_->section eq $invoice_data{finance_section} }
794 $cust_bill_pkg->cust_bill_pkg_display ) {
795 # I think these are always setup fees, but just to be sure...
796 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
799 $invoice_data{finance_amount} =
800 sprintf('%.2f', sum( @finance_charges ) || 0);
804 unless ( $conf->exists('disable_previous_balance', $agentnum)
805 || $conf->exists('previous_balance-summary_only')
806 || ! $self->can('_items_previous')
810 warn "$me adding previous balances\n"
813 foreach my $line_item ( $self->_items_previous ) {
816 ext_description => [],
818 $detail->{'ref'} = $line_item->{'pkgnum'};
819 $detail->{'quantity'} = 1;
820 $detail->{'section'} = $multisection ? $previous_section
822 $detail->{'description'} = &$escape_function($line_item->{'description'});
823 if ( exists $line_item->{'ext_description'} ) {
824 @{$detail->{'ext_description'}} = map {
825 &$escape_function($_);
826 } @{$line_item->{'ext_description'}};
828 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
829 $line_item->{'amount'};
830 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
832 push @detail_items, $detail;
833 push @buf, [ $detail->{'description'},
834 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
840 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
842 push @buf, ['','-----------'];
843 push @buf, [ $self->mt('Total Previous Balance'),
844 $money_char. sprintf("%10.2f", $pr_total) ];
848 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
849 warn "$me adding DID summary\n"
852 my ($didsummary,$minutes) = $self->_did_summary;
853 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
855 { 'description' => $didsummary_desc,
856 'ext_description' => [ $didsummary, $minutes ],
860 foreach my $section (@sections, @$late_sections) {
862 warn "$me adding section \n". Dumper($section)
865 # begin some normalization
866 $section->{'subtotal'} = $section->{'amount'}
868 && !exists($section->{subtotal})
869 && exists($section->{amount});
871 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
872 if ( $invoice_data{finance_section} &&
873 $section->{'description'} eq $invoice_data{finance_section} );
875 $section->{'subtotal'} = $other_money_char.
876 sprintf('%.2f', $section->{'subtotal'})
879 # continue some normalization
880 $section->{'amount'} = $section->{'subtotal'}
884 if ( $section->{'description'} ) {
885 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
890 warn "$me setting options\n"
893 my $multilocation = scalar($cust_main->cust_location); #too expensive?
895 $options{'section'} = $section if $multisection;
896 $options{'format'} = $format;
897 $options{'escape_function'} = $escape_function;
898 $options{'no_usage'} = 1 unless $unsquelched;
899 $options{'unsquelched'} = $unsquelched;
900 $options{'summary_page'} = $summarypage;
901 $options{'skip_usage'} =
902 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
903 $options{'multilocation'} = $multilocation;
904 $options{'multisection'} = $multisection;
906 warn "$me searching for line items\n"
909 foreach my $line_item ( $self->_items_pkg(%options) ) {
911 warn "$me adding line item $line_item\n"
915 ext_description => [],
917 $detail->{'ref'} = $line_item->{'pkgnum'};
918 $detail->{'quantity'} = $line_item->{'quantity'};
919 $detail->{'section'} = $section;
920 $detail->{'description'} = &$escape_function($line_item->{'description'});
921 if ( exists $line_item->{'ext_description'} ) {
922 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
924 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
925 $line_item->{'amount'};
926 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
927 $line_item->{'unit_amount'};
928 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
930 $detail->{'sdate'} = $line_item->{'sdate'};
931 $detail->{'edate'} = $line_item->{'edate'};
932 $detail->{'seconds'} = $line_item->{'seconds'};
934 push @detail_items, $detail;
935 push @buf, ( [ $detail->{'description'},
936 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
938 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
942 if ( $section->{'description'} ) {
943 push @buf, ( ['','-----------'],
944 [ $section->{'description'}. ' sub-total',
945 $section->{'subtotal'} # already formatted this
954 $invoice_data{current_less_finance} =
955 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
957 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
958 || $conf->exists('previous_balance-summary_only') )
960 unshift @sections, $previous_section if $pr_total;
963 warn "$me adding taxes\n"
966 foreach my $tax ( $self->_items_tax ) {
968 $taxtotal += $tax->{'amount'};
970 my $description = &$escape_function( $tax->{'description'} );
971 my $amount = sprintf( '%.2f', $tax->{'amount'} );
973 if ( $multisection ) {
975 my $money = $old_latex ? '' : $money_char;
976 push @detail_items, {
977 ext_description => [],
980 description => $description,
981 amount => $money. $amount,
983 section => $tax_section,
989 'total_item' => $description,
990 'total_amount' => $other_money_char. $amount,
995 push @buf,[ $description,
996 $money_char. $amount,
1003 $total->{'total_item'} = $self->mt('Sub-total');
1004 $total->{'total_amount'} =
1005 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1007 if ( $multisection ) {
1008 $tax_section->{'subtotal'} = $other_money_char.
1009 sprintf('%.2f', $taxtotal);
1010 $tax_section->{'pretotal'} = 'New charges sub-total '.
1011 $total->{'total_amount'};
1012 push @sections, $tax_section if $taxtotal;
1014 unshift @total_items, $total;
1017 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1019 push @buf,['','-----------'];
1020 push @buf,[$self->mt(
1021 $conf->exists('disable_previous_balance', $agentnum)
1023 : 'Total New Charges'
1025 $money_char. sprintf("%10.2f",$self->charged) ];
1031 $item = $conf->config('previous_balance-exclude_from_total')
1032 || 'Total New Charges'
1033 if $conf->exists('previous_balance-exclude_from_total');
1034 my $amount = $self->charged +
1035 ( $conf->exists('disable_previous_balance', $agentnum) ||
1036 $conf->exists('previous_balance-exclude_from_total')
1040 $total->{'total_item'} = &$embolden_function($self->mt($item));
1041 $total->{'total_amount'} =
1042 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1043 if ( $multisection ) {
1044 if ( $adjust_section->{'sort_weight'} ) {
1045 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1046 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1048 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1049 $other_money_char. sprintf('%.2f', $self->charged );
1052 push @total_items, $total;
1054 push @buf,['','-----------'];
1057 sprintf( '%10.2f', $amount )
1062 unless ( $conf->exists('disable_previous_balance', $agentnum)
1063 || ! $self->can('_items_credits')
1064 || ! $self->can('_items_payments')
1067 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1070 my $credittotal = 0;
1071 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1074 $total->{'total_item'} = &$escape_function($credit->{'description'});
1075 $credittotal += $credit->{'amount'};
1076 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1077 $adjusttotal += $credit->{'amount'};
1078 if ( $multisection ) {
1079 my $money = $old_latex ? '' : $money_char;
1080 push @detail_items, {
1081 ext_description => [],
1084 description => &$escape_function($credit->{'description'}),
1085 amount => $money. $credit->{'amount'},
1087 section => $adjust_section,
1090 push @total_items, $total;
1094 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1097 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1098 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1102 my $paymenttotal = 0;
1103 foreach my $payment ( $self->_items_payments ) {
1105 $total->{'total_item'} = &$escape_function($payment->{'description'});
1106 $paymenttotal += $payment->{'amount'};
1107 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1108 $adjusttotal += $payment->{'amount'};
1109 if ( $multisection ) {
1110 my $money = $old_latex ? '' : $money_char;
1111 push @detail_items, {
1112 ext_description => [],
1115 description => &$escape_function($payment->{'description'}),
1116 amount => $money. $payment->{'amount'},
1118 section => $adjust_section,
1121 push @total_items, $total;
1123 push @buf, [ $payment->{'description'},
1124 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1127 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1129 if ( $multisection ) {
1130 $adjust_section->{'subtotal'} = $other_money_char.
1131 sprintf('%.2f', $adjusttotal);
1132 push @sections, $adjust_section
1133 unless $adjust_section->{sort_weight};
1136 # create Balance Due message
1139 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1140 $total->{'total_amount'} =
1141 &$embolden_function(
1142 $other_money_char. sprintf('%.2f', $summarypage
1144 $self->billing_balance
1145 : $self->owed + $pr_total
1148 if ( $multisection && !$adjust_section->{sort_weight} ) {
1149 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1150 $total->{'total_amount'};
1152 push @total_items, $total;
1154 push @buf,['','-----------'];
1155 push @buf,[$self->balance_due_msg, $money_char.
1156 sprintf("%10.2f", $balance_due ) ];
1159 if ( $conf->exists('previous_balance-show_credit')
1160 and $cust_main->balance < 0 ) {
1161 my $credit_total = {
1162 'total_item' => &$embolden_function($self->credit_balance_msg),
1163 'total_amount' => &$embolden_function(
1164 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1167 if ( $multisection ) {
1168 $adjust_section->{'posttotal'} .= $newline_token .
1169 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1172 push @total_items, $credit_total;
1174 push @buf,['','-----------'];
1175 push @buf,[$self->credit_balance_msg, $money_char.
1176 sprintf("%10.2f", -$cust_main->balance ) ];
1180 if ( $multisection ) {
1181 if ( $conf->exists('svc_phone_sections')
1182 && $self->can('_items_svc_phone_sections')
1186 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1187 $total->{'total_amount'} =
1188 &$embolden_function(
1189 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1191 my $last_section = pop @sections;
1192 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1193 $total->{'total_amount'};
1194 push @sections, $last_section;
1196 push @sections, @$late_sections
1200 # make a discounts-available section, even without multisection
1201 if ( $conf->exists('discount-show_available')
1202 and my @discounts_avail = $self->_items_discounts_avail ) {
1203 my $discount_section = {
1204 'description' => $self->mt('Discounts Available'),
1209 push @sections, $discount_section;
1210 push @detail_items, map { +{
1211 'ref' => '', #should this be something else?
1212 'section' => $discount_section,
1213 'description' => &$escape_function( $_->{description} ),
1214 'amount' => $money_char . &$escape_function( $_->{amount} ),
1215 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1216 } } @discounts_avail;
1219 # All sections and items are built; now fill in templates.
1220 my @includelist = ();
1221 push @includelist, 'summary' if $summarypage;
1222 foreach my $include ( @includelist ) {
1224 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1227 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1229 @inc_src = $conf->config($inc_file, $agentnum);
1233 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1235 my $convert_map = $convert_maps{$format}{$include};
1237 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1238 s/--\@\]/$delimiters{$format}[1]/g;
1241 &$convert_map( $conf->config($inc_file, $agentnum) );
1245 my $inc_tt = new Text::Template (
1247 SOURCE => [ map "$_\n", @inc_src ],
1248 DELIMITERS => $delimiters{$format},
1249 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1251 unless ( $inc_tt->compile() ) {
1252 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1253 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1257 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1259 $invoice_data{$include} =~ s/\n+$//
1260 if ($format eq 'latex');
1265 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1266 /invoice_lines\((\d*)\)/;
1267 $invoice_lines += $1 || scalar(@buf);
1270 die "no invoice_lines() functions in template?"
1271 if ( $format eq 'template' && !$wasfunc );
1273 if ($format eq 'template') {
1275 if ( $invoice_lines ) {
1276 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1277 $invoice_data{'total_pages'}++
1278 if scalar(@buf) % $invoice_lines;
1281 #setup subroutine for the template
1282 $invoice_data{invoice_lines} = sub {
1283 my $lines = shift || scalar(@buf);
1295 push @collect, split("\n",
1296 $text_template->fill_in( HASH => \%invoice_data )
1298 $invoice_data{'page'}++;
1300 map "$_\n", @collect;
1302 } else { # this is where we actually create the invoice
1304 warn "filling in template for invoice ". $self->invnum. "\n"
1306 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1309 $text_template->fill_in(HASH => \%invoice_data);
1313 sub notice_name { '('.shift->table.')'; }
1315 sub template_conf { 'invoice_'; }
1317 # helper routine for generating date ranges
1318 sub _prior_month30s {
1321 [ 1, 2592000 ], # 0-30 days ago
1322 [ 2592000, 5184000 ], # 30-60 days ago
1323 [ 5184000, 7776000 ], # 60-90 days ago
1324 [ 7776000, 0 ], # 90+ days ago
1327 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1328 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1333 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1335 Returns an postscript invoice, as a scalar.
1337 Options can be passed as a hashref (recommended) or as a list of time, template
1338 and then any key/value pairs for any other options.
1340 I<time> an optional value used to control the printing of overdue messages. The
1341 default is now. It isn't the date of the invoice; that's the `_date' field.
1342 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1343 L<Time::Local> and L<Date::Parse> for conversion functions.
1345 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1352 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1353 my $ps = generate_ps($file);
1355 unlink($barcodefile) if $barcodefile;
1360 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1362 Returns an PDF invoice, as a scalar.
1364 Options can be passed as a hashref (recommended) or as a list of time, template
1365 and then any key/value pairs for any other options.
1367 I<time> an optional value used to control the printing of overdue messages. The
1368 default is now. It isn't the date of the invoice; that's the `_date' field.
1369 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1370 L<Time::Local> and L<Date::Parse> for conversion functions.
1372 I<template>, if specified, is the name of a suffix for alternate invoices.
1374 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1381 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1382 my $pdf = generate_pdf($file);
1384 unlink($barcodefile) if $barcodefile;
1389 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1391 Returns an HTML invoice, as a scalar.
1393 I<time> an optional value used to control the printing of overdue messages. The
1394 default is now. It isn't the date of the invoice; that's the `_date' field.
1395 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1396 L<Time::Local> and L<Date::Parse> for conversion functions.
1398 I<template>, if specified, is the name of a suffix for alternate invoices.
1400 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1402 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1403 when emailing the invoice as part of a multipart/related MIME email.
1411 %params = %{ shift() };
1413 $params{'time'} = shift;
1414 $params{'template'} = shift;
1415 $params{'cid'} = shift;
1418 $params{'format'} = 'html';
1420 $self->print_generic( %params );
1423 # quick subroutine for print_latex
1425 # There are ten characters that LaTeX treats as special characters, which
1426 # means that they do not simply typeset themselves:
1427 # # $ % & ~ _ ^ \ { }
1429 # TeX ignores blanks following an escaped character; if you want a blank (as
1430 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1434 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1435 $value =~ s/([<>])/\$$1\$/g;
1441 encode_entities($value);
1445 sub _html_escape_nbsp {
1446 my $value = _html_escape(shift);
1447 $value =~ s/ +/ /g;
1451 #utility methods for print_*
1453 sub _translate_old_latex_format {
1454 warn "_translate_old_latex_format called\n"
1461 if ( $line =~ /^%%Detail\s*$/ ) {
1463 push @template, q![@--!,
1464 q! foreach my $_tr_line (@detail_items) {!,
1465 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1466 q! $_tr_line->{'description'} .= !,
1467 q! "\\tabularnewline\n~~".!,
1468 q! join( "\\tabularnewline\n~~",!,
1469 q! @{$_tr_line->{'ext_description'}}!,
1473 while ( ( my $line_item_line = shift )
1474 !~ /^%%EndDetail\s*$/ ) {
1475 $line_item_line =~ s/'/\\'/g; # nice LTS
1476 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1477 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1478 push @template, " \$OUT .= '$line_item_line';";
1481 push @template, '}',
1484 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1486 push @template, '[@--',
1487 ' foreach my $_tr_line (@total_items) {';
1489 while ( ( my $total_item_line = shift )
1490 !~ /^%%EndTotalDetails\s*$/ ) {
1491 $total_item_line =~ s/'/\\'/g; # nice LTS
1492 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1493 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1494 push @template, " \$OUT .= '$total_item_line';";
1497 push @template, '}',
1501 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1502 push @template, $line;
1508 warn "$_\n" foreach @template;
1516 my $conf = $self->conf;
1518 #check for an invoice-specific override
1519 return $self->invoice_terms if $self->invoice_terms;
1521 #check for a customer- specific override
1522 my $cust_main = $self->cust_main;
1523 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1525 #use configured default
1526 $conf->config('invoice_default_terms') || '';
1532 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1533 $duedate = $self->_date() + ( $1 * 86400 );
1540 $self->due_date ? time2str(shift, $self->due_date) : '';
1543 sub balance_due_msg {
1545 my $msg = $self->mt('Balance Due');
1546 return $msg unless $self->terms;
1547 if ( $self->due_date ) {
1548 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1549 $self->due_date2str($date_format);
1550 } elsif ( $self->terms ) {
1551 $msg .= ' - '. $self->terms;
1556 sub balance_due_date {
1558 my $conf = $self->conf;
1560 if ( $conf->exists('invoice_default_terms')
1561 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1562 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1567 sub credit_balance_msg {
1569 $self->mt('Credit Balance Remaining')
1574 Returns a string with the date, for example: "3/20/2008"
1580 time2str($date_format, $self->_date);
1583 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1585 Generate section information for all items appearing on this invoice.
1586 This will only be called for multi-section invoices.
1588 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1589 related display records (L<FS::cust_bill_pkg_display>) and organize
1590 them into two groups ("early" and "late" according to whether they come
1591 before or after the total), then into sections. A subtotal is calculated
1594 Section descriptions are returned in sort weight order. Each consists
1595 of a hash containing:
1597 description: the package category name, escaped
1598 subtotal: the total charges in that section
1599 tax_section: a flag indicating that the section contains only tax charges
1600 summarized: same as tax_section, for some reason
1601 sort_weight: the package category's sort weight
1603 If 'condense' is set on the display record, it also contains everything
1604 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1605 coderefs to generate parts of the invoice. This is not advised.
1609 LATE: an arrayref to push the "late" section hashes onto. The "early"
1610 group is simply returned from the method.
1612 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1613 Turning this on has the following effects:
1614 - Ignores display items with the 'summary' flag.
1615 - Combines all items into the "early" group.
1616 - Creates sections for all non-disabled package categories, even if they
1617 have no charges on this invoice, as well as a section with no name.
1619 ESCAPE: an escape function to use for section titles.
1621 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1622 sorted list. If there are any of these, section subtotals exclude
1625 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1626 passed through to C<_condense_section()>.
1630 use vars qw(%pkg_category_cache);
1631 sub _items_sections {
1634 my $summarypage = shift;
1636 my $extra_sections = shift;
1640 my %late_subtotal = ();
1643 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1646 my $usage = $cust_bill_pkg->usage;
1648 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1649 next if ( $display->summary && $summarypage );
1651 my $section = $display->section;
1652 my $type = $display->type;
1654 $not_tax{$section} = 1
1655 unless $cust_bill_pkg->pkgnum == 0;
1657 if ( $display->post_total && !$summarypage ) {
1658 if (! $type || $type eq 'S') {
1659 $late_subtotal{$section} += $cust_bill_pkg->setup
1660 if $cust_bill_pkg->setup != 0
1661 || $cust_bill_pkg->setup_show_zero;
1665 $late_subtotal{$section} += $cust_bill_pkg->recur
1666 if $cust_bill_pkg->recur != 0
1667 || $cust_bill_pkg->recur_show_zero;
1670 if ($type && $type eq 'R') {
1671 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1672 if $cust_bill_pkg->recur != 0
1673 || $cust_bill_pkg->recur_show_zero;
1676 if ($type && $type eq 'U') {
1677 $late_subtotal{$section} += $usage
1678 unless scalar(@$extra_sections);
1683 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1685 if (! $type || $type eq 'S') {
1686 $subtotal{$section} += $cust_bill_pkg->setup
1687 if $cust_bill_pkg->setup != 0
1688 || $cust_bill_pkg->setup_show_zero;
1692 $subtotal{$section} += $cust_bill_pkg->recur
1693 if $cust_bill_pkg->recur != 0
1694 || $cust_bill_pkg->recur_show_zero;
1697 if ($type && $type eq 'R') {
1698 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1699 if $cust_bill_pkg->recur != 0
1700 || $cust_bill_pkg->recur_show_zero;
1703 if ($type && $type eq 'U') {
1704 $subtotal{$section} += $usage
1705 unless scalar(@$extra_sections);
1714 %pkg_category_cache = ();
1716 push @$late, map { { 'description' => &{$escape}($_),
1717 'subtotal' => $late_subtotal{$_},
1719 'sort_weight' => ( _pkg_category($_)
1720 ? _pkg_category($_)->weight
1723 ((_pkg_category($_) && _pkg_category($_)->condense)
1724 ? $self->_condense_section($format)
1728 sort _sectionsort keys %late_subtotal;
1731 if ( $summarypage ) {
1732 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1733 map { $_->categoryname } qsearch('pkg_category', {});
1734 push @sections, '' if exists($subtotal{''});
1736 @sections = keys %subtotal;
1739 my @early = map { { 'description' => &{$escape}($_),
1740 'subtotal' => $subtotal{$_},
1741 'summarized' => $not_tax{$_} ? '' : 'Y',
1742 'tax_section' => $not_tax{$_} ? '' : 'Y',
1743 'sort_weight' => ( _pkg_category($_)
1744 ? _pkg_category($_)->weight
1747 ((_pkg_category($_) && _pkg_category($_)->condense)
1748 ? $self->_condense_section($format)
1753 push @early, @$extra_sections if $extra_sections;
1755 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1759 #helper subs for above
1762 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1766 my $categoryname = shift;
1767 $pkg_category_cache{$categoryname} ||=
1768 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1771 my %condensed_format = (
1772 'label' => [ qw( Description Qty Amount ) ],
1774 sub { shift->{description} },
1775 sub { shift->{quantity} },
1776 sub { my($href, %opt) = @_;
1777 ($opt{dollar} || ''). $href->{amount};
1780 'align' => [ qw( l r r ) ],
1781 'span' => [ qw( 5 1 1 ) ], # unitprices?
1782 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1785 sub _condense_section {
1786 my ( $self, $format ) = ( shift, shift );
1788 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1789 qw( description_generator
1792 total_line_generator
1797 sub _condensed_generator_defaults {
1798 my ( $self, $format ) = ( shift, shift );
1799 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1808 sub _condensed_header_generator {
1809 my ( $self, $format ) = ( shift, shift );
1811 my ( $f, $prefix, $suffix, $separator, $column ) =
1812 _condensed_generator_defaults($format);
1814 if ($format eq 'latex') {
1815 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1816 $suffix = "\\\\\n\\hline";
1819 sub { my ($d,$a,$s,$w) = @_;
1820 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1822 } elsif ( $format eq 'html' ) {
1823 $prefix = '<th></th>';
1827 sub { my ($d,$a,$s,$w) = @_;
1828 return qq!<th align="$html_align{$a}">$d</th>!;
1836 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1838 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1841 $prefix. join($separator, @result). $suffix;
1846 sub _condensed_description_generator {
1847 my ( $self, $format ) = ( shift, shift );
1849 my ( $f, $prefix, $suffix, $separator, $column ) =
1850 _condensed_generator_defaults($format);
1852 my $money_char = '$';
1853 if ($format eq 'latex') {
1854 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1856 $separator = " & \n";
1858 sub { my ($d,$a,$s,$w) = @_;
1859 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1861 $money_char = '\\dollar';
1862 }elsif ( $format eq 'html' ) {
1863 $prefix = '"><td align="center"></td>';
1867 sub { my ($d,$a,$s,$w) = @_;
1868 return qq!<td align="$html_align{$a}">$d</td>!;
1870 #$money_char = $conf->config('money_char') || '$';
1871 $money_char = ''; # this is madness
1879 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1881 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1883 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1884 map { $f->{$_}->[$i] } qw(align span width)
1888 $prefix. join( $separator, @result ). $suffix;
1893 sub _condensed_total_generator {
1894 my ( $self, $format ) = ( shift, shift );
1896 my ( $f, $prefix, $suffix, $separator, $column ) =
1897 _condensed_generator_defaults($format);
1900 if ($format eq 'latex') {
1903 $separator = " & \n";
1905 sub { my ($d,$a,$s,$w) = @_;
1906 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1908 }elsif ( $format eq 'html' ) {
1912 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1914 sub { my ($d,$a,$s,$w) = @_;
1915 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1924 # my $r = &{$f->{fields}->[$i]}(@args);
1925 # $r .= ' Total' unless $i;
1927 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1929 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1930 map { $f->{$_}->[$i] } qw(align span width)
1934 $prefix. join( $separator, @result ). $suffix;
1939 =item total_line_generator FORMAT
1941 Returns a coderef used for generation of invoice total line items for this
1942 usage_class. FORMAT is either html or latex
1946 # should not be used: will have issues with hash element names (description vs
1947 # total_item and amount vs total_amount -- another array of functions?
1949 sub _condensed_total_line_generator {
1950 my ( $self, $format ) = ( shift, shift );
1952 my ( $f, $prefix, $suffix, $separator, $column ) =
1953 _condensed_generator_defaults($format);
1956 if ($format eq 'latex') {
1959 $separator = " & \n";
1961 sub { my ($d,$a,$s,$w) = @_;
1962 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1964 }elsif ( $format eq 'html' ) {
1968 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1970 sub { my ($d,$a,$s,$w) = @_;
1971 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1980 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1982 &{$column}( &{$f->{fields}->[$i]}(@args),
1983 map { $f->{$_}->[$i] } qw(align span width)
1987 $prefix. join( $separator, @result ). $suffix;
1992 # sub _items { # seems to be unused
1995 # #my @display = scalar(@_)
1997 # # : qw( _items_previous _items_pkg );
1998 # # #: qw( _items_pkg );
1999 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2000 # my @display = qw( _items_previous _items_pkg );
2003 # foreach my $display ( @display ) {
2004 # push @b, $self->$display(@_);
2009 =item _items_pkg [ OPTIONS ]
2011 Return line item hashes for each package item on this invoice. Nearly
2014 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2016 The only OPTIONS accepted is 'section', which may point to a hashref
2017 with a key named 'condensed', which may have a true value. If it
2018 does, this method tries to merge identical items into items with
2019 'quantity' equal to the number of items (not the sum of their
2020 separate quantities, for some reason).
2028 warn "$me _items_pkg searching for all package line items\n"
2031 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2033 warn "$me _items_pkg filtering line items\n"
2035 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2037 if ($options{section} && $options{section}->{condensed}) {
2039 warn "$me _items_pkg condensing section\n"
2043 local $Storable::canonical = 1;
2044 foreach ( @items ) {
2046 delete $item->{ref};
2047 delete $item->{ext_description};
2048 my $key = freeze($item);
2049 $itemshash{$key} ||= 0;
2050 $itemshash{$key} ++; # += $item->{quantity};
2052 @items = sort { $a->{description} cmp $b->{description} }
2053 map { my $i = thaw($_);
2054 $i->{quantity} = $itemshash{$_};
2056 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2062 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2069 return 0 unless $a->itemdesc cmp $b->itemdesc;
2070 return -1 if $b->itemdesc eq 'Tax';
2071 return 1 if $a->itemdesc eq 'Tax';
2072 return -1 if $b->itemdesc eq 'Other surcharges';
2073 return 1 if $a->itemdesc eq 'Other surcharges';
2074 $a->itemdesc cmp $b->itemdesc;
2079 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2080 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2083 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2085 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2086 list of hashrefs describing the line items they generate on the invoice.
2088 OPTIONS may include:
2090 format: the invoice format.
2092 escape_function: the function used to escape strings.
2094 DEPRECATED? (expensive, mostly unused?)
2095 format_function: the function used to format CDRs.
2097 section: a hashref containing 'description'; if this is present,
2098 cust_bill_pkg_display records not belonging to this section are
2101 multisection: a flag indicating that this is a multisection invoice,
2102 which does something complicated.
2104 multilocation: a flag to display the location label for the package.
2106 Returns a list of hashrefs, each of which may contain:
2108 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
2109 ext_description, which is an arrayref of detail lines to show below
2114 sub _items_cust_bill_pkg {
2116 my $conf = $self->conf;
2117 my $cust_bill_pkgs = shift;
2120 my $format = $opt{format} || '';
2121 my $escape_function = $opt{escape_function} || sub { shift };
2122 my $format_function = $opt{format_function} || '';
2123 my $no_usage = $opt{no_usage} || '';
2124 my $unsquelched = $opt{unsquelched} || ''; #unused
2125 my $section = $opt{section}->{description} if $opt{section};
2126 my $summary_page = $opt{summary_page} || ''; #unused
2127 my $multilocation = $opt{multilocation} || '';
2128 my $multisection = $opt{multisection} || '';
2129 my $discount_show_always = 0;
2131 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2133 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2136 my ($s, $r, $u) = ( undef, undef, undef );
2137 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2140 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2141 if ( $_ && !$cust_bill_pkg->hidden ) {
2142 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2143 $_->{amount} =~ s/^\-0\.00$/0.00/;
2144 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2146 if $_->{amount} != 0
2147 || $discount_show_always
2148 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2149 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2155 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2156 ? $cust_bill_pkg->cust_bill_pkg_display
2157 : ( $cust_bill_pkg );
2159 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2160 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2163 foreach my $display ( grep { defined($section)
2164 ? $_->section eq $section
2167 #grep { !$_->summary || !$summary_page } # bunk!
2168 grep { !$_->summary || $multisection }
2169 @cust_bill_pkg_display
2173 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2174 $display->billpkgdisplaynum. "\n"
2177 my $type = $display->type;
2179 my $desc = $cust_bill_pkg->desc;
2180 $desc = substr($desc, 0, $maxlength). '...'
2181 if $format eq 'latex' && length($desc) > $maxlength;
2183 my %details_opt = ( 'format' => $format,
2184 'escape_function' => $escape_function,
2185 'format_function' => $format_function,
2186 'no_usage' => $opt{'no_usage'},
2189 if ( $cust_bill_pkg->pkgnum > 0 ) {
2191 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2194 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2196 # start/end dates for invoice formats that do nonstandard
2198 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2200 if ( (!$type || $type eq 'S')
2201 && ( $cust_bill_pkg->setup != 0
2202 || $cust_bill_pkg->setup_show_zero
2207 warn "$me _items_cust_bill_pkg adding setup\n"
2210 my $description = $desc;
2211 $description .= ' Setup'
2212 if $cust_bill_pkg->recur != 0
2213 || $discount_show_always
2214 || $cust_bill_pkg->recur_show_zero;
2217 unless ( $cust_pkg->part_pkg->hide_svc_detail
2218 || $cust_bill_pkg->hidden )
2221 push @d, map &{$escape_function}($_),
2222 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2223 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2225 if ( $multilocation ) {
2226 my $loc = $cust_pkg->location_label;
2227 $loc = substr($loc, 0, $maxlength). '...'
2228 if $format eq 'latex' && length($loc) > $maxlength;
2229 push @d, &{$escape_function}($loc);
2232 } #unless hiding service details
2234 push @d, $cust_bill_pkg->details(%details_opt)
2235 if $cust_bill_pkg->recur == 0;
2237 if ( $cust_bill_pkg->hidden ) {
2238 $s->{amount} += $cust_bill_pkg->setup;
2239 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2240 push @{ $s->{ext_description} }, @d;
2244 description => $description,
2245 #pkgpart => $part_pkg->pkgpart,
2246 pkgnum => $cust_bill_pkg->pkgnum,
2247 amount => $cust_bill_pkg->setup,
2248 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2249 unit_amount => $cust_bill_pkg->unitsetup,
2250 quantity => $cust_bill_pkg->quantity,
2251 ext_description => \@d,
2257 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2259 $cust_bill_pkg->recur != 0
2260 || $cust_bill_pkg->setup == 0
2261 || $discount_show_always
2262 || $cust_bill_pkg->recur_show_zero
2267 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2270 my $is_summary = $display->summary;
2271 my $description = ($is_summary && $type && $type eq 'U')
2272 ? "Usage charges" : $desc;
2274 #pry be a bit more efficient to look some of this conf stuff up
2277 $conf->exists('disable_line_item_date_ranges')
2278 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2281 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2282 $cust_main->agentnum
2284 if ( defined($date_style) && $date_style eq 'month_of' ) {
2285 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2286 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2287 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2288 $cust_main->agentnum
2290 $desc .= ' ' unless $desc =~ /\s$/;
2291 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2293 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2294 " - ". time2str($date_format, $cust_bill_pkg->edate);
2296 $description .= " ($time_period)";
2300 my @seconds = (); # for display of usage info
2302 #at least until cust_bill_pkg has "past" ranges in addition to
2303 #the "future" sdate/edate ones... see #3032
2304 my @dates = ( $self->_date );
2305 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2306 push @dates, $prev->sdate if $prev;
2307 push @dates, undef if !$prev;
2309 unless ( $cust_pkg->part_pkg->hide_svc_detail
2310 || $cust_bill_pkg->itemdesc
2311 || $cust_bill_pkg->hidden
2312 || $is_summary && $type && $type eq 'U' )
2315 warn "$me _items_cust_bill_pkg adding service details\n"
2318 push @d, map &{$escape_function}($_),
2319 $cust_pkg->h_labels_short(@dates, 'I')
2320 #$cust_bill_pkg->edate,
2321 #$cust_bill_pkg->sdate)
2322 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2324 warn "$me _items_cust_bill_pkg done adding service details\n"
2327 if ( $multilocation ) {
2328 my $loc = $cust_pkg->location_label;
2329 $loc = substr($loc, 0, $maxlength). '...'
2330 if $format eq 'latex' && length($loc) > $maxlength;
2331 push @d, &{$escape_function}($loc);
2334 # Display of seconds_since_sqlradacct:
2335 # On the invoice, when processing @detail_items, look for a field
2336 # named 'seconds'. This will contain total seconds for each
2337 # service, in the same order as @ext_description. For services
2338 # that don't support this it will show undef.
2339 if ( $conf->exists('svc_acct-usage_seconds')
2340 and ! $cust_bill_pkg->pkgpart_override ) {
2341 foreach my $cust_svc (
2342 $cust_pkg->h_cust_svc(@dates, 'I')
2345 # eval because not having any part_export_usage exports
2346 # is a fatal error, last_bill/_date because that's how
2347 # sqlradius_hour billing does it
2349 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2351 push @seconds, $sec;
2353 } #if svc_acct-usage_seconds
2357 unless ( $is_summary ) {
2358 warn "$me _items_cust_bill_pkg adding details\n"
2361 #instead of omitting details entirely in this case (unwanted side
2362 # effects), just omit CDRs
2363 $details_opt{'no_usage'} = 1
2364 if $type && $type eq 'R';
2366 push @d, $cust_bill_pkg->details(%details_opt);
2369 warn "$me _items_cust_bill_pkg calculating amount\n"
2374 $amount = $cust_bill_pkg->recur;
2375 } elsif ($type eq 'R') {
2376 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2377 } elsif ($type eq 'U') {
2378 $amount = $cust_bill_pkg->usage;
2381 if ( !$type || $type eq 'R' ) {
2383 warn "$me _items_cust_bill_pkg adding recur\n"
2386 if ( $cust_bill_pkg->hidden ) {
2387 $r->{amount} += $amount;
2388 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
2389 push @{ $r->{ext_description} }, @d;
2392 description => $description,
2393 #pkgpart => $part_pkg->pkgpart,
2394 pkgnum => $cust_bill_pkg->pkgnum,
2396 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2397 unit_amount => $cust_bill_pkg->unitrecur,
2398 quantity => $cust_bill_pkg->quantity,
2400 ext_description => \@d,
2402 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2405 } else { # $type eq 'U'
2407 warn "$me _items_cust_bill_pkg adding usage\n"
2410 if ( $cust_bill_pkg->hidden ) {
2411 $u->{amount} += $amount;
2412 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
2413 push @{ $u->{ext_description} }, @d;
2416 description => $description,
2417 #pkgpart => $part_pkg->pkgpart,
2418 pkgnum => $cust_bill_pkg->pkgnum,
2420 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2421 unit_amount => $cust_bill_pkg->unitrecur,
2422 quantity => $cust_bill_pkg->quantity,
2424 ext_description => \@d,
2429 } # recurring or usage with recurring charge
2431 } else { #pkgnum tax or one-shot line item (??)
2433 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2436 if ( $cust_bill_pkg->setup != 0 ) {
2438 'description' => $desc,
2439 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2442 if ( $cust_bill_pkg->recur != 0 ) {
2444 'description' => "$desc (".
2445 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2446 time2str($date_format, $cust_bill_pkg->edate). ')',
2447 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2455 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2456 && $conf->exists('discount-show-always'));
2460 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2462 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2463 $_->{amount} =~ s/^\-0\.00$/0.00/;
2464 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2466 if $_->{amount} != 0
2467 || $discount_show_always
2468 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2469 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2473 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2480 =item _items_discounts_avail
2482 Returns an array of line item hashrefs representing available term discounts
2483 for this invoice. This makes the same assumptions that apply to term
2484 discounts in general: that the package is billed monthly, at a flat rate,
2485 with no usage charges. A prorated first month will be handled, as will
2486 a setup fee if the discount is allowed to apply to setup fees.
2490 sub _items_discounts_avail {
2493 #maybe move this method from cust_bill when quotations support discount_plans
2494 return () unless $self->can('discount_plans');
2495 my %plans = $self->discount_plans;
2497 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2498 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2502 my $plan = $plans{$months};
2504 my $term_total = sprintf('%.2f', $plan->discounted_total);
2505 my $percent = sprintf('%.0f',
2506 100 * (1 - $term_total / $plan->base_total) );
2507 my $permonth = sprintf('%.2f', $term_total / $months);
2508 my $detail = $self->mt('discount on item'). ' '.
2509 join(', ', map { "#$_" } $plan->pkgnums)
2512 # discounts for non-integer months don't work anyway
2513 $months = sprintf("%d", $months);
2516 description => $self->mt('Save [_1]% by paying for [_2] months',
2518 amount => $self->mt('[_1] ([_2] per month)',
2519 $term_total, $money_char.$permonth),
2520 ext_description => ($detail || ''),
2523 sort { $b <=> $a } keys %plans;