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 #these are used on the summary page only
589 # the customer's current balance as shown on the invoice before this one
590 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
592 # the change in balance from that invoice to this one
593 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
595 # the sum of amount owed on all previous invoices
596 # ($pr_total is used elsewhere but not as $previous_balance)
597 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
599 # the sum of amount owed on all invoices
600 # (this is used in the summary & on the payment coupon)
601 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
603 # info from customer's last invoice before this one, for some
605 $invoice_data{'last_bill'} = {};
606 my $last_bill = $pr_cust_bill[-1];
608 $invoice_data{'last_bill'} = {
609 '_date' => $last_bill->_date, #unformatted
610 # all we need for now
614 my $summarypage = '';
615 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
618 $invoice_data{'summarypage'} = $summarypage;
620 warn "$me substituting variables in notes, footer, smallfooter\n"
623 my $tc = $self->template_conf;
624 my @include = ( [ $tc, 'notes' ],
625 [ 'invoice_', 'footer' ],
626 [ 'invoice_', 'smallfooter', ],
628 push @include, [ $tc, 'coupon', ]
629 unless $params{'no_coupon'};
631 foreach my $i (@include) {
633 my($base, $include) = @$i;
635 my $inc_file = $conf->key_orbase("$base$format$include", $template);
638 if ( $conf->exists($inc_file, $agentnum)
639 && length( $conf->config($inc_file, $agentnum) ) ) {
641 @inc_src = $conf->config($inc_file, $agentnum);
645 $inc_file = $conf->key_orbase("${base}latex$include", $template);
647 my $convert_map = $convert_maps{$format}{$include};
649 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
650 s/--\@\]/$delimiters{$format}[1]/g;
653 &$convert_map( $conf->config($inc_file, $agentnum) );
657 my $inc_tt = new Text::Template (
659 SOURCE => [ map "$_\n", @inc_src ],
660 DELIMITERS => $delimiters{$format},
661 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
663 unless ( $inc_tt->compile() ) {
664 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
665 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
669 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
671 $invoice_data{$include} =~ s/\n+$//
672 if ($format eq 'latex');
675 # let invoices use either of these as needed
676 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
677 ? $cust_main->payinfo : '';
678 $invoice_data{'po_line'} =
679 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
680 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
683 my %money_chars = ( 'latex' => '',
684 'html' => $conf->config('money_char') || '$',
687 my $money_char = $money_chars{$format};
689 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
690 'html' => $conf->config('money_char') || '$',
693 my $other_money_char = $other_money_chars{$format};
694 $invoice_data{'dollar'} = $other_money_char;
696 my @detail_items = ();
697 my @total_items = ();
701 $invoice_data{'detail_items'} = \@detail_items;
702 $invoice_data{'total_items'} = \@total_items;
703 $invoice_data{'buf'} = \@buf;
704 $invoice_data{'sections'} = \@sections;
706 warn "$me generating sections\n"
709 # Previous Charges section
710 # subtotal is the first return value from $self->previous
711 my $previous_section = { 'description' => $self->mt('Previous Charges'),
712 'subtotal' => $other_money_char.
713 sprintf('%.2f', $pr_total),
714 'summarized' => '', #why? $summarypage ? 'Y' : '',
716 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
717 join(' / ', map { $cust_main->balance_date_range(@$_) }
718 $self->_prior_month30s
720 if $conf->exists('invoice_include_aging');
723 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
724 'subtotal' => $taxtotal, # adjusted below
726 my $tax_weight = _pkg_category($tax_section->{description})
727 ? _pkg_category($tax_section->{description})->weight
729 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
730 $tax_section->{'sort_weight'} = $tax_weight;
734 my $adjust_section = {
735 'description' => $self->mt('Credits, Payments, and Adjustments'),
736 'adjust_section' => 1,
737 'subtotal' => 0, # adjusted below
739 my $adjust_weight = _pkg_category($adjust_section->{description})
740 ? _pkg_category($adjust_section->{description})->weight
742 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
743 $adjust_section->{'sort_weight'} = $adjust_weight;
745 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
746 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
747 $invoice_data{'multisection'} = $multisection;
748 my $late_sections = [];
749 my $extra_sections = [];
750 my $extra_lines = ();
752 my $default_section = { 'description' => '',
757 if ( $multisection ) {
758 ($extra_sections, $extra_lines) =
759 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
760 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
761 && $self->can('_items_extra_usage_sections');
763 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
765 push @detail_items, @$extra_lines if $extra_lines;
767 $self->_items_sections( $late_sections, # this could stand a refactor
769 $escape_function_nonbsp,
773 if ( $conf->exists('svc_phone_sections')
774 && $self->can('_items_svc_phone_sections')
777 my ($phone_sections, $phone_lines) =
778 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
779 push @{$late_sections}, @$phone_sections;
780 push @detail_items, @$phone_lines;
782 if ( $conf->exists('voip-cust_accountcode_cdr')
783 && $cust_main->accountcode_cdr
784 && $self->can('_items_accountcode_cdr')
787 my ($accountcode_section, $accountcode_lines) =
788 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
789 if ( scalar(@$accountcode_lines) ) {
790 push @{$late_sections}, $accountcode_section;
791 push @detail_items, @$accountcode_lines;
794 } else {# not multisection
795 # make a default section
796 push @sections, $default_section;
797 # and calculate the finance charge total, since it won't get done otherwise.
798 # XXX possibly other totals?
799 # XXX possibly finance_pkgclass should not be used in this manner?
800 if ( $conf->exists('finance_pkgclass') ) {
802 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
803 if ( grep { $_->section eq $invoice_data{finance_section} }
804 $cust_bill_pkg->cust_bill_pkg_display ) {
805 # I think these are always setup fees, but just to be sure...
806 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
809 $invoice_data{finance_amount} =
810 sprintf('%.2f', sum( @finance_charges ) || 0);
814 # previous invoice balances in the Previous Charges section if there
815 # is one, otherwise in the main detail section
816 if ( $self->can('_items_previous') &&
817 $self->enable_previous &&
818 ! $conf->exists('previous_balance-summary_only') ) {
820 warn "$me adding previous balances\n"
823 foreach my $line_item ( $self->_items_previous ) {
826 ext_description => [],
828 $detail->{'ref'} = $line_item->{'pkgnum'};
829 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
830 $detail->{'quantity'} = 1;
831 $detail->{'section'} = $multisection ? $previous_section
833 $detail->{'description'} = &$escape_function($line_item->{'description'});
834 if ( exists $line_item->{'ext_description'} ) {
835 @{$detail->{'ext_description'}} = map {
836 &$escape_function($_);
837 } @{$line_item->{'ext_description'}};
839 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
840 $line_item->{'amount'};
841 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
843 push @detail_items, $detail;
844 push @buf, [ $detail->{'description'},
845 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
851 if ( @pr_cust_bill && $self->enable_previous ) {
852 push @buf, ['','-----------'];
853 push @buf, [ $self->mt('Total Previous Balance'),
854 $money_char. sprintf("%10.2f", $pr_total) ];
858 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
859 warn "$me adding DID summary\n"
862 my ($didsummary,$minutes) = $self->_did_summary;
863 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
865 { 'description' => $didsummary_desc,
866 'ext_description' => [ $didsummary, $minutes ],
870 foreach my $section (@sections, @$late_sections) {
872 warn "$me adding section \n". Dumper($section)
875 # begin some normalization
876 $section->{'subtotal'} = $section->{'amount'}
878 && !exists($section->{subtotal})
879 && exists($section->{amount});
881 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
882 if ( $invoice_data{finance_section} &&
883 $section->{'description'} eq $invoice_data{finance_section} );
885 $section->{'subtotal'} = $other_money_char.
886 sprintf('%.2f', $section->{'subtotal'})
889 # continue some normalization
890 $section->{'amount'} = $section->{'subtotal'}
894 if ( $section->{'description'} ) {
895 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
900 warn "$me setting options\n"
904 $options{'section'} = $section if $multisection;
905 $options{'format'} = $format;
906 $options{'escape_function'} = $escape_function;
907 $options{'no_usage'} = 1 unless $unsquelched;
908 $options{'unsquelched'} = $unsquelched;
909 $options{'summary_page'} = $summarypage;
910 $options{'skip_usage'} =
911 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
912 $options{'multisection'} = $multisection;
914 warn "$me searching for line items\n"
917 foreach my $line_item ( $self->_items_pkg(%options) ) {
919 warn "$me adding line item $line_item\n"
923 ext_description => [],
925 $detail->{'ref'} = $line_item->{'pkgnum'};
926 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
927 $detail->{'quantity'} = $line_item->{'quantity'};
928 $detail->{'section'} = $section;
929 $detail->{'description'} = &$escape_function($line_item->{'description'});
930 if ( exists $line_item->{'ext_description'} ) {
931 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
933 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
934 $line_item->{'amount'};
935 if ( exists $line_item->{'unit_amount'} ) {
936 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
937 $line_item->{'unit_amount'};
939 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
941 $detail->{'sdate'} = $line_item->{'sdate'};
942 $detail->{'edate'} = $line_item->{'edate'};
943 $detail->{'seconds'} = $line_item->{'seconds'};
944 $detail->{'svc_label'} = $line_item->{'svc_label'};
946 push @detail_items, $detail;
947 push @buf, ( [ $detail->{'description'},
948 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
950 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
954 if ( $section->{'description'} ) {
955 push @buf, ( ['','-----------'],
956 [ $section->{'description'}. ' sub-total',
957 $section->{'subtotal'} # already formatted this
966 $invoice_data{current_less_finance} =
967 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
969 # create a major section for previous balance if we have major sections,
970 # or if previous_section is in summary form
971 if ( ( $multisection && $self->enable_previous )
972 || $conf->exists('previous_balance-summary_only') )
974 unshift @sections, $previous_section if $pr_total;
977 warn "$me adding taxes\n"
980 foreach my $tax ( $self->_items_tax ) {
982 $taxtotal += $tax->{'amount'};
984 my $description = &$escape_function( $tax->{'description'} );
985 my $amount = sprintf( '%.2f', $tax->{'amount'} );
987 if ( $multisection ) {
989 my $money = $old_latex ? '' : $money_char;
990 push @detail_items, {
991 ext_description => [],
994 description => $description,
995 amount => $money. $amount,
997 section => $tax_section,
1002 push @total_items, {
1003 'total_item' => $description,
1004 'total_amount' => $other_money_char. $amount,
1009 push @buf,[ $description,
1010 $money_char. $amount,
1017 $total->{'total_item'} = $self->mt('Sub-total');
1018 $total->{'total_amount'} =
1019 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1021 if ( $multisection ) {
1022 $tax_section->{'subtotal'} = $other_money_char.
1023 sprintf('%.2f', $taxtotal);
1024 $tax_section->{'pretotal'} = 'New charges sub-total '.
1025 $total->{'total_amount'};
1026 push @sections, $tax_section if $taxtotal;
1028 unshift @total_items, $total;
1031 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1033 push @buf,['','-----------'];
1034 push @buf,[$self->mt(
1035 (!$self->enable_previous)
1037 : 'Total New Charges'
1039 $money_char. sprintf("%10.2f",$self->charged) ];
1042 # calculate total, possibly including total owed on previous
1047 $item = $conf->config('previous_balance-exclude_from_total')
1048 || 'Total New Charges'
1049 if $conf->exists('previous_balance-exclude_from_total');
1050 my $amount = $self->charged;
1051 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1052 $amount += $pr_total;
1055 $total->{'total_item'} = &$embolden_function($self->mt($item));
1056 $total->{'total_amount'} =
1057 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1058 if ( $multisection ) {
1059 if ( $adjust_section->{'sort_weight'} ) {
1060 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1061 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1063 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1064 $other_money_char. sprintf('%.2f', $self->charged );
1067 push @total_items, $total;
1069 push @buf,['','-----------'];
1072 sprintf( '%10.2f', $amount )
1077 # if we're showing previous invoices, also show previous
1078 # credits and payments
1079 if ( $self->enable_previous
1080 and $self->can('_items_credits')
1081 and $self->can('_items_payments') )
1083 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1086 my $credittotal = 0;
1087 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1090 $total->{'total_item'} = &$escape_function($credit->{'description'});
1091 $credittotal += $credit->{'amount'};
1092 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1093 $adjusttotal += $credit->{'amount'};
1094 if ( $multisection ) {
1095 my $money = $old_latex ? '' : $money_char;
1096 push @detail_items, {
1097 ext_description => [],
1100 description => &$escape_function($credit->{'description'}),
1101 amount => $money. $credit->{'amount'},
1103 section => $adjust_section,
1106 push @total_items, $total;
1110 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1113 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1114 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1118 my $paymenttotal = 0;
1119 foreach my $payment ( $self->_items_payments ) {
1121 $total->{'total_item'} = &$escape_function($payment->{'description'});
1122 $paymenttotal += $payment->{'amount'};
1123 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1124 $adjusttotal += $payment->{'amount'};
1125 if ( $multisection ) {
1126 my $money = $old_latex ? '' : $money_char;
1127 push @detail_items, {
1128 ext_description => [],
1131 description => &$escape_function($payment->{'description'}),
1132 amount => $money. $payment->{'amount'},
1134 section => $adjust_section,
1137 push @total_items, $total;
1139 push @buf, [ $payment->{'description'},
1140 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1143 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1145 if ( $multisection ) {
1146 $adjust_section->{'subtotal'} = $other_money_char.
1147 sprintf('%.2f', $adjusttotal);
1148 push @sections, $adjust_section
1149 unless $adjust_section->{sort_weight};
1152 # create Balance Due message
1155 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1156 $total->{'total_amount'} =
1157 &$embolden_function(
1158 $other_money_char. sprintf('%.2f', #why? $summarypage
1159 # ? $self->charged +
1160 # $self->billing_balance
1162 $self->owed + $pr_total
1165 if ( $multisection && !$adjust_section->{sort_weight} ) {
1166 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1167 $total->{'total_amount'};
1169 push @total_items, $total;
1171 push @buf,['','-----------'];
1172 push @buf,[$self->balance_due_msg, $money_char.
1173 sprintf("%10.2f", $balance_due ) ];
1176 if ( $conf->exists('previous_balance-show_credit')
1177 and $cust_main->balance < 0 ) {
1178 my $credit_total = {
1179 'total_item' => &$embolden_function($self->credit_balance_msg),
1180 'total_amount' => &$embolden_function(
1181 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1184 if ( $multisection ) {
1185 $adjust_section->{'posttotal'} .= $newline_token .
1186 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1189 push @total_items, $credit_total;
1191 push @buf,['','-----------'];
1192 push @buf,[$self->credit_balance_msg, $money_char.
1193 sprintf("%10.2f", -$cust_main->balance ) ];
1197 if ( $multisection ) {
1198 if ( $conf->exists('svc_phone_sections')
1199 && $self->can('_items_svc_phone_sections')
1203 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1204 $total->{'total_amount'} =
1205 &$embolden_function(
1206 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1208 my $last_section = pop @sections;
1209 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1210 $total->{'total_amount'};
1211 push @sections, $last_section;
1213 push @sections, @$late_sections
1217 # make a discounts-available section, even without multisection
1218 if ( $conf->exists('discount-show_available')
1219 and my @discounts_avail = $self->_items_discounts_avail ) {
1220 my $discount_section = {
1221 'description' => $self->mt('Discounts Available'),
1226 push @sections, $discount_section;
1227 push @detail_items, map { +{
1228 'ref' => '', #should this be something else?
1229 'section' => $discount_section,
1230 'description' => &$escape_function( $_->{description} ),
1231 'amount' => $money_char . &$escape_function( $_->{amount} ),
1232 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1233 } } @discounts_avail;
1236 # debugging hook: call this with 'diag' => 1 to just get a hash of
1237 # the invoice variables
1238 return \%invoice_data if ( $params{'diag'} );
1240 # All sections and items are built; now fill in templates.
1241 my @includelist = ();
1242 push @includelist, 'summary' if $summarypage;
1243 foreach my $include ( @includelist ) {
1245 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1248 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1250 @inc_src = $conf->config($inc_file, $agentnum);
1254 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1256 my $convert_map = $convert_maps{$format}{$include};
1258 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1259 s/--\@\]/$delimiters{$format}[1]/g;
1262 &$convert_map( $conf->config($inc_file, $agentnum) );
1266 my $inc_tt = new Text::Template (
1268 SOURCE => [ map "$_\n", @inc_src ],
1269 DELIMITERS => $delimiters{$format},
1270 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1272 unless ( $inc_tt->compile() ) {
1273 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1274 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1278 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1280 $invoice_data{$include} =~ s/\n+$//
1281 if ($format eq 'latex');
1286 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1287 /invoice_lines\((\d*)\)/;
1288 $invoice_lines += $1 || scalar(@buf);
1291 die "no invoice_lines() functions in template?"
1292 if ( $format eq 'template' && !$wasfunc );
1294 if ($format eq 'template') {
1296 if ( $invoice_lines ) {
1297 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1298 $invoice_data{'total_pages'}++
1299 if scalar(@buf) % $invoice_lines;
1302 #setup subroutine for the template
1303 $invoice_data{invoice_lines} = sub {
1304 my $lines = shift || scalar(@buf);
1316 push @collect, split("\n",
1317 $text_template->fill_in( HASH => \%invoice_data )
1319 $invoice_data{'page'}++;
1321 map "$_\n", @collect;
1323 } else { # this is where we actually create the invoice
1325 warn "filling in template for invoice ". $self->invnum. "\n"
1327 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1330 $text_template->fill_in(HASH => \%invoice_data);
1334 sub notice_name { '('.shift->table.')'; }
1336 sub template_conf { 'invoice_'; }
1338 # helper routine for generating date ranges
1339 sub _prior_month30s {
1342 [ 1, 2592000 ], # 0-30 days ago
1343 [ 2592000, 5184000 ], # 30-60 days ago
1344 [ 5184000, 7776000 ], # 60-90 days ago
1345 [ 7776000, 0 ], # 90+ days ago
1348 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1349 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1354 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1356 Returns an postscript invoice, as a scalar.
1358 Options can be passed as a hashref (recommended) or as a list of time, template
1359 and then any key/value pairs for any other options.
1361 I<time> an optional value used to control the printing of overdue messages. The
1362 default is now. It isn't the date of the invoice; that's the `_date' field.
1363 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1364 L<Time::Local> and L<Date::Parse> for conversion functions.
1366 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1373 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1374 my $ps = generate_ps($file);
1376 unlink($barcodefile) if $barcodefile;
1381 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1383 Returns an PDF invoice, as a scalar.
1385 Options can be passed as a hashref (recommended) or as a list of time, template
1386 and then any key/value pairs for any other options.
1388 I<time> an optional value used to control the printing of overdue messages. The
1389 default is now. It isn't the date of the invoice; that's the `_date' field.
1390 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1391 L<Time::Local> and L<Date::Parse> for conversion functions.
1393 I<template>, if specified, is the name of a suffix for alternate invoices.
1395 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1402 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1403 my $pdf = generate_pdf($file);
1405 unlink($barcodefile) if $barcodefile;
1410 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1412 Returns an HTML invoice, as a scalar.
1414 I<time> an optional value used to control the printing of overdue messages. The
1415 default is now. It isn't the date of the invoice; that's the `_date' field.
1416 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1417 L<Time::Local> and L<Date::Parse> for conversion functions.
1419 I<template>, if specified, is the name of a suffix for alternate invoices.
1421 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1423 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1424 when emailing the invoice as part of a multipart/related MIME email.
1432 %params = %{ shift() };
1434 $params{'time'} = shift;
1435 $params{'template'} = shift;
1436 $params{'cid'} = shift;
1439 $params{'format'} = 'html';
1441 $self->print_generic( %params );
1444 # quick subroutine for print_latex
1446 # There are ten characters that LaTeX treats as special characters, which
1447 # means that they do not simply typeset themselves:
1448 # # $ % & ~ _ ^ \ { }
1450 # TeX ignores blanks following an escaped character; if you want a blank (as
1451 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1455 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1456 $value =~ s/([<>])/\$$1\$/g;
1462 encode_entities($value);
1466 sub _html_escape_nbsp {
1467 my $value = _html_escape(shift);
1468 $value =~ s/ +/ /g;
1472 #utility methods for print_*
1474 sub _translate_old_latex_format {
1475 warn "_translate_old_latex_format called\n"
1482 if ( $line =~ /^%%Detail\s*$/ ) {
1484 push @template, q![@--!,
1485 q! foreach my $_tr_line (@detail_items) {!,
1486 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1487 q! $_tr_line->{'description'} .= !,
1488 q! "\\tabularnewline\n~~".!,
1489 q! join( "\\tabularnewline\n~~",!,
1490 q! @{$_tr_line->{'ext_description'}}!,
1494 while ( ( my $line_item_line = shift )
1495 !~ /^%%EndDetail\s*$/ ) {
1496 $line_item_line =~ s/'/\\'/g; # nice LTS
1497 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1498 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1499 push @template, " \$OUT .= '$line_item_line';";
1502 push @template, '}',
1505 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1507 push @template, '[@--',
1508 ' foreach my $_tr_line (@total_items) {';
1510 while ( ( my $total_item_line = shift )
1511 !~ /^%%EndTotalDetails\s*$/ ) {
1512 $total_item_line =~ s/'/\\'/g; # nice LTS
1513 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1514 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1515 push @template, " \$OUT .= '$total_item_line';";
1518 push @template, '}',
1522 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1523 push @template, $line;
1529 warn "$_\n" foreach @template;
1537 my $conf = $self->conf;
1539 #check for an invoice-specific override
1540 return $self->invoice_terms if $self->invoice_terms;
1542 #check for a customer- specific override
1543 my $cust_main = $self->cust_main;
1544 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1546 #use configured default
1547 $conf->config('invoice_default_terms') || '';
1553 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1554 $duedate = $self->_date() + ( $1 * 86400 );
1561 $self->due_date ? time2str(shift, $self->due_date) : '';
1564 sub balance_due_msg {
1566 my $msg = $self->mt('Balance Due');
1567 return $msg unless $self->terms;
1568 if ( $self->due_date ) {
1569 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1570 $self->due_date2str($date_format);
1571 } elsif ( $self->terms ) {
1572 $msg .= ' - '. $self->terms;
1577 sub balance_due_date {
1579 my $conf = $self->conf;
1581 if ( $conf->exists('invoice_default_terms')
1582 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1583 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1588 sub credit_balance_msg {
1590 $self->mt('Credit Balance Remaining')
1595 Returns a string with the date, for example: "3/20/2008"
1601 time2str($date_format, $self->_date);
1604 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1606 Generate section information for all items appearing on this invoice.
1607 This will only be called for multi-section invoices.
1609 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1610 related display records (L<FS::cust_bill_pkg_display>) and organize
1611 them into two groups ("early" and "late" according to whether they come
1612 before or after the total), then into sections. A subtotal is calculated
1615 Section descriptions are returned in sort weight order. Each consists
1616 of a hash containing:
1618 description: the package category name, escaped
1619 subtotal: the total charges in that section
1620 tax_section: a flag indicating that the section contains only tax charges
1621 summarized: same as tax_section, for some reason
1622 sort_weight: the package category's sort weight
1624 If 'condense' is set on the display record, it also contains everything
1625 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1626 coderefs to generate parts of the invoice. This is not advised.
1630 LATE: an arrayref to push the "late" section hashes onto. The "early"
1631 group is simply returned from the method.
1633 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1634 Turning this on has the following effects:
1635 - Ignores display items with the 'summary' flag.
1636 - Combines all items into the "early" group.
1637 - Creates sections for all non-disabled package categories, even if they
1638 have no charges on this invoice, as well as a section with no name.
1640 ESCAPE: an escape function to use for section titles.
1642 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1643 sorted list. If there are any of these, section subtotals exclude
1646 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1647 passed through to C<_condense_section()>.
1651 use vars qw(%pkg_category_cache);
1652 sub _items_sections {
1655 my $summarypage = shift;
1657 my $extra_sections = shift;
1661 my %late_subtotal = ();
1664 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1667 my $usage = $cust_bill_pkg->usage;
1669 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1670 next if ( $display->summary && $summarypage );
1672 my $section = $display->section;
1673 my $type = $display->type;
1675 $not_tax{$section} = 1
1676 unless $cust_bill_pkg->pkgnum == 0;
1678 # there's actually a very important piece of logic buried in here:
1679 # incrementing $late_subtotal{$section} CREATES
1680 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1681 # to define the list of late sections, and likewise keys(%subtotal).
1682 # When _items_cust_bill_pkg is called to generate line items for
1683 # real, it will be called with 'section' => $section for each
1685 if ( $display->post_total && !$summarypage ) {
1686 if (! $type || $type eq 'S') {
1687 $late_subtotal{$section} += $cust_bill_pkg->setup
1688 if $cust_bill_pkg->setup != 0
1689 || $cust_bill_pkg->setup_show_zero;
1693 $late_subtotal{$section} += $cust_bill_pkg->recur
1694 if $cust_bill_pkg->recur != 0
1695 || $cust_bill_pkg->recur_show_zero;
1698 if ($type && $type eq 'R') {
1699 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1700 if $cust_bill_pkg->recur != 0
1701 || $cust_bill_pkg->recur_show_zero;
1704 if ($type && $type eq 'U') {
1705 $late_subtotal{$section} += $usage
1706 unless scalar(@$extra_sections);
1711 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1713 if (! $type || $type eq 'S') {
1714 $subtotal{$section} += $cust_bill_pkg->setup
1715 if $cust_bill_pkg->setup != 0
1716 || $cust_bill_pkg->setup_show_zero;
1720 $subtotal{$section} += $cust_bill_pkg->recur
1721 if $cust_bill_pkg->recur != 0
1722 || $cust_bill_pkg->recur_show_zero;
1725 if ($type && $type eq 'R') {
1726 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1727 if $cust_bill_pkg->recur != 0
1728 || $cust_bill_pkg->recur_show_zero;
1731 if ($type && $type eq 'U') {
1732 $subtotal{$section} += $usage
1733 unless scalar(@$extra_sections);
1742 %pkg_category_cache = ();
1744 push @$late, map { { 'description' => &{$escape}($_),
1745 'subtotal' => $late_subtotal{$_},
1747 'sort_weight' => ( _pkg_category($_)
1748 ? _pkg_category($_)->weight
1751 ((_pkg_category($_) && _pkg_category($_)->condense)
1752 ? $self->_condense_section($format)
1756 sort _sectionsort keys %late_subtotal;
1759 if ( $summarypage ) {
1760 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1761 map { $_->categoryname } qsearch('pkg_category', {});
1762 push @sections, '' if exists($subtotal{''});
1764 @sections = keys %subtotal;
1767 my @early = map { { 'description' => &{$escape}($_),
1768 'subtotal' => $subtotal{$_},
1769 'summarized' => $not_tax{$_} ? '' : 'Y',
1770 'tax_section' => $not_tax{$_} ? '' : 'Y',
1771 'sort_weight' => ( _pkg_category($_)
1772 ? _pkg_category($_)->weight
1775 ((_pkg_category($_) && _pkg_category($_)->condense)
1776 ? $self->_condense_section($format)
1781 push @early, @$extra_sections if $extra_sections;
1783 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1787 #helper subs for above
1790 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1794 my $categoryname = shift;
1795 $pkg_category_cache{$categoryname} ||=
1796 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1799 my %condensed_format = (
1800 'label' => [ qw( Description Qty Amount ) ],
1802 sub { shift->{description} },
1803 sub { shift->{quantity} },
1804 sub { my($href, %opt) = @_;
1805 ($opt{dollar} || ''). $href->{amount};
1808 'align' => [ qw( l r r ) ],
1809 'span' => [ qw( 5 1 1 ) ], # unitprices?
1810 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1813 sub _condense_section {
1814 my ( $self, $format ) = ( shift, shift );
1816 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1817 qw( description_generator
1820 total_line_generator
1825 sub _condensed_generator_defaults {
1826 my ( $self, $format ) = ( shift, shift );
1827 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1836 sub _condensed_header_generator {
1837 my ( $self, $format ) = ( shift, shift );
1839 my ( $f, $prefix, $suffix, $separator, $column ) =
1840 _condensed_generator_defaults($format);
1842 if ($format eq 'latex') {
1843 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1844 $suffix = "\\\\\n\\hline";
1847 sub { my ($d,$a,$s,$w) = @_;
1848 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1850 } elsif ( $format eq 'html' ) {
1851 $prefix = '<th></th>';
1855 sub { my ($d,$a,$s,$w) = @_;
1856 return qq!<th align="$html_align{$a}">$d</th>!;
1864 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1866 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1869 $prefix. join($separator, @result). $suffix;
1874 sub _condensed_description_generator {
1875 my ( $self, $format ) = ( shift, shift );
1877 my ( $f, $prefix, $suffix, $separator, $column ) =
1878 _condensed_generator_defaults($format);
1880 my $money_char = '$';
1881 if ($format eq 'latex') {
1882 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1884 $separator = " & \n";
1886 sub { my ($d,$a,$s,$w) = @_;
1887 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1889 $money_char = '\\dollar';
1890 }elsif ( $format eq 'html' ) {
1891 $prefix = '"><td align="center"></td>';
1895 sub { my ($d,$a,$s,$w) = @_;
1896 return qq!<td align="$html_align{$a}">$d</td>!;
1898 #$money_char = $conf->config('money_char') || '$';
1899 $money_char = ''; # this is madness
1907 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1909 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1911 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1912 map { $f->{$_}->[$i] } qw(align span width)
1916 $prefix. join( $separator, @result ). $suffix;
1921 sub _condensed_total_generator {
1922 my ( $self, $format ) = ( shift, shift );
1924 my ( $f, $prefix, $suffix, $separator, $column ) =
1925 _condensed_generator_defaults($format);
1928 if ($format eq 'latex') {
1931 $separator = " & \n";
1933 sub { my ($d,$a,$s,$w) = @_;
1934 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1936 }elsif ( $format eq 'html' ) {
1940 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1942 sub { my ($d,$a,$s,$w) = @_;
1943 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1952 # my $r = &{$f->{fields}->[$i]}(@args);
1953 # $r .= ' Total' unless $i;
1955 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1957 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1958 map { $f->{$_}->[$i] } qw(align span width)
1962 $prefix. join( $separator, @result ). $suffix;
1967 =item total_line_generator FORMAT
1969 Returns a coderef used for generation of invoice total line items for this
1970 usage_class. FORMAT is either html or latex
1974 # should not be used: will have issues with hash element names (description vs
1975 # total_item and amount vs total_amount -- another array of functions?
1977 sub _condensed_total_line_generator {
1978 my ( $self, $format ) = ( shift, shift );
1980 my ( $f, $prefix, $suffix, $separator, $column ) =
1981 _condensed_generator_defaults($format);
1984 if ($format eq 'latex') {
1987 $separator = " & \n";
1989 sub { my ($d,$a,$s,$w) = @_;
1990 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1992 }elsif ( $format eq 'html' ) {
1996 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1998 sub { my ($d,$a,$s,$w) = @_;
1999 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2008 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2010 &{$column}( &{$f->{fields}->[$i]}(@args),
2011 map { $f->{$_}->[$i] } qw(align span width)
2015 $prefix. join( $separator, @result ). $suffix;
2020 # sub _items { # seems to be unused
2023 # #my @display = scalar(@_)
2025 # # : qw( _items_previous _items_pkg );
2026 # # #: qw( _items_pkg );
2027 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2028 # my @display = qw( _items_previous _items_pkg );
2031 # foreach my $display ( @display ) {
2032 # push @b, $self->$display(@_);
2037 =item _items_pkg [ OPTIONS ]
2039 Return line item hashes for each package item on this invoice. Nearly
2042 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2044 The only OPTIONS accepted is 'section', which may point to a hashref
2045 with a key named 'condensed', which may have a true value. If it
2046 does, this method tries to merge identical items into items with
2047 'quantity' equal to the number of items (not the sum of their
2048 separate quantities, for some reason).
2054 grep { $_->pkgnum } $self->cust_bill_pkg;
2061 warn "$me _items_pkg searching for all package line items\n"
2064 my @cust_bill_pkg = $self->_items_nontax;
2066 warn "$me _items_pkg filtering line items\n"
2068 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2070 if ($options{section} && $options{section}->{condensed}) {
2072 warn "$me _items_pkg condensing section\n"
2076 local $Storable::canonical = 1;
2077 foreach ( @items ) {
2079 delete $item->{ref};
2080 delete $item->{ext_description};
2081 my $key = freeze($item);
2082 $itemshash{$key} ||= 0;
2083 $itemshash{$key} ++; # += $item->{quantity};
2085 @items = sort { $a->{description} cmp $b->{description} }
2086 map { my $i = thaw($_);
2087 $i->{quantity} = $itemshash{$_};
2089 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2095 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2102 return 0 unless $a->itemdesc cmp $b->itemdesc;
2103 return -1 if $b->itemdesc eq 'Tax';
2104 return 1 if $a->itemdesc eq 'Tax';
2105 return -1 if $b->itemdesc eq 'Other surcharges';
2106 return 1 if $a->itemdesc eq 'Other surcharges';
2107 $a->itemdesc cmp $b->itemdesc;
2112 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2113 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2116 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2118 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2119 list of hashrefs describing the line items they generate on the invoice.
2121 OPTIONS may include:
2123 format: the invoice format.
2125 escape_function: the function used to escape strings.
2127 DEPRECATED? (expensive, mostly unused?)
2128 format_function: the function used to format CDRs.
2130 section: a hashref containing 'description'; if this is present,
2131 cust_bill_pkg_display records not belonging to this section are
2134 multisection: a flag indicating that this is a multisection invoice,
2135 which does something complicated.
2137 Returns a list of hashrefs, each of which may contain:
2139 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2140 ext_description, which is an arrayref of detail lines to show below
2145 sub _items_cust_bill_pkg {
2147 my $conf = $self->conf;
2148 my $cust_bill_pkgs = shift;
2151 my $format = $opt{format} || '';
2152 my $escape_function = $opt{escape_function} || sub { shift };
2153 my $format_function = $opt{format_function} || '';
2154 my $no_usage = $opt{no_usage} || '';
2155 my $unsquelched = $opt{unsquelched} || ''; #unused
2156 my $section = $opt{section}->{description} if $opt{section};
2157 my $summary_page = $opt{summary_page} || ''; #unused
2158 my $multisection = $opt{multisection} || '';
2159 my $discount_show_always = 0;
2161 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2163 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2164 # and location labels
2167 my ($s, $r, $u) = ( undef, undef, undef );
2168 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2171 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2172 if ( $_ && !$cust_bill_pkg->hidden ) {
2173 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2174 $_->{amount} =~ s/^\-0\.00$/0.00/;
2175 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2177 if $_->{amount} != 0
2178 || $discount_show_always
2179 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2180 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2186 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2187 ? $cust_bill_pkg->cust_bill_pkg_display
2188 : ( $cust_bill_pkg );
2190 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2191 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2194 foreach my $display ( grep { defined($section)
2195 ? $_->section eq $section
2198 grep { !$_->summary || $multisection }
2199 @cust_bill_pkg_display
2203 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2204 $display->billpkgdisplaynum. "\n"
2207 my $type = $display->type;
2209 my $desc = $cust_bill_pkg->desc;
2210 $desc = substr($desc, 0, $maxlength). '...'
2211 if $format eq 'latex' && length($desc) > $maxlength;
2213 my %details_opt = ( 'format' => $format,
2214 'escape_function' => $escape_function,
2215 'format_function' => $format_function,
2216 'no_usage' => $opt{'no_usage'},
2219 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2221 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2224 if ( $cust_bill_pkg->setup != 0 ) {
2225 my $description = $desc;
2226 $description .= ' Setup'
2227 if $cust_bill_pkg->recur != 0
2228 || $discount_show_always
2229 || $cust_bill_pkg->recur_show_zero;
2231 'description' => $description,
2232 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2235 if ( $cust_bill_pkg->recur != 0 ) {
2237 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2238 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2242 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2244 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2247 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2249 # which pkgpart to show for display purposes?
2250 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2252 # start/end dates for invoice formats that do nonstandard
2254 my %item_dates = ();
2255 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2256 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2258 if ( (!$type || $type eq 'S')
2259 && ( $cust_bill_pkg->setup != 0
2260 || $cust_bill_pkg->setup_show_zero
2265 warn "$me _items_cust_bill_pkg adding setup\n"
2268 my $description = $desc;
2269 $description .= ' Setup'
2270 if $cust_bill_pkg->recur != 0
2271 || $discount_show_always
2272 || $cust_bill_pkg->recur_show_zero;
2276 unless ( $cust_pkg->part_pkg->hide_svc_detail
2277 || $cust_bill_pkg->hidden )
2280 my @svc_labels = map &{$escape_function}($_),
2281 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2282 push @d, @svc_labels
2283 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2284 $svc_label = $svc_labels[0];
2286 if ( ! $cust_pkg->locationnum or
2287 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2288 my $loc = $cust_pkg->location_label;
2289 $loc = substr($loc, 0, $maxlength). '...'
2290 if $format eq 'latex' && length($loc) > $maxlength;
2291 push @d, &{$escape_function}($loc);
2294 } #unless hiding service details
2296 push @d, $cust_bill_pkg->details(%details_opt)
2297 if $cust_bill_pkg->recur == 0;
2299 if ( $cust_bill_pkg->hidden ) {
2300 $s->{amount} += $cust_bill_pkg->setup;
2301 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2302 push @{ $s->{ext_description} }, @d;
2306 description => $description,
2307 pkgpart => $pkgpart,
2308 pkgnum => $cust_bill_pkg->pkgnum,
2309 amount => $cust_bill_pkg->setup,
2310 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2311 unit_amount => $cust_bill_pkg->unitsetup,
2312 quantity => $cust_bill_pkg->quantity,
2313 ext_description => \@d,
2314 svc_label => ($svc_label || ''),
2320 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2322 $cust_bill_pkg->recur != 0
2323 || $cust_bill_pkg->setup == 0
2324 || $discount_show_always
2325 || $cust_bill_pkg->recur_show_zero
2330 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2333 my $is_summary = $display->summary;
2334 my $description = ($is_summary && $type && $type eq 'U')
2335 ? "Usage charges" : $desc;
2337 my $part_pkg = $cust_pkg->part_pkg;
2339 #pry be a bit more efficient to look some of this conf stuff up
2342 $conf->exists('disable_line_item_date_ranges')
2343 || $part_pkg->option('disable_line_item_date_ranges',1)
2344 || ! $cust_bill_pkg->sdate
2345 || ! $cust_bill_pkg->edate
2348 my $date_style = '';
2349 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2350 $cust_main->agentnum
2352 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2353 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2354 $cust_main->agentnum
2356 if ( defined($date_style) && $date_style eq 'month_of' ) {
2357 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2358 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2359 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2360 $cust_main->agentnum
2362 $desc .= ' ' unless $desc =~ /\s$/;
2363 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2365 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2366 " - ". time2str($date_format, $cust_bill_pkg->edate);
2368 $description .= " ($time_period)";
2372 my @seconds = (); # for display of usage info
2375 #at least until cust_bill_pkg has "past" ranges in addition to
2376 #the "future" sdate/edate ones... see #3032
2377 my @dates = ( $self->_date );
2378 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2379 push @dates, $prev->sdate if $prev;
2380 push @dates, undef if !$prev;
2382 unless ( $part_pkg->hide_svc_detail
2383 || $cust_bill_pkg->itemdesc
2384 || $cust_bill_pkg->hidden
2385 || $is_summary && $type && $type eq 'U'
2389 warn "$me _items_cust_bill_pkg adding service details\n"
2392 my @svc_labels = map &{$escape_function}($_),
2393 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2394 push @d, @svc_labels
2395 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2396 $svc_label = $svc_labels[0];
2398 warn "$me _items_cust_bill_pkg done adding service details\n"
2401 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2402 my $loc = $cust_pkg->location_label;
2403 $loc = substr($loc, 0, $maxlength). '...'
2404 if $format eq 'latex' && length($loc) > $maxlength;
2405 push @d, &{$escape_function}($loc);
2408 # Display of seconds_since_sqlradacct:
2409 # On the invoice, when processing @detail_items, look for a field
2410 # named 'seconds'. This will contain total seconds for each
2411 # service, in the same order as @ext_description. For services
2412 # that don't support this it will show undef.
2413 if ( $conf->exists('svc_acct-usage_seconds')
2414 and ! $cust_bill_pkg->pkgpart_override ) {
2415 foreach my $cust_svc (
2416 $cust_pkg->h_cust_svc(@dates, 'I')
2419 # eval because not having any part_export_usage exports
2420 # is a fatal error, last_bill/_date because that's how
2421 # sqlradius_hour billing does it
2423 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2425 push @seconds, $sec;
2427 } #if svc_acct-usage_seconds
2431 unless ( $is_summary ) {
2432 warn "$me _items_cust_bill_pkg adding details\n"
2435 #instead of omitting details entirely in this case (unwanted side
2436 # effects), just omit CDRs
2437 $details_opt{'no_usage'} = 1
2438 if $type && $type eq 'R';
2440 push @d, $cust_bill_pkg->details(%details_opt);
2443 warn "$me _items_cust_bill_pkg calculating amount\n"
2448 $amount = $cust_bill_pkg->recur;
2449 } elsif ($type eq 'R') {
2450 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2451 } elsif ($type eq 'U') {
2452 $amount = $cust_bill_pkg->usage;
2456 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2459 if ( !$type || $type eq 'R' ) {
2461 warn "$me _items_cust_bill_pkg adding recur\n"
2464 if ( $cust_bill_pkg->hidden ) {
2465 $r->{amount} += $amount;
2466 $r->{unit_amount} += $unit_amount;
2467 push @{ $r->{ext_description} }, @d;
2470 description => $description,
2471 pkgpart => $pkgpart,
2472 pkgnum => $cust_bill_pkg->pkgnum,
2474 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2475 unit_amount => $unit_amount,
2476 quantity => $cust_bill_pkg->quantity,
2478 ext_description => \@d,
2479 svc_label => ($svc_label || ''),
2481 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2484 } else { # $type eq 'U'
2486 warn "$me _items_cust_bill_pkg adding usage\n"
2489 if ( $cust_bill_pkg->hidden ) {
2490 $u->{amount} += $amount;
2491 $u->{unit_amount} += $unit_amount,
2492 push @{ $u->{ext_description} }, @d;
2495 description => $description,
2496 pkgpart => $pkgpart,
2497 pkgnum => $cust_bill_pkg->pkgnum,
2499 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2500 unit_amount => $unit_amount,
2501 quantity => $cust_bill_pkg->quantity,
2503 ext_description => \@d,
2508 } # recurring or usage with recurring charge
2510 } else { #pkgnum tax or one-shot line item (??)
2512 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2515 if ( $cust_bill_pkg->setup != 0 ) {
2517 'description' => $desc,
2518 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2521 if ( $cust_bill_pkg->recur != 0 ) {
2523 'description' => "$desc (".
2524 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2525 time2str($date_format, $cust_bill_pkg->edate). ')',
2526 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2534 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2535 && $conf->exists('discount-show-always'));
2539 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2541 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2542 $_->{amount} =~ s/^\-0\.00$/0.00/;
2543 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2545 if $_->{amount} != 0
2546 || $discount_show_always
2547 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2548 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2552 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2559 =item _items_discounts_avail
2561 Returns an array of line item hashrefs representing available term discounts
2562 for this invoice. This makes the same assumptions that apply to term
2563 discounts in general: that the package is billed monthly, at a flat rate,
2564 with no usage charges. A prorated first month will be handled, as will
2565 a setup fee if the discount is allowed to apply to setup fees.
2569 sub _items_discounts_avail {
2572 #maybe move this method from cust_bill when quotations support discount_plans
2573 return () unless $self->can('discount_plans');
2574 my %plans = $self->discount_plans;
2576 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2577 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2581 my $plan = $plans{$months};
2583 my $term_total = sprintf('%.2f', $plan->discounted_total);
2584 my $percent = sprintf('%.0f',
2585 100 * (1 - $term_total / $plan->base_total) );
2586 my $permonth = sprintf('%.2f', $term_total / $months);
2587 my $detail = $self->mt('discount on item'). ' '.
2588 join(', ', map { "#$_" } $plan->pkgnums)
2591 # discounts for non-integer months don't work anyway
2592 $months = sprintf("%d", $months);
2595 description => $self->mt('Save [_1]% by paying for [_2] months',
2597 amount => $self->mt('[_1] ([_2] per month)',
2598 $term_total, $money_char.$permonth),
2599 ext_description => ($detail || ''),
2602 sort { $b <=> $a } keys %plans;