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).
2056 warn "$me _items_pkg searching for all package line items\n"
2059 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2061 warn "$me _items_pkg filtering line items\n"
2063 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2065 if ($options{section} && $options{section}->{condensed}) {
2067 warn "$me _items_pkg condensing section\n"
2071 local $Storable::canonical = 1;
2072 foreach ( @items ) {
2074 delete $item->{ref};
2075 delete $item->{ext_description};
2076 my $key = freeze($item);
2077 $itemshash{$key} ||= 0;
2078 $itemshash{$key} ++; # += $item->{quantity};
2080 @items = sort { $a->{description} cmp $b->{description} }
2081 map { my $i = thaw($_);
2082 $i->{quantity} = $itemshash{$_};
2084 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2090 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2097 return 0 unless $a->itemdesc cmp $b->itemdesc;
2098 return -1 if $b->itemdesc eq 'Tax';
2099 return 1 if $a->itemdesc eq 'Tax';
2100 return -1 if $b->itemdesc eq 'Other surcharges';
2101 return 1 if $a->itemdesc eq 'Other surcharges';
2102 $a->itemdesc cmp $b->itemdesc;
2107 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2108 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2111 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2113 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2114 list of hashrefs describing the line items they generate on the invoice.
2116 OPTIONS may include:
2118 format: the invoice format.
2120 escape_function: the function used to escape strings.
2122 DEPRECATED? (expensive, mostly unused?)
2123 format_function: the function used to format CDRs.
2125 section: a hashref containing 'description'; if this is present,
2126 cust_bill_pkg_display records not belonging to this section are
2129 multisection: a flag indicating that this is a multisection invoice,
2130 which does something complicated.
2132 Returns a list of hashrefs, each of which may contain:
2134 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2135 ext_description, which is an arrayref of detail lines to show below
2140 sub _items_cust_bill_pkg {
2142 my $conf = $self->conf;
2143 my $cust_bill_pkgs = shift;
2146 my $format = $opt{format} || '';
2147 my $escape_function = $opt{escape_function} || sub { shift };
2148 my $format_function = $opt{format_function} || '';
2149 my $no_usage = $opt{no_usage} || '';
2150 my $unsquelched = $opt{unsquelched} || ''; #unused
2151 my $section = $opt{section}->{description} if $opt{section};
2152 my $summary_page = $opt{summary_page} || ''; #unused
2153 my $multisection = $opt{multisection} || '';
2154 my $discount_show_always = 0;
2156 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2158 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2159 # and location labels
2162 my ($s, $r, $u) = ( undef, undef, undef );
2163 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2166 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2167 if ( $_ && !$cust_bill_pkg->hidden ) {
2168 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2169 $_->{amount} =~ s/^\-0\.00$/0.00/;
2170 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2172 if $_->{amount} != 0
2173 || $discount_show_always
2174 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2175 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2181 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2182 ? $cust_bill_pkg->cust_bill_pkg_display
2183 : ( $cust_bill_pkg );
2185 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2186 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2189 foreach my $display ( grep { defined($section)
2190 ? $_->section eq $section
2193 grep { !$_->summary || $multisection }
2194 @cust_bill_pkg_display
2198 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2199 $display->billpkgdisplaynum. "\n"
2202 my $type = $display->type;
2204 my $desc = $cust_bill_pkg->desc;
2205 $desc = substr($desc, 0, $maxlength). '...'
2206 if $format eq 'latex' && length($desc) > $maxlength;
2208 my %details_opt = ( 'format' => $format,
2209 'escape_function' => $escape_function,
2210 'format_function' => $format_function,
2211 'no_usage' => $opt{'no_usage'},
2214 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2216 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2219 if ( $cust_bill_pkg->setup != 0 ) {
2220 my $description = $desc;
2221 $description .= ' Setup'
2222 if $cust_bill_pkg->recur != 0
2223 || $discount_show_always
2224 || $cust_bill_pkg->recur_show_zero;
2226 'description' => $description,
2227 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2230 if ( $cust_bill_pkg->recur != 0 ) {
2232 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2233 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2237 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2239 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2242 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2244 # which pkgpart to show for display purposes?
2245 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2247 # start/end dates for invoice formats that do nonstandard
2249 my %item_dates = ();
2250 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2251 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2253 if ( (!$type || $type eq 'S')
2254 && ( $cust_bill_pkg->setup != 0
2255 || $cust_bill_pkg->setup_show_zero
2260 warn "$me _items_cust_bill_pkg adding setup\n"
2263 my $description = $desc;
2264 $description .= ' Setup'
2265 if $cust_bill_pkg->recur != 0
2266 || $discount_show_always
2267 || $cust_bill_pkg->recur_show_zero;
2271 unless ( $cust_pkg->part_pkg->hide_svc_detail
2272 || $cust_bill_pkg->hidden )
2275 my @svc_labels = map &{$escape_function}($_),
2276 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2277 push @d, @svc_labels
2278 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2279 $svc_label = $svc_labels[0];
2281 if ( ! $cust_pkg->locationnum or
2282 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2283 my $loc = $cust_pkg->location_label;
2284 $loc = substr($loc, 0, $maxlength). '...'
2285 if $format eq 'latex' && length($loc) > $maxlength;
2286 push @d, &{$escape_function}($loc);
2289 } #unless hiding service details
2291 push @d, $cust_bill_pkg->details(%details_opt)
2292 if $cust_bill_pkg->recur == 0;
2294 if ( $cust_bill_pkg->hidden ) {
2295 $s->{amount} += $cust_bill_pkg->setup;
2296 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2297 push @{ $s->{ext_description} }, @d;
2301 description => $description,
2302 pkgpart => $pkgpart,
2303 pkgnum => $cust_bill_pkg->pkgnum,
2304 amount => $cust_bill_pkg->setup,
2305 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2306 unit_amount => $cust_bill_pkg->unitsetup,
2307 quantity => $cust_bill_pkg->quantity,
2308 ext_description => \@d,
2309 svc_label => ($svc_label || ''),
2315 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2317 $cust_bill_pkg->recur != 0
2318 || $cust_bill_pkg->setup == 0
2319 || $discount_show_always
2320 || $cust_bill_pkg->recur_show_zero
2325 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2328 my $is_summary = $display->summary;
2329 my $description = ($is_summary && $type && $type eq 'U')
2330 ? "Usage charges" : $desc;
2332 my $part_pkg = $cust_pkg->part_pkg;
2334 #pry be a bit more efficient to look some of this conf stuff up
2337 $conf->exists('disable_line_item_date_ranges')
2338 || $part_pkg->option('disable_line_item_date_ranges',1)
2339 || ! $cust_bill_pkg->sdate
2340 || ! $cust_bill_pkg->edate
2343 my $date_style = '';
2344 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2345 $cust_main->agentnum
2347 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2348 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2349 $cust_main->agentnum
2351 if ( defined($date_style) && $date_style eq 'month_of' ) {
2352 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2353 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2354 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2355 $cust_main->agentnum
2357 $desc .= ' ' unless $desc =~ /\s$/;
2358 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2360 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2361 " - ". time2str($date_format, $cust_bill_pkg->edate);
2363 $description .= " ($time_period)";
2367 my @seconds = (); # for display of usage info
2370 #at least until cust_bill_pkg has "past" ranges in addition to
2371 #the "future" sdate/edate ones... see #3032
2372 my @dates = ( $self->_date );
2373 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2374 push @dates, $prev->sdate if $prev;
2375 push @dates, undef if !$prev;
2377 unless ( $part_pkg->hide_svc_detail
2378 || $cust_bill_pkg->itemdesc
2379 || $cust_bill_pkg->hidden
2380 || $is_summary && $type && $type eq 'U'
2384 warn "$me _items_cust_bill_pkg adding service details\n"
2387 my @svc_labels = map &{$escape_function}($_),
2388 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2389 push @d, @svc_labels
2390 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2391 $svc_label = $svc_labels[0];
2393 warn "$me _items_cust_bill_pkg done adding service details\n"
2396 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2397 my $loc = $cust_pkg->location_label;
2398 $loc = substr($loc, 0, $maxlength). '...'
2399 if $format eq 'latex' && length($loc) > $maxlength;
2400 push @d, &{$escape_function}($loc);
2403 # Display of seconds_since_sqlradacct:
2404 # On the invoice, when processing @detail_items, look for a field
2405 # named 'seconds'. This will contain total seconds for each
2406 # service, in the same order as @ext_description. For services
2407 # that don't support this it will show undef.
2408 if ( $conf->exists('svc_acct-usage_seconds')
2409 and ! $cust_bill_pkg->pkgpart_override ) {
2410 foreach my $cust_svc (
2411 $cust_pkg->h_cust_svc(@dates, 'I')
2414 # eval because not having any part_export_usage exports
2415 # is a fatal error, last_bill/_date because that's how
2416 # sqlradius_hour billing does it
2418 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2420 push @seconds, $sec;
2422 } #if svc_acct-usage_seconds
2426 unless ( $is_summary ) {
2427 warn "$me _items_cust_bill_pkg adding details\n"
2430 #instead of omitting details entirely in this case (unwanted side
2431 # effects), just omit CDRs
2432 $details_opt{'no_usage'} = 1
2433 if $type && $type eq 'R';
2435 push @d, $cust_bill_pkg->details(%details_opt);
2438 warn "$me _items_cust_bill_pkg calculating amount\n"
2443 $amount = $cust_bill_pkg->recur;
2444 } elsif ($type eq 'R') {
2445 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2446 } elsif ($type eq 'U') {
2447 $amount = $cust_bill_pkg->usage;
2451 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2454 if ( !$type || $type eq 'R' ) {
2456 warn "$me _items_cust_bill_pkg adding recur\n"
2459 if ( $cust_bill_pkg->hidden ) {
2460 $r->{amount} += $amount;
2461 $r->{unit_amount} += $unit_amount;
2462 push @{ $r->{ext_description} }, @d;
2465 description => $description,
2466 pkgpart => $pkgpart,
2467 pkgnum => $cust_bill_pkg->pkgnum,
2469 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2470 unit_amount => $unit_amount,
2471 quantity => $cust_bill_pkg->quantity,
2473 ext_description => \@d,
2474 svc_label => ($svc_label || ''),
2476 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2479 } else { # $type eq 'U'
2481 warn "$me _items_cust_bill_pkg adding usage\n"
2484 if ( $cust_bill_pkg->hidden ) {
2485 $u->{amount} += $amount;
2486 $u->{unit_amount} += $unit_amount,
2487 push @{ $u->{ext_description} }, @d;
2490 description => $description,
2491 pkgpart => $pkgpart,
2492 pkgnum => $cust_bill_pkg->pkgnum,
2494 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2495 unit_amount => $unit_amount,
2496 quantity => $cust_bill_pkg->quantity,
2498 ext_description => \@d,
2503 } # recurring or usage with recurring charge
2505 } else { #pkgnum tax or one-shot line item (??)
2507 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2510 if ( $cust_bill_pkg->setup != 0 ) {
2512 'description' => $desc,
2513 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2516 if ( $cust_bill_pkg->recur != 0 ) {
2518 'description' => "$desc (".
2519 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2520 time2str($date_format, $cust_bill_pkg->edate). ')',
2521 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2529 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2530 && $conf->exists('discount-show-always'));
2534 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2536 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2537 $_->{amount} =~ s/^\-0\.00$/0.00/;
2538 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2540 if $_->{amount} != 0
2541 || $discount_show_always
2542 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2543 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2547 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2554 =item _items_discounts_avail
2556 Returns an array of line item hashrefs representing available term discounts
2557 for this invoice. This makes the same assumptions that apply to term
2558 discounts in general: that the package is billed monthly, at a flat rate,
2559 with no usage charges. A prorated first month will be handled, as will
2560 a setup fee if the discount is allowed to apply to setup fees.
2564 sub _items_discounts_avail {
2567 #maybe move this method from cust_bill when quotations support discount_plans
2568 return () unless $self->can('discount_plans');
2569 my %plans = $self->discount_plans;
2571 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2572 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2576 my $plan = $plans{$months};
2578 my $term_total = sprintf('%.2f', $plan->discounted_total);
2579 my $percent = sprintf('%.0f',
2580 100 * (1 - $term_total / $plan->base_total) );
2581 my $permonth = sprintf('%.2f', $term_total / $months);
2582 my $detail = $self->mt('discount on item'). ' '.
2583 join(', ', map { "#$_" } $plan->pkgnums)
2586 # discounts for non-integer months don't work anyway
2587 $months = sprintf("%d", $months);
2590 description => $self->mt('Save [_1]% by paying for [_2] months',
2592 amount => $self->mt('[_1] ([_2] per month)',
2593 $term_total, $money_char.$permonth),
2594 ext_description => ($detail || ''),
2597 sort { $b <=> $a } keys %plans;