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'};
945 push @detail_items, $detail;
946 push @buf, ( [ $detail->{'description'},
947 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
949 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
953 if ( $section->{'description'} ) {
954 push @buf, ( ['','-----------'],
955 [ $section->{'description'}. ' sub-total',
956 $section->{'subtotal'} # already formatted this
965 $invoice_data{current_less_finance} =
966 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
968 # create a major section for previous balance if we have major sections,
969 # or if previous_section is in summary form
970 if ( ( $multisection && $self->enable_previous )
971 || $conf->exists('previous_balance-summary_only') )
973 unshift @sections, $previous_section if $pr_total;
976 warn "$me adding taxes\n"
979 foreach my $tax ( $self->_items_tax ) {
981 $taxtotal += $tax->{'amount'};
983 my $description = &$escape_function( $tax->{'description'} );
984 my $amount = sprintf( '%.2f', $tax->{'amount'} );
986 if ( $multisection ) {
988 my $money = $old_latex ? '' : $money_char;
989 push @detail_items, {
990 ext_description => [],
993 description => $description,
994 amount => $money. $amount,
996 section => $tax_section,
1001 push @total_items, {
1002 'total_item' => $description,
1003 'total_amount' => $other_money_char. $amount,
1008 push @buf,[ $description,
1009 $money_char. $amount,
1016 $total->{'total_item'} = $self->mt('Sub-total');
1017 $total->{'total_amount'} =
1018 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1020 if ( $multisection ) {
1021 $tax_section->{'subtotal'} = $other_money_char.
1022 sprintf('%.2f', $taxtotal);
1023 $tax_section->{'pretotal'} = 'New charges sub-total '.
1024 $total->{'total_amount'};
1025 push @sections, $tax_section if $taxtotal;
1027 unshift @total_items, $total;
1030 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1032 push @buf,['','-----------'];
1033 push @buf,[$self->mt(
1034 (!$self->enable_previous)
1036 : 'Total New Charges'
1038 $money_char. sprintf("%10.2f",$self->charged) ];
1041 # calculate total, possibly including total owed on previous
1046 $item = $conf->config('previous_balance-exclude_from_total')
1047 || 'Total New Charges'
1048 if $conf->exists('previous_balance-exclude_from_total');
1049 my $amount = $self->charged;
1050 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1051 $amount += $pr_total;
1054 $total->{'total_item'} = &$embolden_function($self->mt($item));
1055 $total->{'total_amount'} =
1056 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1057 if ( $multisection ) {
1058 if ( $adjust_section->{'sort_weight'} ) {
1059 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1060 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1062 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1063 $other_money_char. sprintf('%.2f', $self->charged );
1066 push @total_items, $total;
1068 push @buf,['','-----------'];
1071 sprintf( '%10.2f', $amount )
1076 # if we're showing previous invoices, also show previous
1077 # credits and payments
1078 if ( $self->enable_previous
1079 and $self->can('_items_credits')
1080 and $self->can('_items_payments') )
1082 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1085 my $credittotal = 0;
1086 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1089 $total->{'total_item'} = &$escape_function($credit->{'description'});
1090 $credittotal += $credit->{'amount'};
1091 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1092 $adjusttotal += $credit->{'amount'};
1093 if ( $multisection ) {
1094 my $money = $old_latex ? '' : $money_char;
1095 push @detail_items, {
1096 ext_description => [],
1099 description => &$escape_function($credit->{'description'}),
1100 amount => $money. $credit->{'amount'},
1102 section => $adjust_section,
1105 push @total_items, $total;
1109 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1112 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1113 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1117 my $paymenttotal = 0;
1118 foreach my $payment ( $self->_items_payments ) {
1120 $total->{'total_item'} = &$escape_function($payment->{'description'});
1121 $paymenttotal += $payment->{'amount'};
1122 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1123 $adjusttotal += $payment->{'amount'};
1124 if ( $multisection ) {
1125 my $money = $old_latex ? '' : $money_char;
1126 push @detail_items, {
1127 ext_description => [],
1130 description => &$escape_function($payment->{'description'}),
1131 amount => $money. $payment->{'amount'},
1133 section => $adjust_section,
1136 push @total_items, $total;
1138 push @buf, [ $payment->{'description'},
1139 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1142 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1144 if ( $multisection ) {
1145 $adjust_section->{'subtotal'} = $other_money_char.
1146 sprintf('%.2f', $adjusttotal);
1147 push @sections, $adjust_section
1148 unless $adjust_section->{sort_weight};
1151 # create Balance Due message
1154 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1155 $total->{'total_amount'} =
1156 &$embolden_function(
1157 $other_money_char. sprintf('%.2f', #why? $summarypage
1158 # ? $self->charged +
1159 # $self->billing_balance
1161 $self->owed + $pr_total
1164 if ( $multisection && !$adjust_section->{sort_weight} ) {
1165 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1166 $total->{'total_amount'};
1168 push @total_items, $total;
1170 push @buf,['','-----------'];
1171 push @buf,[$self->balance_due_msg, $money_char.
1172 sprintf("%10.2f", $balance_due ) ];
1175 if ( $conf->exists('previous_balance-show_credit')
1176 and $cust_main->balance < 0 ) {
1177 my $credit_total = {
1178 'total_item' => &$embolden_function($self->credit_balance_msg),
1179 'total_amount' => &$embolden_function(
1180 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1183 if ( $multisection ) {
1184 $adjust_section->{'posttotal'} .= $newline_token .
1185 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1188 push @total_items, $credit_total;
1190 push @buf,['','-----------'];
1191 push @buf,[$self->credit_balance_msg, $money_char.
1192 sprintf("%10.2f", -$cust_main->balance ) ];
1196 if ( $multisection ) {
1197 if ( $conf->exists('svc_phone_sections')
1198 && $self->can('_items_svc_phone_sections')
1202 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1203 $total->{'total_amount'} =
1204 &$embolden_function(
1205 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1207 my $last_section = pop @sections;
1208 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1209 $total->{'total_amount'};
1210 push @sections, $last_section;
1212 push @sections, @$late_sections
1216 # make a discounts-available section, even without multisection
1217 if ( $conf->exists('discount-show_available')
1218 and my @discounts_avail = $self->_items_discounts_avail ) {
1219 my $discount_section = {
1220 'description' => $self->mt('Discounts Available'),
1225 push @sections, $discount_section;
1226 push @detail_items, map { +{
1227 'ref' => '', #should this be something else?
1228 'section' => $discount_section,
1229 'description' => &$escape_function( $_->{description} ),
1230 'amount' => $money_char . &$escape_function( $_->{amount} ),
1231 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1232 } } @discounts_avail;
1235 # debugging hook: call this with 'diag' => 1 to just get a hash of
1236 # the invoice variables
1237 return \%invoice_data if ( $params{'diag'} );
1239 # All sections and items are built; now fill in templates.
1240 my @includelist = ();
1241 push @includelist, 'summary' if $summarypage;
1242 foreach my $include ( @includelist ) {
1244 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1247 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1249 @inc_src = $conf->config($inc_file, $agentnum);
1253 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1255 my $convert_map = $convert_maps{$format}{$include};
1257 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1258 s/--\@\]/$delimiters{$format}[1]/g;
1261 &$convert_map( $conf->config($inc_file, $agentnum) );
1265 my $inc_tt = new Text::Template (
1267 SOURCE => [ map "$_\n", @inc_src ],
1268 DELIMITERS => $delimiters{$format},
1269 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1271 unless ( $inc_tt->compile() ) {
1272 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1273 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1277 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1279 $invoice_data{$include} =~ s/\n+$//
1280 if ($format eq 'latex');
1285 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1286 /invoice_lines\((\d*)\)/;
1287 $invoice_lines += $1 || scalar(@buf);
1290 die "no invoice_lines() functions in template?"
1291 if ( $format eq 'template' && !$wasfunc );
1293 if ($format eq 'template') {
1295 if ( $invoice_lines ) {
1296 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1297 $invoice_data{'total_pages'}++
1298 if scalar(@buf) % $invoice_lines;
1301 #setup subroutine for the template
1302 $invoice_data{invoice_lines} = sub {
1303 my $lines = shift || scalar(@buf);
1315 push @collect, split("\n",
1316 $text_template->fill_in( HASH => \%invoice_data )
1318 $invoice_data{'page'}++;
1320 map "$_\n", @collect;
1322 } else { # this is where we actually create the invoice
1324 warn "filling in template for invoice ". $self->invnum. "\n"
1326 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1329 $text_template->fill_in(HASH => \%invoice_data);
1333 sub notice_name { '('.shift->table.')'; }
1335 sub template_conf { 'invoice_'; }
1337 # helper routine for generating date ranges
1338 sub _prior_month30s {
1341 [ 1, 2592000 ], # 0-30 days ago
1342 [ 2592000, 5184000 ], # 30-60 days ago
1343 [ 5184000, 7776000 ], # 60-90 days ago
1344 [ 7776000, 0 ], # 90+ days ago
1347 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1348 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1353 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1355 Returns an postscript invoice, as a scalar.
1357 Options can be passed as a hashref (recommended) or as a list of time, template
1358 and then any key/value pairs for any other options.
1360 I<time> an optional value used to control the printing of overdue messages. The
1361 default is now. It isn't the date of the invoice; that's the `_date' field.
1362 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1363 L<Time::Local> and L<Date::Parse> for conversion functions.
1365 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1372 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1373 my $ps = generate_ps($file);
1375 unlink($barcodefile) if $barcodefile;
1380 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1382 Returns an PDF invoice, as a scalar.
1384 Options can be passed as a hashref (recommended) or as a list of time, template
1385 and then any key/value pairs for any other options.
1387 I<time> an optional value used to control the printing of overdue messages. The
1388 default is now. It isn't the date of the invoice; that's the `_date' field.
1389 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1390 L<Time::Local> and L<Date::Parse> for conversion functions.
1392 I<template>, if specified, is the name of a suffix for alternate invoices.
1394 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1401 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1402 my $pdf = generate_pdf($file);
1404 unlink($barcodefile) if $barcodefile;
1409 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1411 Returns an HTML invoice, as a scalar.
1413 I<time> an optional value used to control the printing of overdue messages. The
1414 default is now. It isn't the date of the invoice; that's the `_date' field.
1415 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1416 L<Time::Local> and L<Date::Parse> for conversion functions.
1418 I<template>, if specified, is the name of a suffix for alternate invoices.
1420 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1422 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1423 when emailing the invoice as part of a multipart/related MIME email.
1431 %params = %{ shift() };
1433 $params{'time'} = shift;
1434 $params{'template'} = shift;
1435 $params{'cid'} = shift;
1438 $params{'format'} = 'html';
1440 $self->print_generic( %params );
1443 # quick subroutine for print_latex
1445 # There are ten characters that LaTeX treats as special characters, which
1446 # means that they do not simply typeset themselves:
1447 # # $ % & ~ _ ^ \ { }
1449 # TeX ignores blanks following an escaped character; if you want a blank (as
1450 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1454 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1455 $value =~ s/([<>])/\$$1\$/g;
1461 encode_entities($value);
1465 sub _html_escape_nbsp {
1466 my $value = _html_escape(shift);
1467 $value =~ s/ +/ /g;
1471 #utility methods for print_*
1473 sub _translate_old_latex_format {
1474 warn "_translate_old_latex_format called\n"
1481 if ( $line =~ /^%%Detail\s*$/ ) {
1483 push @template, q![@--!,
1484 q! foreach my $_tr_line (@detail_items) {!,
1485 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1486 q! $_tr_line->{'description'} .= !,
1487 q! "\\tabularnewline\n~~".!,
1488 q! join( "\\tabularnewline\n~~",!,
1489 q! @{$_tr_line->{'ext_description'}}!,
1493 while ( ( my $line_item_line = shift )
1494 !~ /^%%EndDetail\s*$/ ) {
1495 $line_item_line =~ s/'/\\'/g; # nice LTS
1496 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1497 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1498 push @template, " \$OUT .= '$line_item_line';";
1501 push @template, '}',
1504 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1506 push @template, '[@--',
1507 ' foreach my $_tr_line (@total_items) {';
1509 while ( ( my $total_item_line = shift )
1510 !~ /^%%EndTotalDetails\s*$/ ) {
1511 $total_item_line =~ s/'/\\'/g; # nice LTS
1512 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1513 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1514 push @template, " \$OUT .= '$total_item_line';";
1517 push @template, '}',
1521 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1522 push @template, $line;
1528 warn "$_\n" foreach @template;
1536 my $conf = $self->conf;
1538 #check for an invoice-specific override
1539 return $self->invoice_terms if $self->invoice_terms;
1541 #check for a customer- specific override
1542 my $cust_main = $self->cust_main;
1543 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1545 #use configured default
1546 $conf->config('invoice_default_terms') || '';
1552 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1553 $duedate = $self->_date() + ( $1 * 86400 );
1560 $self->due_date ? time2str(shift, $self->due_date) : '';
1563 sub balance_due_msg {
1565 my $msg = $self->mt('Balance Due');
1566 return $msg unless $self->terms;
1567 if ( $self->due_date ) {
1568 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1569 $self->due_date2str($date_format);
1570 } elsif ( $self->terms ) {
1571 $msg .= ' - '. $self->terms;
1576 sub balance_due_date {
1578 my $conf = $self->conf;
1580 if ( $conf->exists('invoice_default_terms')
1581 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1582 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1587 sub credit_balance_msg {
1589 $self->mt('Credit Balance Remaining')
1594 Returns a string with the date, for example: "3/20/2008"
1600 time2str($date_format, $self->_date);
1603 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1605 Generate section information for all items appearing on this invoice.
1606 This will only be called for multi-section invoices.
1608 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1609 related display records (L<FS::cust_bill_pkg_display>) and organize
1610 them into two groups ("early" and "late" according to whether they come
1611 before or after the total), then into sections. A subtotal is calculated
1614 Section descriptions are returned in sort weight order. Each consists
1615 of a hash containing:
1617 description: the package category name, escaped
1618 subtotal: the total charges in that section
1619 tax_section: a flag indicating that the section contains only tax charges
1620 summarized: same as tax_section, for some reason
1621 sort_weight: the package category's sort weight
1623 If 'condense' is set on the display record, it also contains everything
1624 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1625 coderefs to generate parts of the invoice. This is not advised.
1629 LATE: an arrayref to push the "late" section hashes onto. The "early"
1630 group is simply returned from the method.
1632 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1633 Turning this on has the following effects:
1634 - Ignores display items with the 'summary' flag.
1635 - Combines all items into the "early" group.
1636 - Creates sections for all non-disabled package categories, even if they
1637 have no charges on this invoice, as well as a section with no name.
1639 ESCAPE: an escape function to use for section titles.
1641 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1642 sorted list. If there are any of these, section subtotals exclude
1645 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1646 passed through to C<_condense_section()>.
1650 use vars qw(%pkg_category_cache);
1651 sub _items_sections {
1654 my $summarypage = shift;
1656 my $extra_sections = shift;
1660 my %late_subtotal = ();
1663 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1666 my $usage = $cust_bill_pkg->usage;
1668 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1669 next if ( $display->summary && $summarypage );
1671 my $section = $display->section;
1672 my $type = $display->type;
1674 $not_tax{$section} = 1
1675 unless $cust_bill_pkg->pkgnum == 0;
1677 # there's actually a very important piece of logic buried in here:
1678 # incrementing $late_subtotal{$section} CREATES
1679 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1680 # to define the list of late sections, and likewise keys(%subtotal).
1681 # When _items_cust_bill_pkg is called to generate line items for
1682 # real, it will be called with 'section' => $section for each
1684 if ( $display->post_total && !$summarypage ) {
1685 if (! $type || $type eq 'S') {
1686 $late_subtotal{$section} += $cust_bill_pkg->setup
1687 if $cust_bill_pkg->setup != 0
1688 || $cust_bill_pkg->setup_show_zero;
1692 $late_subtotal{$section} += $cust_bill_pkg->recur
1693 if $cust_bill_pkg->recur != 0
1694 || $cust_bill_pkg->recur_show_zero;
1697 if ($type && $type eq 'R') {
1698 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1699 if $cust_bill_pkg->recur != 0
1700 || $cust_bill_pkg->recur_show_zero;
1703 if ($type && $type eq 'U') {
1704 $late_subtotal{$section} += $usage
1705 unless scalar(@$extra_sections);
1710 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1712 if (! $type || $type eq 'S') {
1713 $subtotal{$section} += $cust_bill_pkg->setup
1714 if $cust_bill_pkg->setup != 0
1715 || $cust_bill_pkg->setup_show_zero;
1719 $subtotal{$section} += $cust_bill_pkg->recur
1720 if $cust_bill_pkg->recur != 0
1721 || $cust_bill_pkg->recur_show_zero;
1724 if ($type && $type eq 'R') {
1725 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1726 if $cust_bill_pkg->recur != 0
1727 || $cust_bill_pkg->recur_show_zero;
1730 if ($type && $type eq 'U') {
1731 $subtotal{$section} += $usage
1732 unless scalar(@$extra_sections);
1741 %pkg_category_cache = ();
1743 push @$late, map { { 'description' => &{$escape}($_),
1744 'subtotal' => $late_subtotal{$_},
1746 'sort_weight' => ( _pkg_category($_)
1747 ? _pkg_category($_)->weight
1750 ((_pkg_category($_) && _pkg_category($_)->condense)
1751 ? $self->_condense_section($format)
1755 sort _sectionsort keys %late_subtotal;
1758 if ( $summarypage ) {
1759 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1760 map { $_->categoryname } qsearch('pkg_category', {});
1761 push @sections, '' if exists($subtotal{''});
1763 @sections = keys %subtotal;
1766 my @early = map { { 'description' => &{$escape}($_),
1767 'subtotal' => $subtotal{$_},
1768 'summarized' => $not_tax{$_} ? '' : 'Y',
1769 'tax_section' => $not_tax{$_} ? '' : 'Y',
1770 'sort_weight' => ( _pkg_category($_)
1771 ? _pkg_category($_)->weight
1774 ((_pkg_category($_) && _pkg_category($_)->condense)
1775 ? $self->_condense_section($format)
1780 push @early, @$extra_sections if $extra_sections;
1782 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1786 #helper subs for above
1789 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1793 my $categoryname = shift;
1794 $pkg_category_cache{$categoryname} ||=
1795 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1798 my %condensed_format = (
1799 'label' => [ qw( Description Qty Amount ) ],
1801 sub { shift->{description} },
1802 sub { shift->{quantity} },
1803 sub { my($href, %opt) = @_;
1804 ($opt{dollar} || ''). $href->{amount};
1807 'align' => [ qw( l r r ) ],
1808 'span' => [ qw( 5 1 1 ) ], # unitprices?
1809 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1812 sub _condense_section {
1813 my ( $self, $format ) = ( shift, shift );
1815 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1816 qw( description_generator
1819 total_line_generator
1824 sub _condensed_generator_defaults {
1825 my ( $self, $format ) = ( shift, shift );
1826 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1835 sub _condensed_header_generator {
1836 my ( $self, $format ) = ( shift, shift );
1838 my ( $f, $prefix, $suffix, $separator, $column ) =
1839 _condensed_generator_defaults($format);
1841 if ($format eq 'latex') {
1842 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1843 $suffix = "\\\\\n\\hline";
1846 sub { my ($d,$a,$s,$w) = @_;
1847 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1849 } elsif ( $format eq 'html' ) {
1850 $prefix = '<th></th>';
1854 sub { my ($d,$a,$s,$w) = @_;
1855 return qq!<th align="$html_align{$a}">$d</th>!;
1863 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1865 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1868 $prefix. join($separator, @result). $suffix;
1873 sub _condensed_description_generator {
1874 my ( $self, $format ) = ( shift, shift );
1876 my ( $f, $prefix, $suffix, $separator, $column ) =
1877 _condensed_generator_defaults($format);
1879 my $money_char = '$';
1880 if ($format eq 'latex') {
1881 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1883 $separator = " & \n";
1885 sub { my ($d,$a,$s,$w) = @_;
1886 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1888 $money_char = '\\dollar';
1889 }elsif ( $format eq 'html' ) {
1890 $prefix = '"><td align="center"></td>';
1894 sub { my ($d,$a,$s,$w) = @_;
1895 return qq!<td align="$html_align{$a}">$d</td>!;
1897 #$money_char = $conf->config('money_char') || '$';
1898 $money_char = ''; # this is madness
1906 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1908 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1910 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1911 map { $f->{$_}->[$i] } qw(align span width)
1915 $prefix. join( $separator, @result ). $suffix;
1920 sub _condensed_total_generator {
1921 my ( $self, $format ) = ( shift, shift );
1923 my ( $f, $prefix, $suffix, $separator, $column ) =
1924 _condensed_generator_defaults($format);
1927 if ($format eq 'latex') {
1930 $separator = " & \n";
1932 sub { my ($d,$a,$s,$w) = @_;
1933 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1935 }elsif ( $format eq 'html' ) {
1939 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1941 sub { my ($d,$a,$s,$w) = @_;
1942 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1951 # my $r = &{$f->{fields}->[$i]}(@args);
1952 # $r .= ' Total' unless $i;
1954 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1956 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1957 map { $f->{$_}->[$i] } qw(align span width)
1961 $prefix. join( $separator, @result ). $suffix;
1966 =item total_line_generator FORMAT
1968 Returns a coderef used for generation of invoice total line items for this
1969 usage_class. FORMAT is either html or latex
1973 # should not be used: will have issues with hash element names (description vs
1974 # total_item and amount vs total_amount -- another array of functions?
1976 sub _condensed_total_line_generator {
1977 my ( $self, $format ) = ( shift, shift );
1979 my ( $f, $prefix, $suffix, $separator, $column ) =
1980 _condensed_generator_defaults($format);
1983 if ($format eq 'latex') {
1986 $separator = " & \n";
1988 sub { my ($d,$a,$s,$w) = @_;
1989 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1991 }elsif ( $format eq 'html' ) {
1995 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1997 sub { my ($d,$a,$s,$w) = @_;
1998 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2007 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2009 &{$column}( &{$f->{fields}->[$i]}(@args),
2010 map { $f->{$_}->[$i] } qw(align span width)
2014 $prefix. join( $separator, @result ). $suffix;
2019 # sub _items { # seems to be unused
2022 # #my @display = scalar(@_)
2024 # # : qw( _items_previous _items_pkg );
2025 # # #: qw( _items_pkg );
2026 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2027 # my @display = qw( _items_previous _items_pkg );
2030 # foreach my $display ( @display ) {
2031 # push @b, $self->$display(@_);
2036 =item _items_pkg [ OPTIONS ]
2038 Return line item hashes for each package item on this invoice. Nearly
2041 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2043 The only OPTIONS accepted is 'section', which may point to a hashref
2044 with a key named 'condensed', which may have a true value. If it
2045 does, this method tries to merge identical items into items with
2046 'quantity' equal to the number of items (not the sum of their
2047 separate quantities, for some reason).
2055 warn "$me _items_pkg searching for all package line items\n"
2058 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2060 warn "$me _items_pkg filtering line items\n"
2062 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2064 if ($options{section} && $options{section}->{condensed}) {
2066 warn "$me _items_pkg condensing section\n"
2070 local $Storable::canonical = 1;
2071 foreach ( @items ) {
2073 delete $item->{ref};
2074 delete $item->{ext_description};
2075 my $key = freeze($item);
2076 $itemshash{$key} ||= 0;
2077 $itemshash{$key} ++; # += $item->{quantity};
2079 @items = sort { $a->{description} cmp $b->{description} }
2080 map { my $i = thaw($_);
2081 $i->{quantity} = $itemshash{$_};
2083 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2089 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2096 return 0 unless $a->itemdesc cmp $b->itemdesc;
2097 return -1 if $b->itemdesc eq 'Tax';
2098 return 1 if $a->itemdesc eq 'Tax';
2099 return -1 if $b->itemdesc eq 'Other surcharges';
2100 return 1 if $a->itemdesc eq 'Other surcharges';
2101 $a->itemdesc cmp $b->itemdesc;
2106 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2107 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2110 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2112 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2113 list of hashrefs describing the line items they generate on the invoice.
2115 OPTIONS may include:
2117 format: the invoice format.
2119 escape_function: the function used to escape strings.
2121 DEPRECATED? (expensive, mostly unused?)
2122 format_function: the function used to format CDRs.
2124 section: a hashref containing 'description'; if this is present,
2125 cust_bill_pkg_display records not belonging to this section are
2128 multisection: a flag indicating that this is a multisection invoice,
2129 which does something complicated.
2131 Returns a list of hashrefs, each of which may contain:
2133 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2134 ext_description, which is an arrayref of detail lines to show below
2139 sub _items_cust_bill_pkg {
2141 my $conf = $self->conf;
2142 my $cust_bill_pkgs = shift;
2145 my $format = $opt{format} || '';
2146 my $escape_function = $opt{escape_function} || sub { shift };
2147 my $format_function = $opt{format_function} || '';
2148 my $no_usage = $opt{no_usage} || '';
2149 my $unsquelched = $opt{unsquelched} || ''; #unused
2150 my $section = $opt{section}->{description} if $opt{section};
2151 my $summary_page = $opt{summary_page} || ''; #unused
2152 my $multisection = $opt{multisection} || '';
2153 my $discount_show_always = 0;
2155 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2157 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2158 # and location labels
2161 my ($s, $r, $u) = ( undef, undef, undef );
2162 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2165 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2166 if ( $_ && !$cust_bill_pkg->hidden ) {
2167 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2168 $_->{amount} =~ s/^\-0\.00$/0.00/;
2169 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2171 if $_->{amount} != 0
2172 || $discount_show_always
2173 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2174 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2180 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2181 ? $cust_bill_pkg->cust_bill_pkg_display
2182 : ( $cust_bill_pkg );
2184 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2185 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2188 foreach my $display ( grep { defined($section)
2189 ? $_->section eq $section
2192 grep { !$_->summary || $multisection }
2193 @cust_bill_pkg_display
2197 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2198 $display->billpkgdisplaynum. "\n"
2201 my $type = $display->type;
2203 my $desc = $cust_bill_pkg->desc;
2204 $desc = substr($desc, 0, $maxlength). '...'
2205 if $format eq 'latex' && length($desc) > $maxlength;
2207 my %details_opt = ( 'format' => $format,
2208 'escape_function' => $escape_function,
2209 'format_function' => $format_function,
2210 'no_usage' => $opt{'no_usage'},
2213 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2215 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2218 if ( $cust_bill_pkg->setup != 0 ) {
2219 my $description = $desc;
2220 $description .= ' Setup'
2221 if $cust_bill_pkg->recur != 0
2222 || $discount_show_always
2223 || $cust_bill_pkg->recur_show_zero;
2225 'description' => $description,
2226 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2229 if ( $cust_bill_pkg->recur != 0 ) {
2231 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2232 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2236 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2238 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2241 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2243 # which pkgpart to show for display purposes?
2244 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2246 # start/end dates for invoice formats that do nonstandard
2248 my %item_dates = ();
2249 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2250 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2252 if ( (!$type || $type eq 'S')
2253 && ( $cust_bill_pkg->setup != 0
2254 || $cust_bill_pkg->setup_show_zero
2259 warn "$me _items_cust_bill_pkg adding setup\n"
2262 my $description = $desc;
2263 $description .= ' Setup'
2264 if $cust_bill_pkg->recur != 0
2265 || $discount_show_always
2266 || $cust_bill_pkg->recur_show_zero;
2269 unless ( $cust_pkg->part_pkg->hide_svc_detail
2270 || $cust_bill_pkg->hidden )
2273 push @d, map &{$escape_function}($_),
2274 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2275 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2277 if ( ! $cust_pkg->locationnum or
2278 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2279 my $loc = $cust_pkg->location_label;
2280 $loc = substr($loc, 0, $maxlength). '...'
2281 if $format eq 'latex' && length($loc) > $maxlength;
2282 push @d, &{$escape_function}($loc);
2285 } #unless hiding service details
2287 push @d, $cust_bill_pkg->details(%details_opt)
2288 if $cust_bill_pkg->recur == 0;
2290 if ( $cust_bill_pkg->hidden ) {
2291 $s->{amount} += $cust_bill_pkg->setup;
2292 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2293 push @{ $s->{ext_description} }, @d;
2297 description => $description,
2298 pkgpart => $pkgpart,
2299 pkgnum => $cust_bill_pkg->pkgnum,
2300 amount => $cust_bill_pkg->setup,
2301 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2302 unit_amount => $cust_bill_pkg->unitsetup,
2303 quantity => $cust_bill_pkg->quantity,
2304 ext_description => \@d,
2310 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2312 $cust_bill_pkg->recur != 0
2313 || $cust_bill_pkg->setup == 0
2314 || $discount_show_always
2315 || $cust_bill_pkg->recur_show_zero
2320 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2323 my $is_summary = $display->summary;
2324 my $description = ($is_summary && $type && $type eq 'U')
2325 ? "Usage charges" : $desc;
2327 my $part_pkg = $cust_pkg->part_pkg;
2329 #pry be a bit more efficient to look some of this conf stuff up
2332 $conf->exists('disable_line_item_date_ranges')
2333 || $part_pkg->option('disable_line_item_date_ranges',1)
2334 || ! $cust_bill_pkg->sdate
2335 || ! $cust_bill_pkg->edate
2338 my $date_style = '';
2339 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2340 $cust_main->agentnum
2342 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2343 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2344 $cust_main->agentnum
2346 if ( defined($date_style) && $date_style eq 'month_of' ) {
2347 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2348 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2349 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2350 $cust_main->agentnum
2352 $desc .= ' ' unless $desc =~ /\s$/;
2353 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2355 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2356 " - ". time2str($date_format, $cust_bill_pkg->edate);
2358 $description .= " ($time_period)";
2362 my @seconds = (); # for display of usage info
2364 #at least until cust_bill_pkg has "past" ranges in addition to
2365 #the "future" sdate/edate ones... see #3032
2366 my @dates = ( $self->_date );
2367 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2368 push @dates, $prev->sdate if $prev;
2369 push @dates, undef if !$prev;
2371 unless ( $part_pkg->hide_svc_detail
2372 || $cust_bill_pkg->itemdesc
2373 || $cust_bill_pkg->hidden
2374 || $is_summary && $type && $type eq 'U'
2378 warn "$me _items_cust_bill_pkg adding service details\n"
2381 push @d, map &{$escape_function}($_),
2382 $cust_pkg->h_labels_short(@dates, 'I')
2383 #$cust_bill_pkg->edate,
2384 #$cust_bill_pkg->sdate)
2385 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2387 warn "$me _items_cust_bill_pkg done adding service details\n"
2390 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2391 my $loc = $cust_pkg->location_label;
2392 $loc = substr($loc, 0, $maxlength). '...'
2393 if $format eq 'latex' && length($loc) > $maxlength;
2394 push @d, &{$escape_function}($loc);
2397 # Display of seconds_since_sqlradacct:
2398 # On the invoice, when processing @detail_items, look for a field
2399 # named 'seconds'. This will contain total seconds for each
2400 # service, in the same order as @ext_description. For services
2401 # that don't support this it will show undef.
2402 if ( $conf->exists('svc_acct-usage_seconds')
2403 and ! $cust_bill_pkg->pkgpart_override ) {
2404 foreach my $cust_svc (
2405 $cust_pkg->h_cust_svc(@dates, 'I')
2408 # eval because not having any part_export_usage exports
2409 # is a fatal error, last_bill/_date because that's how
2410 # sqlradius_hour billing does it
2412 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2414 push @seconds, $sec;
2416 } #if svc_acct-usage_seconds
2420 unless ( $is_summary ) {
2421 warn "$me _items_cust_bill_pkg adding details\n"
2424 #instead of omitting details entirely in this case (unwanted side
2425 # effects), just omit CDRs
2426 $details_opt{'no_usage'} = 1
2427 if $type && $type eq 'R';
2429 push @d, $cust_bill_pkg->details(%details_opt);
2432 warn "$me _items_cust_bill_pkg calculating amount\n"
2437 $amount = $cust_bill_pkg->recur;
2438 } elsif ($type eq 'R') {
2439 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2440 } elsif ($type eq 'U') {
2441 $amount = $cust_bill_pkg->usage;
2445 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2448 if ( !$type || $type eq 'R' ) {
2450 warn "$me _items_cust_bill_pkg adding recur\n"
2453 if ( $cust_bill_pkg->hidden ) {
2454 $r->{amount} += $amount;
2455 $r->{unit_amount} += $unit_amount;
2456 push @{ $r->{ext_description} }, @d;
2459 description => $description,
2460 pkgpart => $pkgpart,
2461 pkgnum => $cust_bill_pkg->pkgnum,
2463 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2464 unit_amount => $unit_amount,
2465 quantity => $cust_bill_pkg->quantity,
2467 ext_description => \@d,
2469 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2472 } else { # $type eq 'U'
2474 warn "$me _items_cust_bill_pkg adding usage\n"
2477 if ( $cust_bill_pkg->hidden ) {
2478 $u->{amount} += $amount;
2479 $u->{unit_amount} += $unit_amount,
2480 push @{ $u->{ext_description} }, @d;
2483 description => $description,
2484 pkgpart => $pkgpart,
2485 pkgnum => $cust_bill_pkg->pkgnum,
2487 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2488 unit_amount => $unit_amount,
2489 quantity => $cust_bill_pkg->quantity,
2491 ext_description => \@d,
2496 } # recurring or usage with recurring charge
2498 } else { #pkgnum tax or one-shot line item (??)
2500 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2503 if ( $cust_bill_pkg->setup != 0 ) {
2505 'description' => $desc,
2506 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2509 if ( $cust_bill_pkg->recur != 0 ) {
2511 'description' => "$desc (".
2512 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2513 time2str($date_format, $cust_bill_pkg->edate). ')',
2514 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2522 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2523 && $conf->exists('discount-show-always'));
2527 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2529 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2530 $_->{amount} =~ s/^\-0\.00$/0.00/;
2531 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2533 if $_->{amount} != 0
2534 || $discount_show_always
2535 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2536 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2540 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2547 =item _items_discounts_avail
2549 Returns an array of line item hashrefs representing available term discounts
2550 for this invoice. This makes the same assumptions that apply to term
2551 discounts in general: that the package is billed monthly, at a flat rate,
2552 with no usage charges. A prorated first month will be handled, as will
2553 a setup fee if the discount is allowed to apply to setup fees.
2557 sub _items_discounts_avail {
2560 #maybe move this method from cust_bill when quotations support discount_plans
2561 return () unless $self->can('discount_plans');
2562 my %plans = $self->discount_plans;
2564 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2565 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2569 my $plan = $plans{$months};
2571 my $term_total = sprintf('%.2f', $plan->discounted_total);
2572 my $percent = sprintf('%.0f',
2573 100 * (1 - $term_total / $plan->base_total) );
2574 my $permonth = sprintf('%.2f', $term_total / $months);
2575 my $detail = $self->mt('discount on item'). ' '.
2576 join(', ', map { "#$_" } $plan->pkgnums)
2579 # discounts for non-integer months don't work anyway
2580 $months = sprintf("%d", $months);
2583 description => $self->mt('Save [_1]% by paying for [_2] months',
2585 amount => $self->mt('[_1] ([_2] per month)',
2586 $term_total, $money_char.$permonth),
2587 ext_description => ($detail || ''),
2590 sort { $b <=> $a } keys %plans;