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;
17 use FS::Record qw( qsearch qsearchs );
18 use FS::Misc qw( generate_ps generate_pdf );
24 $me = '[FS::Template_Mixin]';
25 FS::UID->install_callback( sub {
26 my $conf = new FS::Conf; #global
27 $money_char = $conf->config('money_char') || '$';
28 $date_format = $conf->config('date_format') || '%x'; #/YY
29 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
30 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
33 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
35 Returns an text invoice, as a list of lines.
37 Options can be passed as a hashref (recommended) or as a list of time, template
38 and then any key/value pairs for any other options.
40 I<time>, if specified, is used to control the printing of overdue messages. The
41 default is now. It isn't the date of the invoice; that's the `_date' field.
42 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
43 L<Time::Local> and L<Date::Parse> for conversion functions.
45 I<template>, if specified, is the name of a suffix for alternate invoices.
47 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
53 my( $today, $template, %opt );
56 $today = delete($opt{'time'}) || '';
57 $template = delete($opt{template}) || '';
59 ( $today, $template, %opt ) = @_;
62 my %params = ( 'format' => 'template' );
63 $params{'time'} = $today if $today;
64 $params{'template'} = $template if $template;
65 $params{$_} = $opt{$_}
66 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
68 $self->print_generic( %params );
71 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
73 Internal method - returns a filename of a filled-in LaTeX template for this
74 invoice (Note: add ".tex" to get the actual filename), and a filename of
75 an associated logo (with the .eps extension included).
77 See print_ps and print_pdf for methods that return PostScript and PDF output.
79 Options can be passed as a hashref (recommended) or as a list of time, template
80 and then any key/value pairs for any other options.
82 I<time>, if specified, is used to control the printing of overdue messages. The
83 default is now. It isn't the date of the invoice; that's the `_date' field.
84 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
85 L<Time::Local> and L<Date::Parse> for conversion functions.
87 I<template>, if specified, is the name of a suffix for alternate invoices.
89 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
95 my $conf = $self->conf;
96 my( $today, $template, %opt );
99 $today = delete($opt{'time'}) || '';
100 $template = delete($opt{template}) || '';
102 ( $today, $template, %opt ) = @_;
105 my %params = ( 'format' => 'latex' );
106 $params{'time'} = $today if $today;
107 $params{'template'} = $template if $template;
108 $params{$_} = $opt{$_}
109 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
111 $template ||= $self->_agent_template
112 if $self->can('_agent_template');
114 my $pkey = $self->primary_key;
115 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
117 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
118 my $lh = new File::Temp(
119 TEMPLATE => $tmp_template,
123 ) or die "can't open temp file: $!\n";
125 my $agentnum = $self->cust_main->agentnum;
127 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
128 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
129 or die "can't write temp file: $!\n";
131 print $lh $conf->config_binary('logo.eps', $agentnum)
132 or die "can't write temp file: $!\n";
135 $params{'logo_file'} = $lh->filename;
137 if( $conf->exists('invoice-barcode')
138 && $self->can('invoice_barcode')
139 && $self->invnum ) { # don't try to barcode statements
140 my $png_file = $self->invoice_barcode($dir);
141 my $eps_file = $png_file;
142 $eps_file =~ s/\.png$/.eps/g;
143 $png_file =~ /(barcode.*png)/;
145 $eps_file =~ /(barcode.*eps)/;
148 my $curr_dir = cwd();
150 # after painfuly long experimentation, it was determined that sam2p won't
151 # accept : and other chars in the path, no matter how hard I tried to
152 # escape them, hence the chdir (and chdir back, just to be safe)
153 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
154 or die "sam2p failed: $!\n";
158 $params{'barcode_file'} = $eps_file;
161 my @filled_in = $self->print_generic( %params );
163 my $fh = new File::Temp( TEMPLATE => $tmp_template,
167 ) or die "can't open temp file: $!\n";
168 binmode($fh, ':utf8'); # language support
169 print $fh join('', @filled_in );
172 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
173 return ($1, $params{'logo_file'}, $params{'barcode_file'});
177 =item print_generic OPTION => VALUE ...
179 Internal method - returns a filled-in template for this invoice as a scalar.
181 See print_ps and print_pdf for methods that return PostScript and PDF output.
183 Non optional options include
184 format - latex, html, template
186 Optional options include
188 template - a value used as a suffix for a configuration template
190 time - a value used to control the printing of overdue messages. The
191 default is now. It isn't the date of the invoice; that's the `_date' field.
192 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
193 L<Time::Local> and L<Date::Parse> for conversion functions.
197 unsquelch_cdr - overrides any per customer cdr squelching when true
199 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
201 locale - override customer's locale
205 #what's with all the sprintf('%10.2f')'s in here? will it cause any
206 # (alignment in text invoice?) problems to change them all to '%.2f' ?
207 # yes: fixed width/plain text printing will be borked
209 my( $self, %params ) = @_;
210 my $conf = $self->conf;
211 my $today = $params{today} ? $params{today} : time;
212 warn "$me print_generic called on $self with suffix $params{template}\n"
215 my $format = $params{format};
216 die "Unknown format: $format"
217 unless $format =~ /^(latex|html|template)$/;
219 my $cust_main = $self->cust_main || $self->prospect_main;
220 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
221 unless $cust_main->payname
222 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
224 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
225 'html' => [ '<%=', '%>' ],
226 'template' => [ '{', '}' ],
229 warn "$me print_generic creating template\n"
233 my $template = $params{template} ? $params{template} : $self->_agent_template;
234 my $templatefile = $self->template_conf. $format;
235 $templatefile .= "_$template"
236 if length($template) && $conf->exists($templatefile."_$template");
237 my @invoice_template = map "$_\n", $conf->config($templatefile)
238 or die "cannot load config data $templatefile";
241 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
242 #change this to a die when the old code is removed
243 warn "old-style invoice template $templatefile; ".
244 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
246 @invoice_template = _translate_old_latex_format(@invoice_template);
249 warn "$me print_generic creating T:T object\n"
252 my $text_template = new Text::Template(
254 SOURCE => \@invoice_template,
255 DELIMITERS => $delimiters{$format},
258 warn "$me print_generic compiling T:T object\n"
261 $text_template->compile()
262 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
265 # additional substitution could possibly cause breakage in existing templates
268 'notes' => sub { map "$_", @_ },
269 'footer' => sub { map "$_", @_ },
270 'smallfooter' => sub { map "$_", @_ },
271 'returnaddress' => sub { map "$_", @_ },
272 'coupon' => sub { map "$_", @_ },
273 'summary' => sub { map "$_", @_ },
279 s/%%(.*)$/<!-- $1 -->/g;
280 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
281 s/\\begin\{enumerate\}/<ol>/g;
283 s/\\end\{enumerate\}/<\/ol>/g;
284 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
293 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
295 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
301 s/\\hyphenation\{[\w\s\-]+}//;
306 'coupon' => sub { "" },
307 'summary' => sub { "" },
314 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
315 s/\\begin\{enumerate\}//g;
317 s/\\end\{enumerate\}//g;
318 s/\\textbf\{(.*)\}/$1/g;
325 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
327 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
332 s/\\\\\*?\s*$/\n/; # dubious
333 s/\\hyphenation\{[\w\s\-]+}//;
337 'coupon' => sub { "" },
338 'summary' => sub { "" },
343 # hashes for differing output formats
344 my %nbsps = ( 'latex' => '~',
345 'html' => '', # '&nbps;' would be nice
346 'template' => '', # not used
348 my $nbsp = $nbsps{$format};
350 my %escape_functions = ( 'latex' => \&_latex_escape,
351 'html' => \&_html_escape_nbsp,#\&encode_entities,
352 'template' => sub { shift },
354 my $escape_function = $escape_functions{$format};
355 my $escape_function_nonbsp = ($format eq 'html')
356 ? \&_html_escape : $escape_function;
358 my %date_formats = ( 'latex' => $date_format_long,
359 'html' => $date_format_long,
362 $date_formats{'html'} =~ s/ / /g;
364 my $date_format = $date_formats{$format};
366 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
368 'html' => sub { return '<b>'. shift(). '</b>'
370 'template' => sub { shift },
372 my $embolden_function = $embolden_functions{$format};
374 my %newline_tokens = ( 'latex' => '\\\\',
378 my $newline_token = $newline_tokens{$format};
380 warn "$me generating template variables\n"
383 # generate template variables
386 defined( $conf->config_orbase( "invoice_${format}returnaddress",
390 && length( $conf->config_orbase( "invoice_${format}returnaddress",
396 $returnaddress = join("\n",
397 $conf->config_orbase("invoice_${format}returnaddress", $template)
401 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
403 my $convert_map = $convert_maps{$format}{'returnaddress'};
406 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
411 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
413 my $convert_map = $convert_maps{$format}{'returnaddress'};
414 $returnaddress = join( "\n", &$convert_map(
415 map { s/( {2,})/'~' x length($1)/eg;
419 ( $conf->config('company_name', $cust_main->agentnum),
420 $conf->config('company_address', $cust_main->agentnum),
427 my $warning = "Couldn't find a return address; ".
428 "do you need to set the company_address configuration value?";
430 $returnaddress = $nbsp;
431 #$returnaddress = $warning;
435 warn "$me generating invoice data\n"
438 my $agentnum = $cust_main->agentnum;
443 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
444 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
445 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
446 'returnaddress' => $returnaddress,
447 'agent' => &$escape_function($cust_main->agent->agent),
449 #invoice/quotation info
450 'invnum' => $self->invnum,
451 'quotationnum' => $self->quotationnum,
452 'date' => time2str($date_format, $self->_date),
453 'today' => time2str($date_format_long, $today),
454 'terms' => $self->terms,
455 'template' => $template, #params{'template'},
456 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
457 'current_charges' => sprintf("%.2f", $self->charged),
458 'duedate' => $self->due_date2str($rdate_format), #date_format?
461 'custnum' => $cust_main->display_custnum,
462 'prospectnum' => $cust_main->prospectnum,
463 'agent_custid' => &$escape_function($cust_main->agent_custid),
464 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
465 payname company address1 address2 city state zip fax
469 'ship_enable' => $conf->exists('invoice-ship_address'),
470 'unitprices' => $conf->exists('invoice-unitprice'),
471 'smallernotes' => $conf->exists('invoice-smallernotes'),
472 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
473 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
475 #layout info -- would be fancy to calc some of this and bury the template
477 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
478 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
479 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
480 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
481 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
482 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
483 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
484 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
485 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
486 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
488 # better hang on to conf_dir for a while (for old templates)
489 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
491 #these are only used when doing paged plaintext
498 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
499 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
500 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
501 # eval to avoid death for unimplemented languages
502 my $dh = eval { Date::Language->new($info{'name'}) } ||
503 Date::Language->new(); # fall back to English
504 # prototype here to silence warnings
505 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
506 # eventually use this date handle everywhere in here, too
508 my $min_sdate = 999999999999;
510 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
511 next unless $cust_bill_pkg->pkgnum > 0;
512 $min_sdate = $cust_bill_pkg->sdate
513 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
514 $max_edate = $cust_bill_pkg->edate
515 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
518 $invoice_data{'bill_period'} = '';
519 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
520 . " to " . time2str('%e %h', $max_edate)
521 if ($max_edate != 0 && $min_sdate != 999999999999);
523 $invoice_data{finance_section} = '';
524 if ( $conf->config('finance_pkgclass') ) {
526 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
527 $invoice_data{finance_section} = $pkg_class->categoryname;
529 $invoice_data{finance_amount} = '0.00';
530 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
532 my $countrydefault = $conf->config('countrydefault') || 'US';
533 foreach ( qw( address1 address2 city state zip country fax) ){
534 my $method = 'ship_'.$_;
535 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
537 foreach ( qw( contact company ) ) { #compatibility
538 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
540 $invoice_data{'ship_country'} = ''
541 if ( $invoice_data{'ship_country'} eq $countrydefault );
543 $invoice_data{'cid'} = $params{'cid'}
546 if ( $cust_main->country eq $countrydefault ) {
547 $invoice_data{'country'} = '';
549 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
553 $invoice_data{'address'} = \@address;
556 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
557 ? " (P.O. #". $cust_main->payinfo. ")"
561 push @address, $cust_main->company
562 if $cust_main->company;
563 push @address, $cust_main->address1;
564 push @address, $cust_main->address2
565 if $cust_main->address2;
567 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
568 push @address, $invoice_data{'country'}
569 if $invoice_data{'country'};
571 while (scalar(@address) < 5);
573 $invoice_data{'logo_file'} = $params{'logo_file'}
574 if $params{'logo_file'};
575 $invoice_data{'barcode_file'} = $params{'barcode_file'}
576 if $params{'barcode_file'};
577 $invoice_data{'barcode_img'} = $params{'barcode_img'}
578 if $params{'barcode_img'};
579 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
580 if $params{'barcode_cid'};
582 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
583 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
584 #my $balance_due = $self->owed + $pr_total - $cr_total;
585 my $balance_due = $self->owed + $pr_total;
587 # the customer's current balance as shown on the invoice before this one
588 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
590 # the change in balance from that invoice to this one
591 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
593 # the sum of amount owed on all previous invoices
594 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
596 # the sum of amount owed on all invoices
597 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
599 # info from customer's last invoice before this one, for some
601 $invoice_data{'last_bill'} = {};
602 my $last_bill = $pr_cust_bill[-1];
604 $invoice_data{'last_bill'} = {
605 '_date' => $last_bill->_date, #unformatted
606 # all we need for now
610 my $summarypage = '';
611 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
614 $invoice_data{'summarypage'} = $summarypage;
616 warn "$me substituting variables in notes, footer, smallfooter\n"
619 my $tc = $self->template_conf;
620 my @include = ( [ $tc, 'notes' ],
621 [ 'invoice_', 'footer' ],
622 [ 'invoice_', 'smallfooter', ],
624 push @include, [ $tc, 'coupon', ]
625 unless $params{'no_coupon'};
627 foreach my $i (@include) {
629 my($base, $include) = @$i;
631 my $inc_file = $conf->key_orbase("$base$format$include", $template);
634 if ( $conf->exists($inc_file, $agentnum)
635 && length( $conf->config($inc_file, $agentnum) ) ) {
637 @inc_src = $conf->config($inc_file, $agentnum);
641 $inc_file = $conf->key_orbase("${base}latex$include", $template);
643 my $convert_map = $convert_maps{$format}{$include};
645 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
646 s/--\@\]/$delimiters{$format}[1]/g;
649 &$convert_map( $conf->config($inc_file, $agentnum) );
653 my $inc_tt = new Text::Template (
655 SOURCE => [ map "$_\n", @inc_src ],
656 DELIMITERS => $delimiters{$format},
657 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
659 unless ( $inc_tt->compile() ) {
660 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
661 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
665 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
667 $invoice_data{$include} =~ s/\n+$//
668 if ($format eq 'latex');
671 # let invoices use either of these as needed
672 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
673 ? $cust_main->payinfo : '';
674 $invoice_data{'po_line'} =
675 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
676 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
679 my %money_chars = ( 'latex' => '',
680 'html' => $conf->config('money_char') || '$',
683 my $money_char = $money_chars{$format};
685 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
686 'html' => $conf->config('money_char') || '$',
689 my $other_money_char = $other_money_chars{$format};
690 $invoice_data{'dollar'} = $other_money_char;
692 my @detail_items = ();
693 my @total_items = ();
697 $invoice_data{'detail_items'} = \@detail_items;
698 $invoice_data{'total_items'} = \@total_items;
699 $invoice_data{'buf'} = \@buf;
700 $invoice_data{'sections'} = \@sections;
702 warn "$me generating sections\n"
705 # Previous Charges section
706 # subtotal is the first return value from $self->previous
707 my $previous_section = { 'description' => $self->mt('Previous Charges'),
708 'subtotal' => $other_money_char.
709 sprintf('%.2f', $pr_total),
710 'summarized' => '', #why? $summarypage ? 'Y' : '',
712 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
713 join(' / ', map { $cust_main->balance_date_range(@$_) }
714 $self->_prior_month30s
716 if $conf->exists('invoice_include_aging');
719 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
720 'subtotal' => $taxtotal, # adjusted below
722 my $tax_weight = _pkg_category($tax_section->{description})
723 ? _pkg_category($tax_section->{description})->weight
725 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
726 $tax_section->{'sort_weight'} = $tax_weight;
730 my $adjust_section = { 'description' =>
731 $self->mt('Credits, Payments, and Adjustments'),
732 'subtotal' => 0, # adjusted below
734 my $adjust_weight = _pkg_category($adjust_section->{description})
735 ? _pkg_category($adjust_section->{description})->weight
737 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
738 $adjust_section->{'sort_weight'} = $adjust_weight;
740 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
741 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
742 $invoice_data{'multisection'} = $multisection;
743 my $late_sections = [];
744 my $extra_sections = [];
745 my $extra_lines = ();
747 my $default_section = { 'description' => '',
752 if ( $multisection ) {
753 ($extra_sections, $extra_lines) =
754 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
755 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
756 && $self->can('_items_extra_usage_sections');
758 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
760 push @detail_items, @$extra_lines if $extra_lines;
762 $self->_items_sections( $late_sections, # this could stand a refactor
764 $escape_function_nonbsp,
768 if ( $conf->exists('svc_phone_sections')
769 && $self->can('_items_svc_phone_sections')
772 my ($phone_sections, $phone_lines) =
773 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
774 push @{$late_sections}, @$phone_sections;
775 push @detail_items, @$phone_lines;
777 if ( $conf->exists('voip-cust_accountcode_cdr')
778 && $cust_main->accountcode_cdr
779 && $self->can('_items_accountcode_cdr')
782 my ($accountcode_section, $accountcode_lines) =
783 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
784 if ( scalar(@$accountcode_lines) ) {
785 push @{$late_sections}, $accountcode_section;
786 push @detail_items, @$accountcode_lines;
789 } else {# not multisection
790 # make a default section
791 push @sections, $default_section;
792 # and calculate the finance charge total, since it won't get done otherwise.
793 # XXX possibly other totals?
794 # XXX possibly finance_pkgclass should not be used in this manner?
795 if ( $conf->exists('finance_pkgclass') ) {
797 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
798 if ( grep { $_->section eq $invoice_data{finance_section} }
799 $cust_bill_pkg->cust_bill_pkg_display ) {
800 # I think these are always setup fees, but just to be sure...
801 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
804 $invoice_data{finance_amount} =
805 sprintf('%.2f', sum( @finance_charges ) || 0);
809 # previous invoice balances in the Previous Charges section if there
810 # is one, otherwise in the main detail section
811 if ( $self->can('_items_previous') &&
812 $self->enable_previous &&
813 ! $conf->exists('previous_balance-summary_only') ) {
815 warn "$me adding previous balances\n"
818 foreach my $line_item ( $self->_items_previous ) {
821 ext_description => [],
823 $detail->{'ref'} = $line_item->{'pkgnum'};
824 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
825 $detail->{'quantity'} = 1;
826 $detail->{'section'} = $multisection ? $previous_section
828 $detail->{'description'} = &$escape_function($line_item->{'description'});
829 if ( exists $line_item->{'ext_description'} ) {
830 @{$detail->{'ext_description'}} = map {
831 &$escape_function($_);
832 } @{$line_item->{'ext_description'}};
834 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
835 $line_item->{'amount'};
836 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
838 push @detail_items, $detail;
839 push @buf, [ $detail->{'description'},
840 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
846 if ( @pr_cust_bill && $self->enable_previous ) {
847 push @buf, ['','-----------'];
848 push @buf, [ $self->mt('Total Previous Balance'),
849 $money_char. sprintf("%10.2f", $pr_total) ];
853 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
854 warn "$me adding DID summary\n"
857 my ($didsummary,$minutes) = $self->_did_summary;
858 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
860 { 'description' => $didsummary_desc,
861 'ext_description' => [ $didsummary, $minutes ],
865 foreach my $section (@sections, @$late_sections) {
867 warn "$me adding section \n". Dumper($section)
870 # begin some normalization
871 $section->{'subtotal'} = $section->{'amount'}
873 && !exists($section->{subtotal})
874 && exists($section->{amount});
876 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
877 if ( $invoice_data{finance_section} &&
878 $section->{'description'} eq $invoice_data{finance_section} );
880 $section->{'subtotal'} = $other_money_char.
881 sprintf('%.2f', $section->{'subtotal'})
884 # continue some normalization
885 $section->{'amount'} = $section->{'subtotal'}
889 if ( $section->{'description'} ) {
890 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
895 warn "$me setting options\n"
899 $options{'section'} = $section if $multisection;
900 $options{'format'} = $format;
901 $options{'escape_function'} = $escape_function;
902 $options{'no_usage'} = 1 unless $unsquelched;
903 $options{'unsquelched'} = $unsquelched;
904 $options{'summary_page'} = $summarypage;
905 $options{'skip_usage'} =
906 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
907 $options{'multisection'} = $multisection;
909 warn "$me searching for line items\n"
912 foreach my $line_item ( $self->_items_pkg(%options) ) {
914 warn "$me adding line item $line_item\n"
918 ext_description => [],
920 $detail->{'ref'} = $line_item->{'pkgnum'};
921 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
922 $detail->{'quantity'} = $line_item->{'quantity'};
923 $detail->{'section'} = $section;
924 $detail->{'description'} = &$escape_function($line_item->{'description'});
925 if ( exists $line_item->{'ext_description'} ) {
926 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
928 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
929 $line_item->{'amount'};
930 if ( exists $line_item->{'unit_amount'} ) {
931 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
932 $line_item->{'unit_amount'};
934 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
936 $detail->{'sdate'} = $line_item->{'sdate'};
937 $detail->{'edate'} = $line_item->{'edate'};
938 $detail->{'seconds'} = $line_item->{'seconds'};
940 push @detail_items, $detail;
941 push @buf, ( [ $detail->{'description'},
942 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
944 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
948 if ( $section->{'description'} ) {
949 push @buf, ( ['','-----------'],
950 [ $section->{'description'}. ' sub-total',
951 $section->{'subtotal'} # already formatted this
960 $invoice_data{current_less_finance} =
961 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
963 # create a major section for previous balance if we have major sections,
964 # or if previous_section is in summary form
965 if ( ( $multisection && $self->enable_previous )
966 || $conf->exists('previous_balance-summary_only') )
968 unshift @sections, $previous_section if $pr_total;
971 warn "$me adding taxes\n"
974 foreach my $tax ( $self->_items_tax ) {
976 $taxtotal += $tax->{'amount'};
978 my $description = &$escape_function( $tax->{'description'} );
979 my $amount = sprintf( '%.2f', $tax->{'amount'} );
981 if ( $multisection ) {
983 my $money = $old_latex ? '' : $money_char;
984 push @detail_items, {
985 ext_description => [],
988 description => $description,
989 amount => $money. $amount,
991 section => $tax_section,
997 'total_item' => $description,
998 'total_amount' => $other_money_char. $amount,
1003 push @buf,[ $description,
1004 $money_char. $amount,
1011 $total->{'total_item'} = $self->mt('Sub-total');
1012 $total->{'total_amount'} =
1013 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1015 if ( $multisection ) {
1016 $tax_section->{'subtotal'} = $other_money_char.
1017 sprintf('%.2f', $taxtotal);
1018 $tax_section->{'pretotal'} = 'New charges sub-total '.
1019 $total->{'total_amount'};
1020 push @sections, $tax_section if $taxtotal;
1022 unshift @total_items, $total;
1025 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1027 push @buf,['','-----------'];
1028 push @buf,[$self->mt(
1029 (!$self->enable_previous)
1031 : 'Total New Charges'
1033 $money_char. sprintf("%10.2f",$self->charged) ];
1036 # calculate total, possibly including total owed on previous
1041 $item = $conf->config('previous_balance-exclude_from_total')
1042 || 'Total New Charges'
1043 if $conf->exists('previous_balance-exclude_from_total');
1044 my $amount = $self->charged;
1045 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1046 $amount += $pr_total;
1049 $total->{'total_item'} = &$embolden_function($self->mt($item));
1050 $total->{'total_amount'} =
1051 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1052 if ( $multisection ) {
1053 if ( $adjust_section->{'sort_weight'} ) {
1054 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1055 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1057 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1058 $other_money_char. sprintf('%.2f', $self->charged );
1061 push @total_items, $total;
1063 push @buf,['','-----------'];
1066 sprintf( '%10.2f', $amount )
1071 # if we're showing previous invoices, also show previous
1072 # credits and payments
1073 if ( $self->enable_previous
1074 and $self->can('_items_credits')
1075 and $self->can('_items_payments') )
1077 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1080 my $credittotal = 0;
1081 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1084 $total->{'total_item'} = &$escape_function($credit->{'description'});
1085 $credittotal += $credit->{'amount'};
1086 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1087 $adjusttotal += $credit->{'amount'};
1088 if ( $multisection ) {
1089 my $money = $old_latex ? '' : $money_char;
1090 push @detail_items, {
1091 ext_description => [],
1094 description => &$escape_function($credit->{'description'}),
1095 amount => $money. $credit->{'amount'},
1097 section => $adjust_section,
1100 push @total_items, $total;
1104 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1107 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1108 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1112 my $paymenttotal = 0;
1113 foreach my $payment ( $self->_items_payments ) {
1115 $total->{'total_item'} = &$escape_function($payment->{'description'});
1116 $paymenttotal += $payment->{'amount'};
1117 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1118 $adjusttotal += $payment->{'amount'};
1119 if ( $multisection ) {
1120 my $money = $old_latex ? '' : $money_char;
1121 push @detail_items, {
1122 ext_description => [],
1125 description => &$escape_function($payment->{'description'}),
1126 amount => $money. $payment->{'amount'},
1128 section => $adjust_section,
1131 push @total_items, $total;
1133 push @buf, [ $payment->{'description'},
1134 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1137 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1139 if ( $multisection ) {
1140 $adjust_section->{'subtotal'} = $other_money_char.
1141 sprintf('%.2f', $adjusttotal);
1142 push @sections, $adjust_section
1143 unless $adjust_section->{sort_weight};
1146 # create Balance Due message
1149 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1150 $total->{'total_amount'} =
1151 &$embolden_function(
1152 $other_money_char. sprintf('%.2f', $summarypage
1154 $self->billing_balance
1155 : $self->owed + $pr_total
1158 if ( $multisection && !$adjust_section->{sort_weight} ) {
1159 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1160 $total->{'total_amount'};
1162 push @total_items, $total;
1164 push @buf,['','-----------'];
1165 push @buf,[$self->balance_due_msg, $money_char.
1166 sprintf("%10.2f", $balance_due ) ];
1169 if ( $conf->exists('previous_balance-show_credit')
1170 and $cust_main->balance < 0 ) {
1171 my $credit_total = {
1172 'total_item' => &$embolden_function($self->credit_balance_msg),
1173 'total_amount' => &$embolden_function(
1174 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1177 if ( $multisection ) {
1178 $adjust_section->{'posttotal'} .= $newline_token .
1179 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1182 push @total_items, $credit_total;
1184 push @buf,['','-----------'];
1185 push @buf,[$self->credit_balance_msg, $money_char.
1186 sprintf("%10.2f", -$cust_main->balance ) ];
1190 if ( $multisection ) {
1191 if ( $conf->exists('svc_phone_sections')
1192 && $self->can('_items_svc_phone_sections')
1196 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1197 $total->{'total_amount'} =
1198 &$embolden_function(
1199 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1201 my $last_section = pop @sections;
1202 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1203 $total->{'total_amount'};
1204 push @sections, $last_section;
1206 push @sections, @$late_sections
1210 # make a discounts-available section, even without multisection
1211 if ( $conf->exists('discount-show_available')
1212 and my @discounts_avail = $self->_items_discounts_avail ) {
1213 my $discount_section = {
1214 'description' => $self->mt('Discounts Available'),
1219 push @sections, $discount_section;
1220 push @detail_items, map { +{
1221 'ref' => '', #should this be something else?
1222 'section' => $discount_section,
1223 'description' => &$escape_function( $_->{description} ),
1224 'amount' => $money_char . &$escape_function( $_->{amount} ),
1225 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1226 } } @discounts_avail;
1229 # debugging hook: call this with 'diag' => 1 to just get a hash of
1230 # the invoice variables
1231 return \%invoice_data if ( $params{'diag'} );
1233 # All sections and items are built; now fill in templates.
1234 my @includelist = ();
1235 push @includelist, 'summary' if $summarypage;
1236 foreach my $include ( @includelist ) {
1238 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1241 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1243 @inc_src = $conf->config($inc_file, $agentnum);
1247 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1249 my $convert_map = $convert_maps{$format}{$include};
1251 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1252 s/--\@\]/$delimiters{$format}[1]/g;
1255 &$convert_map( $conf->config($inc_file, $agentnum) );
1259 my $inc_tt = new Text::Template (
1261 SOURCE => [ map "$_\n", @inc_src ],
1262 DELIMITERS => $delimiters{$format},
1263 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1265 unless ( $inc_tt->compile() ) {
1266 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1267 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1271 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1273 $invoice_data{$include} =~ s/\n+$//
1274 if ($format eq 'latex');
1279 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1280 /invoice_lines\((\d*)\)/;
1281 $invoice_lines += $1 || scalar(@buf);
1284 die "no invoice_lines() functions in template?"
1285 if ( $format eq 'template' && !$wasfunc );
1287 if ($format eq 'template') {
1289 if ( $invoice_lines ) {
1290 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1291 $invoice_data{'total_pages'}++
1292 if scalar(@buf) % $invoice_lines;
1295 #setup subroutine for the template
1296 $invoice_data{invoice_lines} = sub {
1297 my $lines = shift || scalar(@buf);
1309 push @collect, split("\n",
1310 $text_template->fill_in( HASH => \%invoice_data )
1312 $invoice_data{'page'}++;
1314 map "$_\n", @collect;
1316 } else { # this is where we actually create the invoice
1318 warn "filling in template for invoice ". $self->invnum. "\n"
1320 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1323 $text_template->fill_in(HASH => \%invoice_data);
1327 sub notice_name { '('.shift->table.')'; }
1329 sub template_conf { 'invoice_'; }
1331 # helper routine for generating date ranges
1332 sub _prior_month30s {
1335 [ 1, 2592000 ], # 0-30 days ago
1336 [ 2592000, 5184000 ], # 30-60 days ago
1337 [ 5184000, 7776000 ], # 60-90 days ago
1338 [ 7776000, 0 ], # 90+ days ago
1341 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1342 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1347 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1349 Returns an postscript invoice, as a scalar.
1351 Options can be passed as a hashref (recommended) or as a list of time, template
1352 and then any key/value pairs for any other options.
1354 I<time> an optional value used to control the printing of overdue messages. The
1355 default is now. It isn't the date of the invoice; that's the `_date' field.
1356 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1357 L<Time::Local> and L<Date::Parse> for conversion functions.
1359 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1366 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1367 my $ps = generate_ps($file);
1369 unlink($barcodefile) if $barcodefile;
1374 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1376 Returns an PDF invoice, as a scalar.
1378 Options can be passed as a hashref (recommended) or as a list of time, template
1379 and then any key/value pairs for any other options.
1381 I<time> an optional value used to control the printing of overdue messages. The
1382 default is now. It isn't the date of the invoice; that's the `_date' field.
1383 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1384 L<Time::Local> and L<Date::Parse> for conversion functions.
1386 I<template>, if specified, is the name of a suffix for alternate invoices.
1388 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1395 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1396 my $pdf = generate_pdf($file);
1398 unlink($barcodefile) if $barcodefile;
1403 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1405 Returns an HTML invoice, as a scalar.
1407 I<time> an optional value used to control the printing of overdue messages. The
1408 default is now. It isn't the date of the invoice; that's the `_date' field.
1409 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1410 L<Time::Local> and L<Date::Parse> for conversion functions.
1412 I<template>, if specified, is the name of a suffix for alternate invoices.
1414 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1416 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1417 when emailing the invoice as part of a multipart/related MIME email.
1425 %params = %{ shift() };
1427 $params{'time'} = shift;
1428 $params{'template'} = shift;
1429 $params{'cid'} = shift;
1432 $params{'format'} = 'html';
1434 $self->print_generic( %params );
1437 # quick subroutine for print_latex
1439 # There are ten characters that LaTeX treats as special characters, which
1440 # means that they do not simply typeset themselves:
1441 # # $ % & ~ _ ^ \ { }
1443 # TeX ignores blanks following an escaped character; if you want a blank (as
1444 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1448 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1449 $value =~ s/([<>])/\$$1\$/g;
1455 encode_entities($value);
1459 sub _html_escape_nbsp {
1460 my $value = _html_escape(shift);
1461 $value =~ s/ +/ /g;
1465 #utility methods for print_*
1467 sub _translate_old_latex_format {
1468 warn "_translate_old_latex_format called\n"
1475 if ( $line =~ /^%%Detail\s*$/ ) {
1477 push @template, q![@--!,
1478 q! foreach my $_tr_line (@detail_items) {!,
1479 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1480 q! $_tr_line->{'description'} .= !,
1481 q! "\\tabularnewline\n~~".!,
1482 q! join( "\\tabularnewline\n~~",!,
1483 q! @{$_tr_line->{'ext_description'}}!,
1487 while ( ( my $line_item_line = shift )
1488 !~ /^%%EndDetail\s*$/ ) {
1489 $line_item_line =~ s/'/\\'/g; # nice LTS
1490 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1491 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1492 push @template, " \$OUT .= '$line_item_line';";
1495 push @template, '}',
1498 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1500 push @template, '[@--',
1501 ' foreach my $_tr_line (@total_items) {';
1503 while ( ( my $total_item_line = shift )
1504 !~ /^%%EndTotalDetails\s*$/ ) {
1505 $total_item_line =~ s/'/\\'/g; # nice LTS
1506 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1507 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1508 push @template, " \$OUT .= '$total_item_line';";
1511 push @template, '}',
1515 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1516 push @template, $line;
1522 warn "$_\n" foreach @template;
1530 my $conf = $self->conf;
1532 #check for an invoice-specific override
1533 return $self->invoice_terms if $self->invoice_terms;
1535 #check for a customer- specific override
1536 my $cust_main = $self->cust_main;
1537 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1539 #use configured default
1540 $conf->config('invoice_default_terms') || '';
1546 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1547 $duedate = $self->_date() + ( $1 * 86400 );
1554 $self->due_date ? time2str(shift, $self->due_date) : '';
1557 sub balance_due_msg {
1559 my $msg = $self->mt('Balance Due');
1560 return $msg unless $self->terms;
1561 if ( $self->due_date ) {
1562 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1563 $self->due_date2str($date_format);
1564 } elsif ( $self->terms ) {
1565 $msg .= ' - '. $self->terms;
1570 sub balance_due_date {
1572 my $conf = $self->conf;
1574 if ( $conf->exists('invoice_default_terms')
1575 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1576 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1581 sub credit_balance_msg {
1583 $self->mt('Credit Balance Remaining')
1588 Returns a string with the date, for example: "3/20/2008"
1594 time2str($date_format, $self->_date);
1597 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1599 Generate section information for all items appearing on this invoice.
1600 This will only be called for multi-section invoices.
1602 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1603 related display records (L<FS::cust_bill_pkg_display>) and organize
1604 them into two groups ("early" and "late" according to whether they come
1605 before or after the total), then into sections. A subtotal is calculated
1608 Section descriptions are returned in sort weight order. Each consists
1609 of a hash containing:
1611 description: the package category name, escaped
1612 subtotal: the total charges in that section
1613 tax_section: a flag indicating that the section contains only tax charges
1614 summarized: same as tax_section, for some reason
1615 sort_weight: the package category's sort weight
1617 If 'condense' is set on the display record, it also contains everything
1618 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1619 coderefs to generate parts of the invoice. This is not advised.
1623 LATE: an arrayref to push the "late" section hashes onto. The "early"
1624 group is simply returned from the method.
1626 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1627 Turning this on has the following effects:
1628 - Ignores display items with the 'summary' flag.
1629 - Combines all items into the "early" group.
1630 - Creates sections for all non-disabled package categories, even if they
1631 have no charges on this invoice, as well as a section with no name.
1633 ESCAPE: an escape function to use for section titles.
1635 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1636 sorted list. If there are any of these, section subtotals exclude
1639 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1640 passed through to C<_condense_section()>.
1644 use vars qw(%pkg_category_cache);
1645 sub _items_sections {
1648 my $summarypage = shift;
1650 my $extra_sections = shift;
1654 my %late_subtotal = ();
1657 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1660 my $usage = $cust_bill_pkg->usage;
1662 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1663 next if ( $display->summary && $summarypage );
1665 my $section = $display->section;
1666 my $type = $display->type;
1668 $not_tax{$section} = 1
1669 unless $cust_bill_pkg->pkgnum == 0;
1671 # there's actually a very important piece of logic buried in here:
1672 # incrementing $late_subtotal{$section} CREATES
1673 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1674 # to define the list of late sections, and likewise keys(%subtotal).
1675 # When _items_cust_bill_pkg is called to generate line items for
1676 # real, it will be called with 'section' => $section for each
1678 if ( $display->post_total && !$summarypage ) {
1679 if (! $type || $type eq 'S') {
1680 $late_subtotal{$section} += $cust_bill_pkg->setup
1681 if $cust_bill_pkg->setup != 0
1682 || $cust_bill_pkg->setup_show_zero;
1686 $late_subtotal{$section} += $cust_bill_pkg->recur
1687 if $cust_bill_pkg->recur != 0
1688 || $cust_bill_pkg->recur_show_zero;
1691 if ($type && $type eq 'R') {
1692 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1693 if $cust_bill_pkg->recur != 0
1694 || $cust_bill_pkg->recur_show_zero;
1697 if ($type && $type eq 'U') {
1698 $late_subtotal{$section} += $usage
1699 unless scalar(@$extra_sections);
1704 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1706 if (! $type || $type eq 'S') {
1707 $subtotal{$section} += $cust_bill_pkg->setup
1708 if $cust_bill_pkg->setup != 0
1709 || $cust_bill_pkg->setup_show_zero;
1713 $subtotal{$section} += $cust_bill_pkg->recur
1714 if $cust_bill_pkg->recur != 0
1715 || $cust_bill_pkg->recur_show_zero;
1718 if ($type && $type eq 'R') {
1719 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1720 if $cust_bill_pkg->recur != 0
1721 || $cust_bill_pkg->recur_show_zero;
1724 if ($type && $type eq 'U') {
1725 $subtotal{$section} += $usage
1726 unless scalar(@$extra_sections);
1735 %pkg_category_cache = ();
1737 push @$late, map { { 'description' => &{$escape}($_),
1738 'subtotal' => $late_subtotal{$_},
1740 'sort_weight' => ( _pkg_category($_)
1741 ? _pkg_category($_)->weight
1744 ((_pkg_category($_) && _pkg_category($_)->condense)
1745 ? $self->_condense_section($format)
1749 sort _sectionsort keys %late_subtotal;
1752 if ( $summarypage ) {
1753 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1754 map { $_->categoryname } qsearch('pkg_category', {});
1755 push @sections, '' if exists($subtotal{''});
1757 @sections = keys %subtotal;
1760 my @early = map { { 'description' => &{$escape}($_),
1761 'subtotal' => $subtotal{$_},
1762 'summarized' => $not_tax{$_} ? '' : 'Y',
1763 'tax_section' => $not_tax{$_} ? '' : 'Y',
1764 'sort_weight' => ( _pkg_category($_)
1765 ? _pkg_category($_)->weight
1768 ((_pkg_category($_) && _pkg_category($_)->condense)
1769 ? $self->_condense_section($format)
1774 push @early, @$extra_sections if $extra_sections;
1776 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1780 #helper subs for above
1783 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1787 my $categoryname = shift;
1788 $pkg_category_cache{$categoryname} ||=
1789 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1792 my %condensed_format = (
1793 'label' => [ qw( Description Qty Amount ) ],
1795 sub { shift->{description} },
1796 sub { shift->{quantity} },
1797 sub { my($href, %opt) = @_;
1798 ($opt{dollar} || ''). $href->{amount};
1801 'align' => [ qw( l r r ) ],
1802 'span' => [ qw( 5 1 1 ) ], # unitprices?
1803 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1806 sub _condense_section {
1807 my ( $self, $format ) = ( shift, shift );
1809 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1810 qw( description_generator
1813 total_line_generator
1818 sub _condensed_generator_defaults {
1819 my ( $self, $format ) = ( shift, shift );
1820 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1829 sub _condensed_header_generator {
1830 my ( $self, $format ) = ( shift, shift );
1832 my ( $f, $prefix, $suffix, $separator, $column ) =
1833 _condensed_generator_defaults($format);
1835 if ($format eq 'latex') {
1836 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1837 $suffix = "\\\\\n\\hline";
1840 sub { my ($d,$a,$s,$w) = @_;
1841 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1843 } elsif ( $format eq 'html' ) {
1844 $prefix = '<th></th>';
1848 sub { my ($d,$a,$s,$w) = @_;
1849 return qq!<th align="$html_align{$a}">$d</th>!;
1857 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1859 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1862 $prefix. join($separator, @result). $suffix;
1867 sub _condensed_description_generator {
1868 my ( $self, $format ) = ( shift, shift );
1870 my ( $f, $prefix, $suffix, $separator, $column ) =
1871 _condensed_generator_defaults($format);
1873 my $money_char = '$';
1874 if ($format eq 'latex') {
1875 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1877 $separator = " & \n";
1879 sub { my ($d,$a,$s,$w) = @_;
1880 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1882 $money_char = '\\dollar';
1883 }elsif ( $format eq 'html' ) {
1884 $prefix = '"><td align="center"></td>';
1888 sub { my ($d,$a,$s,$w) = @_;
1889 return qq!<td align="$html_align{$a}">$d</td>!;
1891 #$money_char = $conf->config('money_char') || '$';
1892 $money_char = ''; # this is madness
1900 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1902 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1904 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1905 map { $f->{$_}->[$i] } qw(align span width)
1909 $prefix. join( $separator, @result ). $suffix;
1914 sub _condensed_total_generator {
1915 my ( $self, $format ) = ( shift, shift );
1917 my ( $f, $prefix, $suffix, $separator, $column ) =
1918 _condensed_generator_defaults($format);
1921 if ($format eq 'latex') {
1924 $separator = " & \n";
1926 sub { my ($d,$a,$s,$w) = @_;
1927 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1929 }elsif ( $format eq 'html' ) {
1933 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1935 sub { my ($d,$a,$s,$w) = @_;
1936 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1945 # my $r = &{$f->{fields}->[$i]}(@args);
1946 # $r .= ' Total' unless $i;
1948 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1950 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1951 map { $f->{$_}->[$i] } qw(align span width)
1955 $prefix. join( $separator, @result ). $suffix;
1960 =item total_line_generator FORMAT
1962 Returns a coderef used for generation of invoice total line items for this
1963 usage_class. FORMAT is either html or latex
1967 # should not be used: will have issues with hash element names (description vs
1968 # total_item and amount vs total_amount -- another array of functions?
1970 sub _condensed_total_line_generator {
1971 my ( $self, $format ) = ( shift, shift );
1973 my ( $f, $prefix, $suffix, $separator, $column ) =
1974 _condensed_generator_defaults($format);
1977 if ($format eq 'latex') {
1980 $separator = " & \n";
1982 sub { my ($d,$a,$s,$w) = @_;
1983 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1985 }elsif ( $format eq 'html' ) {
1989 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1991 sub { my ($d,$a,$s,$w) = @_;
1992 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2001 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2003 &{$column}( &{$f->{fields}->[$i]}(@args),
2004 map { $f->{$_}->[$i] } qw(align span width)
2008 $prefix. join( $separator, @result ). $suffix;
2013 # sub _items { # seems to be unused
2016 # #my @display = scalar(@_)
2018 # # : qw( _items_previous _items_pkg );
2019 # # #: qw( _items_pkg );
2020 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2021 # my @display = qw( _items_previous _items_pkg );
2024 # foreach my $display ( @display ) {
2025 # push @b, $self->$display(@_);
2030 =item _items_pkg [ OPTIONS ]
2032 Return line item hashes for each package item on this invoice. Nearly
2035 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2037 The only OPTIONS accepted is 'section', which may point to a hashref
2038 with a key named 'condensed', which may have a true value. If it
2039 does, this method tries to merge identical items into items with
2040 'quantity' equal to the number of items (not the sum of their
2041 separate quantities, for some reason).
2049 warn "$me _items_pkg searching for all package line items\n"
2052 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2054 warn "$me _items_pkg filtering line items\n"
2056 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2058 if ($options{section} && $options{section}->{condensed}) {
2060 warn "$me _items_pkg condensing section\n"
2064 local $Storable::canonical = 1;
2065 foreach ( @items ) {
2067 delete $item->{ref};
2068 delete $item->{ext_description};
2069 my $key = freeze($item);
2070 $itemshash{$key} ||= 0;
2071 $itemshash{$key} ++; # += $item->{quantity};
2073 @items = sort { $a->{description} cmp $b->{description} }
2074 map { my $i = thaw($_);
2075 $i->{quantity} = $itemshash{$_};
2077 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2083 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2090 return 0 unless $a->itemdesc cmp $b->itemdesc;
2091 return -1 if $b->itemdesc eq 'Tax';
2092 return 1 if $a->itemdesc eq 'Tax';
2093 return -1 if $b->itemdesc eq 'Other surcharges';
2094 return 1 if $a->itemdesc eq 'Other surcharges';
2095 $a->itemdesc cmp $b->itemdesc;
2100 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2101 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2104 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2106 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2107 list of hashrefs describing the line items they generate on the invoice.
2109 OPTIONS may include:
2111 format: the invoice format.
2113 escape_function: the function used to escape strings.
2115 DEPRECATED? (expensive, mostly unused?)
2116 format_function: the function used to format CDRs.
2118 section: a hashref containing 'description'; if this is present,
2119 cust_bill_pkg_display records not belonging to this section are
2122 multisection: a flag indicating that this is a multisection invoice,
2123 which does something complicated.
2125 Returns a list of hashrefs, each of which may contain:
2127 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2128 ext_description, which is an arrayref of detail lines to show below
2133 sub _items_cust_bill_pkg {
2135 my $conf = $self->conf;
2136 my $cust_bill_pkgs = shift;
2139 my $format = $opt{format} || '';
2140 my $escape_function = $opt{escape_function} || sub { shift };
2141 my $format_function = $opt{format_function} || '';
2142 my $no_usage = $opt{no_usage} || '';
2143 my $unsquelched = $opt{unsquelched} || ''; #unused
2144 my $section = $opt{section}->{description} if $opt{section};
2145 my $summary_page = $opt{summary_page} || ''; #unused
2146 my $multisection = $opt{multisection} || '';
2147 my $discount_show_always = 0;
2149 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2151 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2152 # and location labels
2155 my ($s, $r, $u) = ( undef, undef, undef );
2156 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2159 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2160 if ( $_ && !$cust_bill_pkg->hidden ) {
2161 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2162 $_->{amount} =~ s/^\-0\.00$/0.00/;
2163 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2165 if $_->{amount} != 0
2166 || $discount_show_always
2167 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2168 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2174 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2175 ? $cust_bill_pkg->cust_bill_pkg_display
2176 : ( $cust_bill_pkg );
2178 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2179 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2182 foreach my $display ( grep { defined($section)
2183 ? $_->section eq $section
2186 grep { !$_->summary || $multisection }
2187 @cust_bill_pkg_display
2191 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2192 $display->billpkgdisplaynum. "\n"
2195 my $type = $display->type;
2197 my $desc = $cust_bill_pkg->desc;
2198 $desc = substr($desc, 0, $maxlength). '...'
2199 if $format eq 'latex' && length($desc) > $maxlength;
2201 my %details_opt = ( 'format' => $format,
2202 'escape_function' => $escape_function,
2203 'format_function' => $format_function,
2204 'no_usage' => $opt{'no_usage'},
2207 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2209 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2212 if ( $cust_bill_pkg->setup != 0 ) {
2213 my $description = $desc;
2214 $description .= ' Setup'
2215 if $cust_bill_pkg->recur != 0
2216 || $discount_show_always
2217 || $cust_bill_pkg->recur_show_zero;
2219 'description' => $description,
2220 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2223 if ( $cust_bill_pkg->recur != 0 ) {
2225 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2226 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2230 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2232 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2235 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2237 # which pkgpart to show for display purposes?
2238 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2240 # start/end dates for invoice formats that do nonstandard
2242 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2244 if ( (!$type || $type eq 'S')
2245 && ( $cust_bill_pkg->setup != 0
2246 || $cust_bill_pkg->setup_show_zero
2251 warn "$me _items_cust_bill_pkg adding setup\n"
2254 my $description = $desc;
2255 $description .= ' Setup'
2256 if $cust_bill_pkg->recur != 0
2257 || $discount_show_always
2258 || $cust_bill_pkg->recur_show_zero;
2261 unless ( $cust_pkg->part_pkg->hide_svc_detail
2262 || $cust_bill_pkg->hidden )
2265 push @d, map &{$escape_function}($_),
2266 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2267 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2269 if ( ! $cust_pkg->locationnum or
2270 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2271 my $loc = $cust_pkg->location_label;
2272 $loc = substr($loc, 0, $maxlength). '...'
2273 if $format eq 'latex' && length($loc) > $maxlength;
2274 push @d, &{$escape_function}($loc);
2277 } #unless hiding service details
2279 push @d, $cust_bill_pkg->details(%details_opt)
2280 if $cust_bill_pkg->recur == 0;
2282 if ( $cust_bill_pkg->hidden ) {
2283 $s->{amount} += $cust_bill_pkg->setup;
2284 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2285 push @{ $s->{ext_description} }, @d;
2289 description => $description,
2290 pkgpart => $pkgpart,
2291 pkgnum => $cust_bill_pkg->pkgnum,
2292 amount => $cust_bill_pkg->setup,
2293 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2294 unit_amount => $cust_bill_pkg->unitsetup,
2295 quantity => $cust_bill_pkg->quantity,
2296 ext_description => \@d,
2302 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2304 $cust_bill_pkg->recur != 0
2305 || $cust_bill_pkg->setup == 0
2306 || $discount_show_always
2307 || $cust_bill_pkg->recur_show_zero
2312 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2315 my $is_summary = $display->summary;
2316 my $description = ($is_summary && $type && $type eq 'U')
2317 ? "Usage charges" : $desc;
2319 #pry be a bit more efficient to look some of this conf stuff up
2322 $conf->exists('disable_line_item_date_ranges')
2323 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2326 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2327 $cust_main->agentnum
2329 if ( defined($date_style) && $date_style eq 'month_of' ) {
2330 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2331 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2332 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2333 $cust_main->agentnum
2335 $desc .= ' ' unless $desc =~ /\s$/;
2336 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2338 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2339 " - ". time2str($date_format, $cust_bill_pkg->edate);
2341 $description .= " ($time_period)";
2345 my @seconds = (); # for display of usage info
2347 #at least until cust_bill_pkg has "past" ranges in addition to
2348 #the "future" sdate/edate ones... see #3032
2349 my @dates = ( $self->_date );
2350 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2351 push @dates, $prev->sdate if $prev;
2352 push @dates, undef if !$prev;
2354 unless ( $cust_pkg->part_pkg->hide_svc_detail
2355 || $cust_bill_pkg->itemdesc
2356 || $cust_bill_pkg->hidden
2357 || $is_summary && $type && $type eq 'U'
2361 warn "$me _items_cust_bill_pkg adding service details\n"
2364 push @d, map &{$escape_function}($_),
2365 $cust_pkg->h_labels_short(@dates, 'I')
2366 #$cust_bill_pkg->edate,
2367 #$cust_bill_pkg->sdate)
2368 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2370 warn "$me _items_cust_bill_pkg done adding service details\n"
2373 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2374 my $loc = $cust_pkg->location_label;
2375 $loc = substr($loc, 0, $maxlength). '...'
2376 if $format eq 'latex' && length($loc) > $maxlength;
2377 push @d, &{$escape_function}($loc);
2380 # Display of seconds_since_sqlradacct:
2381 # On the invoice, when processing @detail_items, look for a field
2382 # named 'seconds'. This will contain total seconds for each
2383 # service, in the same order as @ext_description. For services
2384 # that don't support this it will show undef.
2385 if ( $conf->exists('svc_acct-usage_seconds')
2386 and ! $cust_bill_pkg->pkgpart_override ) {
2387 foreach my $cust_svc (
2388 $cust_pkg->h_cust_svc(@dates, 'I')
2391 # eval because not having any part_export_usage exports
2392 # is a fatal error, last_bill/_date because that's how
2393 # sqlradius_hour billing does it
2395 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2397 push @seconds, $sec;
2399 } #if svc_acct-usage_seconds
2403 unless ( $is_summary ) {
2404 warn "$me _items_cust_bill_pkg adding details\n"
2407 #instead of omitting details entirely in this case (unwanted side
2408 # effects), just omit CDRs
2409 $details_opt{'no_usage'} = 1
2410 if $type && $type eq 'R';
2412 push @d, $cust_bill_pkg->details(%details_opt);
2415 warn "$me _items_cust_bill_pkg calculating amount\n"
2420 $amount = $cust_bill_pkg->recur;
2421 } elsif ($type eq 'R') {
2422 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2423 } elsif ($type eq 'U') {
2424 $amount = $cust_bill_pkg->usage;
2428 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2431 if ( !$type || $type eq 'R' ) {
2433 warn "$me _items_cust_bill_pkg adding recur\n"
2436 if ( $cust_bill_pkg->hidden ) {
2437 $r->{amount} += $amount;
2438 $r->{unit_amount} += $unit_amount;
2439 push @{ $r->{ext_description} }, @d;
2442 description => $description,
2443 pkgpart => $pkgpart,
2444 pkgnum => $cust_bill_pkg->pkgnum,
2446 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2447 unit_amount => $unit_amount,
2448 quantity => $cust_bill_pkg->quantity,
2450 ext_description => \@d,
2452 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2455 } else { # $type eq 'U'
2457 warn "$me _items_cust_bill_pkg adding usage\n"
2460 if ( $cust_bill_pkg->hidden ) {
2461 $u->{amount} += $amount;
2462 $u->{unit_amount} += $unit_amount,
2463 push @{ $u->{ext_description} }, @d;
2466 description => $description,
2467 pkgpart => $pkgpart,
2468 pkgnum => $cust_bill_pkg->pkgnum,
2470 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2471 unit_amount => $unit_amount,
2472 quantity => $cust_bill_pkg->quantity,
2474 ext_description => \@d,
2479 } # recurring or usage with recurring charge
2481 } else { #pkgnum tax or one-shot line item (??)
2483 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2486 if ( $cust_bill_pkg->setup != 0 ) {
2488 'description' => $desc,
2489 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2492 if ( $cust_bill_pkg->recur != 0 ) {
2494 'description' => "$desc (".
2495 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2496 time2str($date_format, $cust_bill_pkg->edate). ')',
2497 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2505 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2506 && $conf->exists('discount-show-always'));
2510 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2512 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2513 $_->{amount} =~ s/^\-0\.00$/0.00/;
2514 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2516 if $_->{amount} != 0
2517 || $discount_show_always
2518 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2519 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2523 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2530 =item _items_discounts_avail
2532 Returns an array of line item hashrefs representing available term discounts
2533 for this invoice. This makes the same assumptions that apply to term
2534 discounts in general: that the package is billed monthly, at a flat rate,
2535 with no usage charges. A prorated first month will be handled, as will
2536 a setup fee if the discount is allowed to apply to setup fees.
2540 sub _items_discounts_avail {
2543 #maybe move this method from cust_bill when quotations support discount_plans
2544 return () unless $self->can('discount_plans');
2545 my %plans = $self->discount_plans;
2547 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2548 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2552 my $plan = $plans{$months};
2554 my $term_total = sprintf('%.2f', $plan->discounted_total);
2555 my $percent = sprintf('%.0f',
2556 100 * (1 - $term_total / $plan->base_total) );
2557 my $permonth = sprintf('%.2f', $term_total / $months);
2558 my $detail = $self->mt('discount on item'). ' '.
2559 join(', ', map { "#$_" } $plan->pkgnums)
2562 # discounts for non-integer months don't work anyway
2563 $months = sprintf("%d", $months);
2566 description => $self->mt('Save [_1]% by paying for [_2] months',
2568 amount => $self->mt('[_1] ([_2] per month)',
2569 $term_total, $money_char.$permonth),
2570 ext_description => ($detail || ''),
2573 sort { $b <=> $a } keys %plans;