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 unless ( $conf->exists('disable_previous_balance', $agentnum)
810 || $conf->exists('previous_balance-summary_only')
811 || ! $self->can('_items_previous')
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->{'quantity'} = 1;
825 $detail->{'section'} = $multisection ? $previous_section
827 $detail->{'description'} = &$escape_function($line_item->{'description'});
828 if ( exists $line_item->{'ext_description'} ) {
829 @{$detail->{'ext_description'}} = map {
830 &$escape_function($_);
831 } @{$line_item->{'ext_description'}};
833 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
834 $line_item->{'amount'};
835 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
837 push @detail_items, $detail;
838 push @buf, [ $detail->{'description'},
839 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
845 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
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"
898 my $multilocation = scalar($cust_main->cust_location); #too expensive?
900 $options{'section'} = $section if $multisection;
901 $options{'format'} = $format;
902 $options{'escape_function'} = $escape_function;
903 $options{'no_usage'} = 1 unless $unsquelched;
904 $options{'unsquelched'} = $unsquelched;
905 $options{'summary_page'} = $summarypage;
906 $options{'skip_usage'} =
907 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
908 $options{'multilocation'} = $multilocation;
909 $options{'multisection'} = $multisection;
911 warn "$me searching for line items\n"
914 foreach my $line_item ( $self->_items_pkg(%options) ) {
916 warn "$me adding line item $line_item\n"
920 ext_description => [],
922 $detail->{'ref'} = $line_item->{'pkgnum'};
923 $detail->{'quantity'} = $line_item->{'quantity'};
924 $detail->{'section'} = $section;
925 $detail->{'description'} = &$escape_function($line_item->{'description'});
926 if ( exists $line_item->{'ext_description'} ) {
927 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
929 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
930 $line_item->{'amount'};
931 if ( exists $line_item->{'unit_amount'} ) {
932 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
933 $line_item->{'unit_amount'};
935 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
937 $detail->{'sdate'} = $line_item->{'sdate'};
938 $detail->{'edate'} = $line_item->{'edate'};
939 $detail->{'seconds'} = $line_item->{'seconds'};
941 push @detail_items, $detail;
942 push @buf, ( [ $detail->{'description'},
943 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
945 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
949 if ( $section->{'description'} ) {
950 push @buf, ( ['','-----------'],
951 [ $section->{'description'}. ' sub-total',
952 $section->{'subtotal'} # already formatted this
961 $invoice_data{current_less_finance} =
962 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
964 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
965 || $conf->exists('previous_balance-summary_only') )
967 unshift @sections, $previous_section if $pr_total;
970 warn "$me adding taxes\n"
973 foreach my $tax ( $self->_items_tax ) {
975 $taxtotal += $tax->{'amount'};
977 my $description = &$escape_function( $tax->{'description'} );
978 my $amount = sprintf( '%.2f', $tax->{'amount'} );
980 if ( $multisection ) {
982 my $money = $old_latex ? '' : $money_char;
983 push @detail_items, {
984 ext_description => [],
987 description => $description,
988 amount => $money. $amount,
990 section => $tax_section,
996 'total_item' => $description,
997 'total_amount' => $other_money_char. $amount,
1002 push @buf,[ $description,
1003 $money_char. $amount,
1010 $total->{'total_item'} = $self->mt('Sub-total');
1011 $total->{'total_amount'} =
1012 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1014 if ( $multisection ) {
1015 $tax_section->{'subtotal'} = $other_money_char.
1016 sprintf('%.2f', $taxtotal);
1017 $tax_section->{'pretotal'} = 'New charges sub-total '.
1018 $total->{'total_amount'};
1019 push @sections, $tax_section if $taxtotal;
1021 unshift @total_items, $total;
1024 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1026 push @buf,['','-----------'];
1027 push @buf,[$self->mt(
1028 $conf->exists('disable_previous_balance', $agentnum)
1030 : 'Total New Charges'
1032 $money_char. sprintf("%10.2f",$self->charged) ];
1038 $item = $conf->config('previous_balance-exclude_from_total')
1039 || 'Total New Charges'
1040 if $conf->exists('previous_balance-exclude_from_total');
1041 my $amount = $self->charged +
1042 ( $conf->exists('disable_previous_balance', $agentnum) ||
1043 $conf->exists('previous_balance-exclude_from_total')
1047 $total->{'total_item'} = &$embolden_function($self->mt($item));
1048 $total->{'total_amount'} =
1049 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1050 if ( $multisection ) {
1051 if ( $adjust_section->{'sort_weight'} ) {
1052 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1053 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1055 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1056 $other_money_char. sprintf('%.2f', $self->charged );
1059 push @total_items, $total;
1061 push @buf,['','-----------'];
1064 sprintf( '%10.2f', $amount )
1069 unless ( $conf->exists('disable_previous_balance', $agentnum)
1070 || ! $self->can('_items_credits')
1071 || ! $self->can('_items_payments')
1074 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1077 my $credittotal = 0;
1078 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1081 $total->{'total_item'} = &$escape_function($credit->{'description'});
1082 $credittotal += $credit->{'amount'};
1083 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1084 $adjusttotal += $credit->{'amount'};
1085 if ( $multisection ) {
1086 my $money = $old_latex ? '' : $money_char;
1087 push @detail_items, {
1088 ext_description => [],
1091 description => &$escape_function($credit->{'description'}),
1092 amount => $money. $credit->{'amount'},
1094 section => $adjust_section,
1097 push @total_items, $total;
1101 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1104 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1105 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1109 my $paymenttotal = 0;
1110 foreach my $payment ( $self->_items_payments ) {
1112 $total->{'total_item'} = &$escape_function($payment->{'description'});
1113 $paymenttotal += $payment->{'amount'};
1114 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1115 $adjusttotal += $payment->{'amount'};
1116 if ( $multisection ) {
1117 my $money = $old_latex ? '' : $money_char;
1118 push @detail_items, {
1119 ext_description => [],
1122 description => &$escape_function($payment->{'description'}),
1123 amount => $money. $payment->{'amount'},
1125 section => $adjust_section,
1128 push @total_items, $total;
1130 push @buf, [ $payment->{'description'},
1131 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1134 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1136 if ( $multisection ) {
1137 $adjust_section->{'subtotal'} = $other_money_char.
1138 sprintf('%.2f', $adjusttotal);
1139 push @sections, $adjust_section
1140 unless $adjust_section->{sort_weight};
1143 # create Balance Due message
1146 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1147 $total->{'total_amount'} =
1148 &$embolden_function(
1149 $other_money_char. sprintf('%.2f', $summarypage
1151 $self->billing_balance
1152 : $self->owed + $pr_total
1155 if ( $multisection && !$adjust_section->{sort_weight} ) {
1156 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1157 $total->{'total_amount'};
1159 push @total_items, $total;
1161 push @buf,['','-----------'];
1162 push @buf,[$self->balance_due_msg, $money_char.
1163 sprintf("%10.2f", $balance_due ) ];
1166 if ( $conf->exists('previous_balance-show_credit')
1167 and $cust_main->balance < 0 ) {
1168 my $credit_total = {
1169 'total_item' => &$embolden_function($self->credit_balance_msg),
1170 'total_amount' => &$embolden_function(
1171 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1174 if ( $multisection ) {
1175 $adjust_section->{'posttotal'} .= $newline_token .
1176 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1179 push @total_items, $credit_total;
1181 push @buf,['','-----------'];
1182 push @buf,[$self->credit_balance_msg, $money_char.
1183 sprintf("%10.2f", -$cust_main->balance ) ];
1187 if ( $multisection ) {
1188 if ( $conf->exists('svc_phone_sections')
1189 && $self->can('_items_svc_phone_sections')
1193 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1194 $total->{'total_amount'} =
1195 &$embolden_function(
1196 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1198 my $last_section = pop @sections;
1199 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1200 $total->{'total_amount'};
1201 push @sections, $last_section;
1203 push @sections, @$late_sections
1207 # make a discounts-available section, even without multisection
1208 if ( $conf->exists('discount-show_available')
1209 and my @discounts_avail = $self->_items_discounts_avail ) {
1210 my $discount_section = {
1211 'description' => $self->mt('Discounts Available'),
1216 push @sections, $discount_section;
1217 push @detail_items, map { +{
1218 'ref' => '', #should this be something else?
1219 'section' => $discount_section,
1220 'description' => &$escape_function( $_->{description} ),
1221 'amount' => $money_char . &$escape_function( $_->{amount} ),
1222 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1223 } } @discounts_avail;
1226 # All sections and items are built; now fill in templates.
1227 my @includelist = ();
1228 push @includelist, 'summary' if $summarypage;
1229 foreach my $include ( @includelist ) {
1231 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1234 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1236 @inc_src = $conf->config($inc_file, $agentnum);
1240 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1242 my $convert_map = $convert_maps{$format}{$include};
1244 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1245 s/--\@\]/$delimiters{$format}[1]/g;
1248 &$convert_map( $conf->config($inc_file, $agentnum) );
1252 my $inc_tt = new Text::Template (
1254 SOURCE => [ map "$_\n", @inc_src ],
1255 DELIMITERS => $delimiters{$format},
1256 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1258 unless ( $inc_tt->compile() ) {
1259 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1260 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1264 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1266 $invoice_data{$include} =~ s/\n+$//
1267 if ($format eq 'latex');
1272 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1273 /invoice_lines\((\d*)\)/;
1274 $invoice_lines += $1 || scalar(@buf);
1277 die "no invoice_lines() functions in template?"
1278 if ( $format eq 'template' && !$wasfunc );
1280 if ($format eq 'template') {
1282 if ( $invoice_lines ) {
1283 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1284 $invoice_data{'total_pages'}++
1285 if scalar(@buf) % $invoice_lines;
1288 #setup subroutine for the template
1289 $invoice_data{invoice_lines} = sub {
1290 my $lines = shift || scalar(@buf);
1302 push @collect, split("\n",
1303 $text_template->fill_in( HASH => \%invoice_data )
1305 $invoice_data{'page'}++;
1307 map "$_\n", @collect;
1309 } else { # this is where we actually create the invoice
1311 warn "filling in template for invoice ". $self->invnum. "\n"
1313 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1316 $text_template->fill_in(HASH => \%invoice_data);
1320 sub notice_name { '('.shift->table.')'; }
1322 sub template_conf { 'invoice_'; }
1324 # helper routine for generating date ranges
1325 sub _prior_month30s {
1328 [ 1, 2592000 ], # 0-30 days ago
1329 [ 2592000, 5184000 ], # 30-60 days ago
1330 [ 5184000, 7776000 ], # 60-90 days ago
1331 [ 7776000, 0 ], # 90+ days ago
1334 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1335 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1340 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1342 Returns an postscript invoice, as a scalar.
1344 Options can be passed as a hashref (recommended) or as a list of time, template
1345 and then any key/value pairs for any other options.
1347 I<time> an optional value used to control the printing of overdue messages. The
1348 default is now. It isn't the date of the invoice; that's the `_date' field.
1349 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1350 L<Time::Local> and L<Date::Parse> for conversion functions.
1352 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1359 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1360 my $ps = generate_ps($file);
1362 unlink($barcodefile) if $barcodefile;
1367 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1369 Returns an PDF invoice, as a scalar.
1371 Options can be passed as a hashref (recommended) or as a list of time, template
1372 and then any key/value pairs for any other options.
1374 I<time> an optional value used to control the printing of overdue messages. The
1375 default is now. It isn't the date of the invoice; that's the `_date' field.
1376 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1377 L<Time::Local> and L<Date::Parse> for conversion functions.
1379 I<template>, if specified, is the name of a suffix for alternate invoices.
1381 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1388 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1389 my $pdf = generate_pdf($file);
1391 unlink($barcodefile) if $barcodefile;
1396 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1398 Returns an HTML invoice, as a scalar.
1400 I<time> an optional value used to control the printing of overdue messages. The
1401 default is now. It isn't the date of the invoice; that's the `_date' field.
1402 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1403 L<Time::Local> and L<Date::Parse> for conversion functions.
1405 I<template>, if specified, is the name of a suffix for alternate invoices.
1407 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1409 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1410 when emailing the invoice as part of a multipart/related MIME email.
1418 %params = %{ shift() };
1420 $params{'time'} = shift;
1421 $params{'template'} = shift;
1422 $params{'cid'} = shift;
1425 $params{'format'} = 'html';
1427 $self->print_generic( %params );
1430 # quick subroutine for print_latex
1432 # There are ten characters that LaTeX treats as special characters, which
1433 # means that they do not simply typeset themselves:
1434 # # $ % & ~ _ ^ \ { }
1436 # TeX ignores blanks following an escaped character; if you want a blank (as
1437 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1441 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1442 $value =~ s/([<>])/\$$1\$/g;
1448 encode_entities($value);
1452 sub _html_escape_nbsp {
1453 my $value = _html_escape(shift);
1454 $value =~ s/ +/ /g;
1458 #utility methods for print_*
1460 sub _translate_old_latex_format {
1461 warn "_translate_old_latex_format called\n"
1468 if ( $line =~ /^%%Detail\s*$/ ) {
1470 push @template, q![@--!,
1471 q! foreach my $_tr_line (@detail_items) {!,
1472 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1473 q! $_tr_line->{'description'} .= !,
1474 q! "\\tabularnewline\n~~".!,
1475 q! join( "\\tabularnewline\n~~",!,
1476 q! @{$_tr_line->{'ext_description'}}!,
1480 while ( ( my $line_item_line = shift )
1481 !~ /^%%EndDetail\s*$/ ) {
1482 $line_item_line =~ s/'/\\'/g; # nice LTS
1483 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1484 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1485 push @template, " \$OUT .= '$line_item_line';";
1488 push @template, '}',
1491 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1493 push @template, '[@--',
1494 ' foreach my $_tr_line (@total_items) {';
1496 while ( ( my $total_item_line = shift )
1497 !~ /^%%EndTotalDetails\s*$/ ) {
1498 $total_item_line =~ s/'/\\'/g; # nice LTS
1499 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1500 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1501 push @template, " \$OUT .= '$total_item_line';";
1504 push @template, '}',
1508 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1509 push @template, $line;
1515 warn "$_\n" foreach @template;
1523 my $conf = $self->conf;
1525 #check for an invoice-specific override
1526 return $self->invoice_terms if $self->invoice_terms;
1528 #check for a customer- specific override
1529 my $cust_main = $self->cust_main;
1530 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1532 #use configured default
1533 $conf->config('invoice_default_terms') || '';
1539 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1540 $duedate = $self->_date() + ( $1 * 86400 );
1547 $self->due_date ? time2str(shift, $self->due_date) : '';
1550 sub balance_due_msg {
1552 my $msg = $self->mt('Balance Due');
1553 return $msg unless $self->terms;
1554 if ( $self->due_date ) {
1555 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1556 $self->due_date2str($date_format);
1557 } elsif ( $self->terms ) {
1558 $msg .= ' - '. $self->terms;
1563 sub balance_due_date {
1565 my $conf = $self->conf;
1567 if ( $conf->exists('invoice_default_terms')
1568 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1569 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1574 sub credit_balance_msg {
1576 $self->mt('Credit Balance Remaining')
1581 Returns a string with the date, for example: "3/20/2008"
1587 time2str($date_format, $self->_date);
1590 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1592 Generate section information for all items appearing on this invoice.
1593 This will only be called for multi-section invoices.
1595 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1596 related display records (L<FS::cust_bill_pkg_display>) and organize
1597 them into two groups ("early" and "late" according to whether they come
1598 before or after the total), then into sections. A subtotal is calculated
1601 Section descriptions are returned in sort weight order. Each consists
1602 of a hash containing:
1604 description: the package category name, escaped
1605 subtotal: the total charges in that section
1606 tax_section: a flag indicating that the section contains only tax charges
1607 summarized: same as tax_section, for some reason
1608 sort_weight: the package category's sort weight
1610 If 'condense' is set on the display record, it also contains everything
1611 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1612 coderefs to generate parts of the invoice. This is not advised.
1616 LATE: an arrayref to push the "late" section hashes onto. The "early"
1617 group is simply returned from the method.
1619 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1620 Turning this on has the following effects:
1621 - Ignores display items with the 'summary' flag.
1622 - Combines all items into the "early" group.
1623 - Creates sections for all non-disabled package categories, even if they
1624 have no charges on this invoice, as well as a section with no name.
1626 ESCAPE: an escape function to use for section titles.
1628 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1629 sorted list. If there are any of these, section subtotals exclude
1632 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1633 passed through to C<_condense_section()>.
1637 use vars qw(%pkg_category_cache);
1638 sub _items_sections {
1641 my $summarypage = shift;
1643 my $extra_sections = shift;
1647 my %late_subtotal = ();
1650 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1653 my $usage = $cust_bill_pkg->usage;
1655 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1656 next if ( $display->summary && $summarypage );
1658 my $section = $display->section;
1659 my $type = $display->type;
1661 $not_tax{$section} = 1
1662 unless $cust_bill_pkg->pkgnum == 0;
1664 if ( $display->post_total && !$summarypage ) {
1665 if (! $type || $type eq 'S') {
1666 $late_subtotal{$section} += $cust_bill_pkg->setup
1667 if $cust_bill_pkg->setup != 0
1668 || $cust_bill_pkg->setup_show_zero;
1672 $late_subtotal{$section} += $cust_bill_pkg->recur
1673 if $cust_bill_pkg->recur != 0
1674 || $cust_bill_pkg->recur_show_zero;
1677 if ($type && $type eq 'R') {
1678 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1679 if $cust_bill_pkg->recur != 0
1680 || $cust_bill_pkg->recur_show_zero;
1683 if ($type && $type eq 'U') {
1684 $late_subtotal{$section} += $usage
1685 unless scalar(@$extra_sections);
1690 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1692 if (! $type || $type eq 'S') {
1693 $subtotal{$section} += $cust_bill_pkg->setup
1694 if $cust_bill_pkg->setup != 0
1695 || $cust_bill_pkg->setup_show_zero;
1699 $subtotal{$section} += $cust_bill_pkg->recur
1700 if $cust_bill_pkg->recur != 0
1701 || $cust_bill_pkg->recur_show_zero;
1704 if ($type && $type eq 'R') {
1705 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1706 if $cust_bill_pkg->recur != 0
1707 || $cust_bill_pkg->recur_show_zero;
1710 if ($type && $type eq 'U') {
1711 $subtotal{$section} += $usage
1712 unless scalar(@$extra_sections);
1721 %pkg_category_cache = ();
1723 push @$late, map { { 'description' => &{$escape}($_),
1724 'subtotal' => $late_subtotal{$_},
1726 'sort_weight' => ( _pkg_category($_)
1727 ? _pkg_category($_)->weight
1730 ((_pkg_category($_) && _pkg_category($_)->condense)
1731 ? $self->_condense_section($format)
1735 sort _sectionsort keys %late_subtotal;
1738 if ( $summarypage ) {
1739 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1740 map { $_->categoryname } qsearch('pkg_category', {});
1741 push @sections, '' if exists($subtotal{''});
1743 @sections = keys %subtotal;
1746 my @early = map { { 'description' => &{$escape}($_),
1747 'subtotal' => $subtotal{$_},
1748 'summarized' => $not_tax{$_} ? '' : 'Y',
1749 'tax_section' => $not_tax{$_} ? '' : 'Y',
1750 'sort_weight' => ( _pkg_category($_)
1751 ? _pkg_category($_)->weight
1754 ((_pkg_category($_) && _pkg_category($_)->condense)
1755 ? $self->_condense_section($format)
1760 push @early, @$extra_sections if $extra_sections;
1762 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1766 #helper subs for above
1769 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1773 my $categoryname = shift;
1774 $pkg_category_cache{$categoryname} ||=
1775 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1778 my %condensed_format = (
1779 'label' => [ qw( Description Qty Amount ) ],
1781 sub { shift->{description} },
1782 sub { shift->{quantity} },
1783 sub { my($href, %opt) = @_;
1784 ($opt{dollar} || ''). $href->{amount};
1787 'align' => [ qw( l r r ) ],
1788 'span' => [ qw( 5 1 1 ) ], # unitprices?
1789 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1792 sub _condense_section {
1793 my ( $self, $format ) = ( shift, shift );
1795 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1796 qw( description_generator
1799 total_line_generator
1804 sub _condensed_generator_defaults {
1805 my ( $self, $format ) = ( shift, shift );
1806 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1815 sub _condensed_header_generator {
1816 my ( $self, $format ) = ( shift, shift );
1818 my ( $f, $prefix, $suffix, $separator, $column ) =
1819 _condensed_generator_defaults($format);
1821 if ($format eq 'latex') {
1822 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1823 $suffix = "\\\\\n\\hline";
1826 sub { my ($d,$a,$s,$w) = @_;
1827 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1829 } elsif ( $format eq 'html' ) {
1830 $prefix = '<th></th>';
1834 sub { my ($d,$a,$s,$w) = @_;
1835 return qq!<th align="$html_align{$a}">$d</th>!;
1843 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1845 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1848 $prefix. join($separator, @result). $suffix;
1853 sub _condensed_description_generator {
1854 my ( $self, $format ) = ( shift, shift );
1856 my ( $f, $prefix, $suffix, $separator, $column ) =
1857 _condensed_generator_defaults($format);
1859 my $money_char = '$';
1860 if ($format eq 'latex') {
1861 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1863 $separator = " & \n";
1865 sub { my ($d,$a,$s,$w) = @_;
1866 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1868 $money_char = '\\dollar';
1869 }elsif ( $format eq 'html' ) {
1870 $prefix = '"><td align="center"></td>';
1874 sub { my ($d,$a,$s,$w) = @_;
1875 return qq!<td align="$html_align{$a}">$d</td>!;
1877 #$money_char = $conf->config('money_char') || '$';
1878 $money_char = ''; # this is madness
1886 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1888 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1890 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1891 map { $f->{$_}->[$i] } qw(align span width)
1895 $prefix. join( $separator, @result ). $suffix;
1900 sub _condensed_total_generator {
1901 my ( $self, $format ) = ( shift, shift );
1903 my ( $f, $prefix, $suffix, $separator, $column ) =
1904 _condensed_generator_defaults($format);
1907 if ($format eq 'latex') {
1910 $separator = " & \n";
1912 sub { my ($d,$a,$s,$w) = @_;
1913 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1915 }elsif ( $format eq 'html' ) {
1919 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1921 sub { my ($d,$a,$s,$w) = @_;
1922 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1931 # my $r = &{$f->{fields}->[$i]}(@args);
1932 # $r .= ' Total' unless $i;
1934 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1936 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1937 map { $f->{$_}->[$i] } qw(align span width)
1941 $prefix. join( $separator, @result ). $suffix;
1946 =item total_line_generator FORMAT
1948 Returns a coderef used for generation of invoice total line items for this
1949 usage_class. FORMAT is either html or latex
1953 # should not be used: will have issues with hash element names (description vs
1954 # total_item and amount vs total_amount -- another array of functions?
1956 sub _condensed_total_line_generator {
1957 my ( $self, $format ) = ( shift, shift );
1959 my ( $f, $prefix, $suffix, $separator, $column ) =
1960 _condensed_generator_defaults($format);
1963 if ($format eq 'latex') {
1966 $separator = " & \n";
1968 sub { my ($d,$a,$s,$w) = @_;
1969 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1971 }elsif ( $format eq 'html' ) {
1975 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1977 sub { my ($d,$a,$s,$w) = @_;
1978 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1987 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1989 &{$column}( &{$f->{fields}->[$i]}(@args),
1990 map { $f->{$_}->[$i] } qw(align span width)
1994 $prefix. join( $separator, @result ). $suffix;
1999 # sub _items { # seems to be unused
2002 # #my @display = scalar(@_)
2004 # # : qw( _items_previous _items_pkg );
2005 # # #: qw( _items_pkg );
2006 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2007 # my @display = qw( _items_previous _items_pkg );
2010 # foreach my $display ( @display ) {
2011 # push @b, $self->$display(@_);
2016 =item _items_pkg [ OPTIONS ]
2018 Return line item hashes for each package item on this invoice. Nearly
2021 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2023 The only OPTIONS accepted is 'section', which may point to a hashref
2024 with a key named 'condensed', which may have a true value. If it
2025 does, this method tries to merge identical items into items with
2026 'quantity' equal to the number of items (not the sum of their
2027 separate quantities, for some reason).
2035 warn "$me _items_pkg searching for all package line items\n"
2038 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2040 warn "$me _items_pkg filtering line items\n"
2042 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2044 if ($options{section} && $options{section}->{condensed}) {
2046 warn "$me _items_pkg condensing section\n"
2050 local $Storable::canonical = 1;
2051 foreach ( @items ) {
2053 delete $item->{ref};
2054 delete $item->{ext_description};
2055 my $key = freeze($item);
2056 $itemshash{$key} ||= 0;
2057 $itemshash{$key} ++; # += $item->{quantity};
2059 @items = sort { $a->{description} cmp $b->{description} }
2060 map { my $i = thaw($_);
2061 $i->{quantity} = $itemshash{$_};
2063 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2069 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2076 return 0 unless $a->itemdesc cmp $b->itemdesc;
2077 return -1 if $b->itemdesc eq 'Tax';
2078 return 1 if $a->itemdesc eq 'Tax';
2079 return -1 if $b->itemdesc eq 'Other surcharges';
2080 return 1 if $a->itemdesc eq 'Other surcharges';
2081 $a->itemdesc cmp $b->itemdesc;
2086 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2087 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2090 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2092 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2093 list of hashrefs describing the line items they generate on the invoice.
2095 OPTIONS may include:
2097 format: the invoice format.
2099 escape_function: the function used to escape strings.
2101 DEPRECATED? (expensive, mostly unused?)
2102 format_function: the function used to format CDRs.
2104 section: a hashref containing 'description'; if this is present,
2105 cust_bill_pkg_display records not belonging to this section are
2108 multisection: a flag indicating that this is a multisection invoice,
2109 which does something complicated.
2111 multilocation: a flag to display the location label for the package.
2113 Returns a list of hashrefs, each of which may contain:
2115 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
2116 ext_description, which is an arrayref of detail lines to show below
2121 sub _items_cust_bill_pkg {
2123 my $conf = $self->conf;
2124 my $cust_bill_pkgs = shift;
2127 my $format = $opt{format} || '';
2128 my $escape_function = $opt{escape_function} || sub { shift };
2129 my $format_function = $opt{format_function} || '';
2130 my $no_usage = $opt{no_usage} || '';
2131 my $unsquelched = $opt{unsquelched} || ''; #unused
2132 my $section = $opt{section}->{description} if $opt{section};
2133 my $summary_page = $opt{summary_page} || ''; #unused
2134 my $multilocation = $opt{multilocation} || '';
2135 my $multisection = $opt{multisection} || '';
2136 my $discount_show_always = 0;
2138 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2140 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2143 my ($s, $r, $u) = ( undef, undef, undef );
2144 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2147 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2148 if ( $_ && !$cust_bill_pkg->hidden ) {
2149 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2150 $_->{amount} =~ s/^\-0\.00$/0.00/;
2151 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2153 if $_->{amount} != 0
2154 || $discount_show_always
2155 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2156 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2162 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2163 ? $cust_bill_pkg->cust_bill_pkg_display
2164 : ( $cust_bill_pkg );
2166 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2167 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2170 foreach my $display ( grep { defined($section)
2171 ? $_->section eq $section
2174 #grep { !$_->summary || !$summary_page } # bunk!
2175 grep { !$_->summary || $multisection }
2176 @cust_bill_pkg_display
2180 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2181 $display->billpkgdisplaynum. "\n"
2184 my $type = $display->type;
2186 my $desc = $cust_bill_pkg->desc;
2187 $desc = substr($desc, 0, $maxlength). '...'
2188 if $format eq 'latex' && length($desc) > $maxlength;
2190 my %details_opt = ( 'format' => $format,
2191 'escape_function' => $escape_function,
2192 'format_function' => $format_function,
2193 'no_usage' => $opt{'no_usage'},
2196 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2198 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2201 if ( $cust_bill_pkg->setup != 0 ) {
2202 my $description = $desc;
2203 $description .= ' Setup'
2204 if $cust_bill_pkg->recur != 0
2205 || $discount_show_always
2206 || $cust_bill_pkg->recur_show_zero;
2208 'description' => $description,
2209 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2212 if ( $cust_bill_pkg->recur != 0 ) {
2214 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2215 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2219 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2221 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2224 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2226 # start/end dates for invoice formats that do nonstandard
2228 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2230 if ( (!$type || $type eq 'S')
2231 && ( $cust_bill_pkg->setup != 0
2232 || $cust_bill_pkg->setup_show_zero
2237 warn "$me _items_cust_bill_pkg adding setup\n"
2240 my $description = $desc;
2241 $description .= ' Setup'
2242 if $cust_bill_pkg->recur != 0
2243 || $discount_show_always
2244 || $cust_bill_pkg->recur_show_zero;
2247 unless ( $cust_pkg->part_pkg->hide_svc_detail
2248 || $cust_bill_pkg->hidden )
2251 push @d, map &{$escape_function}($_),
2252 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2253 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2255 if ( $multilocation ) {
2256 my $loc = $cust_pkg->location_label;
2257 $loc = substr($loc, 0, $maxlength). '...'
2258 if $format eq 'latex' && length($loc) > $maxlength;
2259 push @d, &{$escape_function}($loc);
2262 } #unless hiding service details
2264 push @d, $cust_bill_pkg->details(%details_opt)
2265 if $cust_bill_pkg->recur == 0;
2267 if ( $cust_bill_pkg->hidden ) {
2268 $s->{amount} += $cust_bill_pkg->setup;
2269 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2270 push @{ $s->{ext_description} }, @d;
2274 description => $description,
2275 #pkgpart => $part_pkg->pkgpart,
2276 pkgnum => $cust_bill_pkg->pkgnum,
2277 amount => $cust_bill_pkg->setup,
2278 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2279 unit_amount => $cust_bill_pkg->unitsetup,
2280 quantity => $cust_bill_pkg->quantity,
2281 ext_description => \@d,
2287 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2289 $cust_bill_pkg->recur != 0
2290 || $cust_bill_pkg->setup == 0
2291 || $discount_show_always
2292 || $cust_bill_pkg->recur_show_zero
2297 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2300 my $is_summary = $display->summary;
2301 my $description = ($is_summary && $type && $type eq 'U')
2302 ? "Usage charges" : $desc;
2304 #pry be a bit more efficient to look some of this conf stuff up
2307 $conf->exists('disable_line_item_date_ranges')
2308 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2311 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2312 $cust_main->agentnum
2314 if ( defined($date_style) && $date_style eq 'month_of' ) {
2315 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2316 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2317 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2318 $cust_main->agentnum
2320 $desc .= ' ' unless $desc =~ /\s$/;
2321 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2323 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2324 " - ". time2str($date_format, $cust_bill_pkg->edate);
2326 $description .= " ($time_period)";
2330 my @seconds = (); # for display of usage info
2332 #at least until cust_bill_pkg has "past" ranges in addition to
2333 #the "future" sdate/edate ones... see #3032
2334 my @dates = ( $self->_date );
2335 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2336 push @dates, $prev->sdate if $prev;
2337 push @dates, undef if !$prev;
2339 unless ( $cust_pkg->part_pkg->hide_svc_detail
2340 || $cust_bill_pkg->itemdesc
2341 || $cust_bill_pkg->hidden
2342 || $is_summary && $type && $type eq 'U' )
2345 warn "$me _items_cust_bill_pkg adding service details\n"
2348 push @d, map &{$escape_function}($_),
2349 $cust_pkg->h_labels_short(@dates, 'I')
2350 #$cust_bill_pkg->edate,
2351 #$cust_bill_pkg->sdate)
2352 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2354 warn "$me _items_cust_bill_pkg done adding service details\n"
2357 if ( $multilocation ) {
2358 my $loc = $cust_pkg->location_label;
2359 $loc = substr($loc, 0, $maxlength). '...'
2360 if $format eq 'latex' && length($loc) > $maxlength;
2361 push @d, &{$escape_function}($loc);
2364 # Display of seconds_since_sqlradacct:
2365 # On the invoice, when processing @detail_items, look for a field
2366 # named 'seconds'. This will contain total seconds for each
2367 # service, in the same order as @ext_description. For services
2368 # that don't support this it will show undef.
2369 if ( $conf->exists('svc_acct-usage_seconds')
2370 and ! $cust_bill_pkg->pkgpart_override ) {
2371 foreach my $cust_svc (
2372 $cust_pkg->h_cust_svc(@dates, 'I')
2375 # eval because not having any part_export_usage exports
2376 # is a fatal error, last_bill/_date because that's how
2377 # sqlradius_hour billing does it
2379 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2381 push @seconds, $sec;
2383 } #if svc_acct-usage_seconds
2387 unless ( $is_summary ) {
2388 warn "$me _items_cust_bill_pkg adding details\n"
2391 #instead of omitting details entirely in this case (unwanted side
2392 # effects), just omit CDRs
2393 $details_opt{'no_usage'} = 1
2394 if $type && $type eq 'R';
2396 push @d, $cust_bill_pkg->details(%details_opt);
2399 warn "$me _items_cust_bill_pkg calculating amount\n"
2404 $amount = $cust_bill_pkg->recur;
2405 } elsif ($type eq 'R') {
2406 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2407 } elsif ($type eq 'U') {
2408 $amount = $cust_bill_pkg->usage;
2411 if ( !$type || $type eq 'R' ) {
2413 warn "$me _items_cust_bill_pkg adding recur\n"
2416 if ( $cust_bill_pkg->hidden ) {
2417 $r->{amount} += $amount;
2418 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
2419 push @{ $r->{ext_description} }, @d;
2422 description => $description,
2423 #pkgpart => $part_pkg->pkgpart,
2424 pkgnum => $cust_bill_pkg->pkgnum,
2426 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2427 unit_amount => $cust_bill_pkg->unitrecur,
2428 quantity => $cust_bill_pkg->quantity,
2430 ext_description => \@d,
2432 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2435 } else { # $type eq 'U'
2437 warn "$me _items_cust_bill_pkg adding usage\n"
2440 if ( $cust_bill_pkg->hidden ) {
2441 $u->{amount} += $amount;
2442 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
2443 push @{ $u->{ext_description} }, @d;
2446 description => $description,
2447 #pkgpart => $part_pkg->pkgpart,
2448 pkgnum => $cust_bill_pkg->pkgnum,
2450 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2451 unit_amount => $cust_bill_pkg->unitrecur,
2452 quantity => $cust_bill_pkg->quantity,
2454 ext_description => \@d,
2459 } # recurring or usage with recurring charge
2461 } else { #pkgnum tax or one-shot line item (??)
2463 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2466 if ( $cust_bill_pkg->setup != 0 ) {
2468 'description' => $desc,
2469 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2472 if ( $cust_bill_pkg->recur != 0 ) {
2474 'description' => "$desc (".
2475 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2476 time2str($date_format, $cust_bill_pkg->edate). ')',
2477 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2485 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2486 && $conf->exists('discount-show-always'));
2490 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2492 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2493 $_->{amount} =~ s/^\-0\.00$/0.00/;
2494 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2496 if $_->{amount} != 0
2497 || $discount_show_always
2498 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2499 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2503 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2510 =item _items_discounts_avail
2512 Returns an array of line item hashrefs representing available term discounts
2513 for this invoice. This makes the same assumptions that apply to term
2514 discounts in general: that the package is billed monthly, at a flat rate,
2515 with no usage charges. A prorated first month will be handled, as will
2516 a setup fee if the discount is allowed to apply to setup fees.
2520 sub _items_discounts_avail {
2523 #maybe move this method from cust_bill when quotations support discount_plans
2524 return () unless $self->can('discount_plans');
2525 my %plans = $self->discount_plans;
2527 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2528 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2532 my $plan = $plans{$months};
2534 my $term_total = sprintf('%.2f', $plan->discounted_total);
2535 my $percent = sprintf('%.0f',
2536 100 * (1 - $term_total / $plan->base_total) );
2537 my $permonth = sprintf('%.2f', $term_total / $months);
2538 my $detail = $self->mt('discount on item'). ' '.
2539 join(', ', map { "#$_" } $plan->pkgnums)
2542 # discounts for non-integer months don't work anyway
2543 $months = sprintf("%d", $months);
2546 description => $self->mt('Save [_1]% by paying for [_2] months',
2548 amount => $self->mt('[_1] ([_2] per month)',
2549 $term_total, $money_char.$permonth),
2550 ext_description => ($detail || ''),
2553 sort { $b <=> $a } keys %plans;