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 $cust_main = $self->cust_main;
126 my $prospect_main = $self->prospect_main;
127 my $agentnum = $cust_main ? $cust_main->agentnum : $prospect_main->agentnum;
129 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
130 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
131 or die "can't write temp file: $!\n";
133 print $lh $conf->config_binary('logo.eps', $agentnum)
134 or die "can't write temp file: $!\n";
137 $params{'logo_file'} = $lh->filename;
139 if( $conf->exists('invoice-barcode')
140 && $self->can('invoice_barcode')
141 && $self->invnum ) { # don't try to barcode statements
142 my $png_file = $self->invoice_barcode($dir);
143 my $eps_file = $png_file;
144 $eps_file =~ s/\.png$/.eps/g;
145 $png_file =~ /(barcode.*png)/;
147 $eps_file =~ /(barcode.*eps)/;
150 my $curr_dir = cwd();
152 # after painfuly long experimentation, it was determined that sam2p won't
153 # accept : and other chars in the path, no matter how hard I tried to
154 # escape them, hence the chdir (and chdir back, just to be safe)
155 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
156 or die "sam2p failed: $!\n";
160 $params{'barcode_file'} = $eps_file;
163 my @filled_in = $self->print_generic( %params );
165 my $fh = new File::Temp( TEMPLATE => $tmp_template,
169 ) or die "can't open temp file: $!\n";
170 binmode($fh, ':utf8'); # language support
171 print $fh join('', @filled_in );
174 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
175 return ($1, $params{'logo_file'}, $params{'barcode_file'});
179 =item print_generic OPTION => VALUE ...
181 Internal method - returns a filled-in template for this invoice as a scalar.
183 See print_ps and print_pdf for methods that return PostScript and PDF output.
185 Non optional options include
186 format - latex, html, template
188 Optional options include
190 template - a value used as a suffix for a configuration template
192 time - a value used to control the printing of overdue messages. The
193 default is now. It isn't the date of the invoice; that's the `_date' field.
194 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
195 L<Time::Local> and L<Date::Parse> for conversion functions.
199 unsquelch_cdr - overrides any per customer cdr squelching when true
201 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
203 locale - override customer's locale
207 #what's with all the sprintf('%10.2f')'s in here? will it cause any
208 # (alignment in text invoice?) problems to change them all to '%.2f' ?
209 # yes: fixed width/plain text printing will be borked
211 my( $self, %params ) = @_;
212 my $conf = $self->conf;
213 my $today = $params{today} ? $params{today} : time;
214 warn "$me print_generic called on $self with suffix $params{template}\n"
217 my $format = $params{format};
218 die "Unknown format: $format"
219 unless $format =~ /^(latex|html|template)$/;
221 my $cust_main = $self->cust_main || $self->prospect_main;
222 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
223 unless $cust_main->payname
224 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
226 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
227 'html' => [ '<%=', '%>' ],
228 'template' => [ '{', '}' ],
231 warn "$me print_generic creating template\n"
235 my $template = $params{template} ? $params{template} : $self->_agent_template;
236 my $templatefile = $self->template_conf. $format;
237 $templatefile .= "_$template"
238 if length($template) && $conf->exists($templatefile."_$template");
239 my @invoice_template = map "$_\n", $conf->config($templatefile)
240 or die "cannot load config data $templatefile";
243 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
244 #change this to a die when the old code is removed
245 warn "old-style invoice template $templatefile; ".
246 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
248 @invoice_template = _translate_old_latex_format(@invoice_template);
251 warn "$me print_generic creating T:T object\n"
254 my $text_template = new Text::Template(
256 SOURCE => \@invoice_template,
257 DELIMITERS => $delimiters{$format},
260 warn "$me print_generic compiling T:T object\n"
263 $text_template->compile()
264 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
267 # additional substitution could possibly cause breakage in existing templates
270 'notes' => sub { map "$_", @_ },
271 'footer' => sub { map "$_", @_ },
272 'smallfooter' => sub { map "$_", @_ },
273 'returnaddress' => sub { map "$_", @_ },
274 'coupon' => sub { map "$_", @_ },
275 'summary' => sub { map "$_", @_ },
281 s/%%(.*)$/<!-- $1 -->/g;
282 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
283 s/\\begin\{enumerate\}/<ol>/g;
285 s/\\end\{enumerate\}/<\/ol>/g;
286 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
295 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
297 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
303 s/\\hyphenation\{[\w\s\-]+}//;
308 'coupon' => sub { "" },
309 'summary' => sub { "" },
316 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
317 s/\\begin\{enumerate\}//g;
319 s/\\end\{enumerate\}//g;
320 s/\\textbf\{(.*)\}/$1/g;
327 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
329 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
334 s/\\\\\*?\s*$/\n/; # dubious
335 s/\\hyphenation\{[\w\s\-]+}//;
339 'coupon' => sub { "" },
340 'summary' => sub { "" },
345 # hashes for differing output formats
346 my %nbsps = ( 'latex' => '~',
347 'html' => '', # '&nbps;' would be nice
348 'template' => '', # not used
350 my $nbsp = $nbsps{$format};
352 my %escape_functions = ( 'latex' => \&_latex_escape,
353 'html' => \&_html_escape_nbsp,#\&encode_entities,
354 'template' => sub { shift },
356 my $escape_function = $escape_functions{$format};
357 my $escape_function_nonbsp = ($format eq 'html')
358 ? \&_html_escape : $escape_function;
360 my %date_formats = ( 'latex' => $date_format_long,
361 'html' => $date_format_long,
364 $date_formats{'html'} =~ s/ / /g;
366 my $date_format = $date_formats{$format};
368 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
370 'html' => sub { return '<b>'. shift(). '</b>'
372 'template' => sub { shift },
374 my $embolden_function = $embolden_functions{$format};
376 my %newline_tokens = ( 'latex' => '\\\\',
380 my $newline_token = $newline_tokens{$format};
382 warn "$me generating template variables\n"
385 # generate template variables
388 defined( $conf->config_orbase( "invoice_${format}returnaddress",
392 && length( $conf->config_orbase( "invoice_${format}returnaddress",
398 $returnaddress = join("\n",
399 $conf->config_orbase("invoice_${format}returnaddress", $template)
403 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
405 my $convert_map = $convert_maps{$format}{'returnaddress'};
408 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
413 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
415 my $convert_map = $convert_maps{$format}{'returnaddress'};
416 $returnaddress = join( "\n", &$convert_map(
417 map { s/( {2,})/'~' x length($1)/eg;
421 ( $conf->config('company_name', $cust_main->agentnum),
422 $conf->config('company_address', $cust_main->agentnum),
429 my $warning = "Couldn't find a return address; ".
430 "do you need to set the company_address configuration value?";
432 $returnaddress = $nbsp;
433 #$returnaddress = $warning;
437 warn "$me generating invoice data\n"
440 my $agentnum = $cust_main->agentnum;
445 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
446 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
447 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
448 'returnaddress' => $returnaddress,
449 'agent' => &$escape_function($cust_main->agent->agent),
451 #invoice/quotation info
452 'invnum' => $self->invnum,
453 'quotationnum' => $self->quotationnum,
454 'date' => time2str($date_format, $self->_date),
455 'today' => time2str($date_format_long, $today),
456 'terms' => $self->terms,
457 'template' => $template, #params{'template'},
458 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
459 'current_charges' => sprintf("%.2f", $self->charged),
460 'duedate' => $self->due_date2str($rdate_format), #date_format?
463 'custnum' => $cust_main->display_custnum,
464 'prospectnum' => $cust_main->prospectnum,
465 'agent_custid' => &$escape_function($cust_main->agent_custid),
466 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
467 payname company address1 address2 city state zip fax
471 'ship_enable' => $conf->exists('invoice-ship_address'),
472 'unitprices' => $conf->exists('invoice-unitprice'),
473 'smallernotes' => $conf->exists('invoice-smallernotes'),
474 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
475 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
477 #layout info -- would be fancy to calc some of this and bury the template
479 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
480 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
481 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
482 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
483 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
484 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
485 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
486 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
487 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
488 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
490 # better hang on to conf_dir for a while (for old templates)
491 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
493 #these are only used when doing paged plaintext
500 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
501 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
502 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
503 # eval to avoid death for unimplemented languages
504 my $dh = eval { Date::Language->new($info{'name'}) } ||
505 Date::Language->new(); # fall back to English
506 # prototype here to silence warnings
507 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
508 # eventually use this date handle everywhere in here, too
510 my $min_sdate = 999999999999;
512 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
513 next unless $cust_bill_pkg->pkgnum > 0;
514 $min_sdate = $cust_bill_pkg->sdate
515 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
516 $max_edate = $cust_bill_pkg->edate
517 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
520 $invoice_data{'bill_period'} = '';
521 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
522 . " to " . time2str('%e %h', $max_edate)
523 if ($max_edate != 0 && $min_sdate != 999999999999);
525 $invoice_data{finance_section} = '';
526 if ( $conf->config('finance_pkgclass') ) {
528 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
529 $invoice_data{finance_section} = $pkg_class->categoryname;
531 $invoice_data{finance_amount} = '0.00';
532 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
534 my $countrydefault = $conf->config('countrydefault') || 'US';
535 foreach ( qw( address1 address2 city state zip country fax) ){
536 my $method = 'ship_'.$_;
537 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
539 foreach ( qw( contact company ) ) { #compatibility
540 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
542 $invoice_data{'ship_country'} = ''
543 if ( $invoice_data{'ship_country'} eq $countrydefault );
545 $invoice_data{'cid'} = $params{'cid'}
548 if ( $cust_main->country eq $countrydefault ) {
549 $invoice_data{'country'} = '';
551 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
555 $invoice_data{'address'} = \@address;
558 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
559 ? " (P.O. #". $cust_main->payinfo. ")"
563 push @address, $cust_main->company
564 if $cust_main->company;
565 push @address, $cust_main->address1;
566 push @address, $cust_main->address2
567 if $cust_main->address2;
569 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
570 push @address, $invoice_data{'country'}
571 if $invoice_data{'country'};
573 while (scalar(@address) < 5);
575 $invoice_data{'logo_file'} = $params{'logo_file'}
576 if $params{'logo_file'};
577 $invoice_data{'barcode_file'} = $params{'barcode_file'}
578 if $params{'barcode_file'};
579 $invoice_data{'barcode_img'} = $params{'barcode_img'}
580 if $params{'barcode_img'};
581 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
582 if $params{'barcode_cid'};
584 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
585 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
586 #my $balance_due = $self->owed + $pr_total - $cr_total;
587 my $balance_due = $self->owed + $pr_total;
589 #these are used on the summary page only
591 # the customer's current balance as shown on the invoice before this one
592 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
594 # the change in balance from that invoice to this one
595 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
597 # the sum of amount owed on all previous invoices
598 # ($pr_total is used elsewhere but not as $previous_balance)
599 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
601 # the sum of amount owed on all invoices
602 # (this is used in the summary & on the payment coupon)
603 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
605 # info from customer's last invoice before this one, for some
607 $invoice_data{'last_bill'} = {};
608 my $last_bill = $pr_cust_bill[-1];
610 $invoice_data{'last_bill'} = {
611 '_date' => $last_bill->_date, #unformatted
612 # all we need for now
616 my $summarypage = '';
617 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
620 $invoice_data{'summarypage'} = $summarypage;
622 warn "$me substituting variables in notes, footer, smallfooter\n"
625 my $tc = $self->template_conf;
626 my @include = ( [ $tc, 'notes' ],
627 [ 'invoice_', 'footer' ],
628 [ 'invoice_', 'smallfooter', ],
630 push @include, [ $tc, 'coupon', ]
631 unless $params{'no_coupon'};
633 foreach my $i (@include) {
635 my($base, $include) = @$i;
637 my $inc_file = $conf->key_orbase("$base$format$include", $template);
640 if ( $conf->exists($inc_file, $agentnum)
641 && length( $conf->config($inc_file, $agentnum) ) ) {
643 @inc_src = $conf->config($inc_file, $agentnum);
647 $inc_file = $conf->key_orbase("${base}latex$include", $template);
649 my $convert_map = $convert_maps{$format}{$include};
651 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
652 s/--\@\]/$delimiters{$format}[1]/g;
655 &$convert_map( $conf->config($inc_file, $agentnum) );
659 my $inc_tt = new Text::Template (
661 SOURCE => [ map "$_\n", @inc_src ],
662 DELIMITERS => $delimiters{$format},
663 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
665 unless ( $inc_tt->compile() ) {
666 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
667 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
671 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
673 $invoice_data{$include} =~ s/\n+$//
674 if ($format eq 'latex');
677 # let invoices use either of these as needed
678 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
679 ? $cust_main->payinfo : '';
680 $invoice_data{'po_line'} =
681 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
682 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
685 my %money_chars = ( 'latex' => '',
686 'html' => $conf->config('money_char') || '$',
689 my $money_char = $money_chars{$format};
691 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
692 'html' => $conf->config('money_char') || '$',
695 my $other_money_char = $other_money_chars{$format};
696 $invoice_data{'dollar'} = $other_money_char;
698 my @detail_items = ();
699 my @total_items = ();
703 $invoice_data{'detail_items'} = \@detail_items;
704 $invoice_data{'total_items'} = \@total_items;
705 $invoice_data{'buf'} = \@buf;
706 $invoice_data{'sections'} = \@sections;
708 warn "$me generating sections\n"
711 # Previous Charges section
712 # subtotal is the first return value from $self->previous
713 my $previous_section = { 'description' => $self->mt('Previous Charges'),
714 'subtotal' => $other_money_char.
715 sprintf('%.2f', $pr_total),
716 'summarized' => '', #why? $summarypage ? 'Y' : '',
718 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
719 join(' / ', map { $cust_main->balance_date_range(@$_) }
720 $self->_prior_month30s
722 if $conf->exists('invoice_include_aging');
725 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
726 'subtotal' => $taxtotal, # adjusted below
728 my $tax_weight = _pkg_category($tax_section->{description})
729 ? _pkg_category($tax_section->{description})->weight
731 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
732 $tax_section->{'sort_weight'} = $tax_weight;
736 my $adjust_section = {
737 'description' => $self->mt('Credits, Payments, and Adjustments'),
738 'adjust_section' => 1,
739 'subtotal' => 0, # adjusted below
741 my $adjust_weight = _pkg_category($adjust_section->{description})
742 ? _pkg_category($adjust_section->{description})->weight
744 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
745 $adjust_section->{'sort_weight'} = $adjust_weight;
747 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
748 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
749 $invoice_data{'multisection'} = $multisection;
750 my $late_sections = [];
751 my $extra_sections = [];
752 my $extra_lines = ();
754 my $default_section = { 'description' => '',
759 if ( $multisection ) {
760 ($extra_sections, $extra_lines) =
761 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
762 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
763 && $self->can('_items_extra_usage_sections');
765 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
767 push @detail_items, @$extra_lines if $extra_lines;
769 $self->_items_sections( $late_sections, # this could stand a refactor
771 $escape_function_nonbsp,
775 if ( $conf->exists('svc_phone_sections')
776 && $self->can('_items_svc_phone_sections')
779 my ($phone_sections, $phone_lines) =
780 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
781 push @{$late_sections}, @$phone_sections;
782 push @detail_items, @$phone_lines;
784 if ( $conf->exists('voip-cust_accountcode_cdr')
785 && $cust_main->accountcode_cdr
786 && $self->can('_items_accountcode_cdr')
789 my ($accountcode_section, $accountcode_lines) =
790 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
791 if ( scalar(@$accountcode_lines) ) {
792 push @{$late_sections}, $accountcode_section;
793 push @detail_items, @$accountcode_lines;
796 } else {# not multisection
797 # make a default section
798 push @sections, $default_section;
799 # and calculate the finance charge total, since it won't get done otherwise.
800 # XXX possibly other totals?
801 # XXX possibly finance_pkgclass should not be used in this manner?
802 if ( $conf->exists('finance_pkgclass') ) {
804 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
805 if ( grep { $_->section eq $invoice_data{finance_section} }
806 $cust_bill_pkg->cust_bill_pkg_display ) {
807 # I think these are always setup fees, but just to be sure...
808 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
811 $invoice_data{finance_amount} =
812 sprintf('%.2f', sum( @finance_charges ) || 0);
816 # previous invoice balances in the Previous Charges section if there
817 # is one, otherwise in the main detail section
818 if ( $self->can('_items_previous') &&
819 $self->enable_previous &&
820 ! $conf->exists('previous_balance-summary_only') ) {
822 warn "$me adding previous balances\n"
825 foreach my $line_item ( $self->_items_previous ) {
828 ext_description => [],
830 $detail->{'ref'} = $line_item->{'pkgnum'};
831 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
832 $detail->{'quantity'} = 1;
833 $detail->{'section'} = $multisection ? $previous_section
835 $detail->{'description'} = &$escape_function($line_item->{'description'});
836 if ( exists $line_item->{'ext_description'} ) {
837 @{$detail->{'ext_description'}} = map {
838 &$escape_function($_);
839 } @{$line_item->{'ext_description'}};
841 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
842 $line_item->{'amount'};
843 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
845 push @detail_items, $detail;
846 push @buf, [ $detail->{'description'},
847 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
853 if ( @pr_cust_bill && $self->enable_previous ) {
854 push @buf, ['','-----------'];
855 push @buf, [ $self->mt('Total Previous Balance'),
856 $money_char. sprintf("%10.2f", $pr_total) ];
860 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
861 warn "$me adding DID summary\n"
864 my ($didsummary,$minutes) = $self->_did_summary;
865 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
867 { 'description' => $didsummary_desc,
868 'ext_description' => [ $didsummary, $minutes ],
872 foreach my $section (@sections, @$late_sections) {
874 warn "$me adding section \n". Dumper($section)
877 # begin some normalization
878 $section->{'subtotal'} = $section->{'amount'}
880 && !exists($section->{subtotal})
881 && exists($section->{amount});
883 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
884 if ( $invoice_data{finance_section} &&
885 $section->{'description'} eq $invoice_data{finance_section} );
887 $section->{'subtotal'} = $other_money_char.
888 sprintf('%.2f', $section->{'subtotal'})
891 # continue some normalization
892 $section->{'amount'} = $section->{'subtotal'}
896 if ( $section->{'description'} ) {
897 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
902 warn "$me setting options\n"
906 $options{'section'} = $section if $multisection;
907 $options{'format'} = $format;
908 $options{'escape_function'} = $escape_function;
909 $options{'no_usage'} = 1 unless $unsquelched;
910 $options{'unsquelched'} = $unsquelched;
911 $options{'summary_page'} = $summarypage;
912 $options{'skip_usage'} =
913 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
914 $options{'multisection'} = $multisection;
916 warn "$me searching for line items\n"
919 foreach my $line_item ( $self->_items_pkg(%options) ) {
921 warn "$me adding line item $line_item\n"
925 ext_description => [],
927 $detail->{'ref'} = $line_item->{'pkgnum'};
928 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
929 $detail->{'quantity'} = $line_item->{'quantity'};
930 $detail->{'section'} = $section;
931 $detail->{'description'} = &$escape_function($line_item->{'description'});
932 if ( exists $line_item->{'ext_description'} ) {
933 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
935 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
936 $line_item->{'amount'};
937 if ( exists $line_item->{'unit_amount'} ) {
938 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
939 $line_item->{'unit_amount'};
941 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
943 $detail->{'sdate'} = $line_item->{'sdate'};
944 $detail->{'edate'} = $line_item->{'edate'};
945 $detail->{'seconds'} = $line_item->{'seconds'};
946 $detail->{'svc_label'} = $line_item->{'svc_label'};
948 push @detail_items, $detail;
949 push @buf, ( [ $detail->{'description'},
950 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
952 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
956 if ( $section->{'description'} ) {
957 push @buf, ( ['','-----------'],
958 [ $section->{'description'}. ' sub-total',
959 $section->{'subtotal'} # already formatted this
968 $invoice_data{current_less_finance} =
969 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
971 # create a major section for previous balance if we have major sections,
972 # or if previous_section is in summary form
973 if ( ( $multisection && $self->enable_previous )
974 || $conf->exists('previous_balance-summary_only') )
976 unshift @sections, $previous_section if $pr_total;
979 warn "$me adding taxes\n"
982 foreach my $tax ( $self->_items_tax ) {
984 $taxtotal += $tax->{'amount'};
986 my $description = &$escape_function( $tax->{'description'} );
987 my $amount = sprintf( '%.2f', $tax->{'amount'} );
989 if ( $multisection ) {
991 my $money = $old_latex ? '' : $money_char;
992 push @detail_items, {
993 ext_description => [],
996 description => $description,
997 amount => $money. $amount,
999 section => $tax_section,
1004 push @total_items, {
1005 'total_item' => $description,
1006 'total_amount' => $other_money_char. $amount,
1011 push @buf,[ $description,
1012 $money_char. $amount,
1019 $total->{'total_item'} = $self->mt('Sub-total');
1020 $total->{'total_amount'} =
1021 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1023 if ( $multisection ) {
1024 $tax_section->{'subtotal'} = $other_money_char.
1025 sprintf('%.2f', $taxtotal);
1026 $tax_section->{'pretotal'} = 'New charges sub-total '.
1027 $total->{'total_amount'};
1028 push @sections, $tax_section if $taxtotal;
1030 unshift @total_items, $total;
1033 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1035 push @buf,['','-----------'];
1036 push @buf,[$self->mt(
1037 (!$self->enable_previous)
1039 : 'Total New Charges'
1041 $money_char. sprintf("%10.2f",$self->charged) ];
1044 # calculate total, possibly including total owed on previous
1049 $item = $conf->config('previous_balance-exclude_from_total')
1050 || 'Total New Charges'
1051 if $conf->exists('previous_balance-exclude_from_total');
1052 my $amount = $self->charged;
1053 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1054 $amount += $pr_total;
1057 $total->{'total_item'} = &$embolden_function($self->mt($item));
1058 $total->{'total_amount'} =
1059 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1060 if ( $multisection ) {
1061 if ( $adjust_section->{'sort_weight'} ) {
1062 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1063 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1065 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1066 $other_money_char. sprintf('%.2f', $self->charged );
1069 push @total_items, $total;
1071 push @buf,['','-----------'];
1074 sprintf( '%10.2f', $amount )
1079 # if we're showing previous invoices, also show previous
1080 # credits and payments
1081 if ( $self->enable_previous
1082 and $self->can('_items_credits')
1083 and $self->can('_items_payments') )
1085 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1088 my $credittotal = 0;
1089 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1092 $total->{'total_item'} = &$escape_function($credit->{'description'});
1093 $credittotal += $credit->{'amount'};
1094 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1095 $adjusttotal += $credit->{'amount'};
1096 if ( $multisection ) {
1097 my $money = $old_latex ? '' : $money_char;
1098 push @detail_items, {
1099 ext_description => [],
1102 description => &$escape_function($credit->{'description'}),
1103 amount => $money. $credit->{'amount'},
1105 section => $adjust_section,
1108 push @total_items, $total;
1112 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1115 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1116 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1120 my $paymenttotal = 0;
1121 foreach my $payment ( $self->_items_payments ) {
1123 $total->{'total_item'} = &$escape_function($payment->{'description'});
1124 $paymenttotal += $payment->{'amount'};
1125 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1126 $adjusttotal += $payment->{'amount'};
1127 if ( $multisection ) {
1128 my $money = $old_latex ? '' : $money_char;
1129 push @detail_items, {
1130 ext_description => [],
1133 description => &$escape_function($payment->{'description'}),
1134 amount => $money. $payment->{'amount'},
1136 section => $adjust_section,
1139 push @total_items, $total;
1141 push @buf, [ $payment->{'description'},
1142 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1145 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1147 if ( $multisection ) {
1148 $adjust_section->{'subtotal'} = $other_money_char.
1149 sprintf('%.2f', $adjusttotal);
1150 push @sections, $adjust_section
1151 unless $adjust_section->{sort_weight};
1154 # create Balance Due message
1157 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1158 $total->{'total_amount'} =
1159 &$embolden_function(
1160 $other_money_char. sprintf('%.2f', #why? $summarypage
1161 # ? $self->charged +
1162 # $self->billing_balance
1164 $self->owed + $pr_total
1167 if ( $multisection && !$adjust_section->{sort_weight} ) {
1168 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1169 $total->{'total_amount'};
1171 push @total_items, $total;
1173 push @buf,['','-----------'];
1174 push @buf,[$self->balance_due_msg, $money_char.
1175 sprintf("%10.2f", $balance_due ) ];
1178 if ( $conf->exists('previous_balance-show_credit')
1179 and $cust_main->balance < 0 ) {
1180 my $credit_total = {
1181 'total_item' => &$embolden_function($self->credit_balance_msg),
1182 'total_amount' => &$embolden_function(
1183 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1186 if ( $multisection ) {
1187 $adjust_section->{'posttotal'} .= $newline_token .
1188 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1191 push @total_items, $credit_total;
1193 push @buf,['','-----------'];
1194 push @buf,[$self->credit_balance_msg, $money_char.
1195 sprintf("%10.2f", -$cust_main->balance ) ];
1199 if ( $multisection ) {
1200 if ( $conf->exists('svc_phone_sections')
1201 && $self->can('_items_svc_phone_sections')
1205 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1206 $total->{'total_amount'} =
1207 &$embolden_function(
1208 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1210 my $last_section = pop @sections;
1211 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1212 $total->{'total_amount'};
1213 push @sections, $last_section;
1215 push @sections, @$late_sections
1219 # make a discounts-available section, even without multisection
1220 if ( $conf->exists('discount-show_available')
1221 and my @discounts_avail = $self->_items_discounts_avail ) {
1222 my $discount_section = {
1223 'description' => $self->mt('Discounts Available'),
1228 push @sections, $discount_section;
1229 push @detail_items, map { +{
1230 'ref' => '', #should this be something else?
1231 'section' => $discount_section,
1232 'description' => &$escape_function( $_->{description} ),
1233 'amount' => $money_char . &$escape_function( $_->{amount} ),
1234 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1235 } } @discounts_avail;
1238 # debugging hook: call this with 'diag' => 1 to just get a hash of
1239 # the invoice variables
1240 return \%invoice_data if ( $params{'diag'} );
1242 # All sections and items are built; now fill in templates.
1243 my @includelist = ();
1244 push @includelist, 'summary' if $summarypage;
1245 foreach my $include ( @includelist ) {
1247 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1250 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1252 @inc_src = $conf->config($inc_file, $agentnum);
1256 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1258 my $convert_map = $convert_maps{$format}{$include};
1260 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1261 s/--\@\]/$delimiters{$format}[1]/g;
1264 &$convert_map( $conf->config($inc_file, $agentnum) );
1268 my $inc_tt = new Text::Template (
1270 SOURCE => [ map "$_\n", @inc_src ],
1271 DELIMITERS => $delimiters{$format},
1272 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1274 unless ( $inc_tt->compile() ) {
1275 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1276 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1280 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1282 $invoice_data{$include} =~ s/\n+$//
1283 if ($format eq 'latex');
1288 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1289 /invoice_lines\((\d*)\)/;
1290 $invoice_lines += $1 || scalar(@buf);
1293 die "no invoice_lines() functions in template?"
1294 if ( $format eq 'template' && !$wasfunc );
1296 if ($format eq 'template') {
1298 if ( $invoice_lines ) {
1299 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1300 $invoice_data{'total_pages'}++
1301 if scalar(@buf) % $invoice_lines;
1304 #setup subroutine for the template
1305 $invoice_data{invoice_lines} = sub {
1306 my $lines = shift || scalar(@buf);
1318 push @collect, split("\n",
1319 $text_template->fill_in( HASH => \%invoice_data )
1321 $invoice_data{'page'}++;
1323 map "$_\n", @collect;
1325 } else { # this is where we actually create the invoice
1327 warn "filling in template for invoice ". $self->invnum. "\n"
1329 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1332 $text_template->fill_in(HASH => \%invoice_data);
1336 sub notice_name { '('.shift->table.')'; }
1338 sub template_conf { 'invoice_'; }
1340 # helper routine for generating date ranges
1341 sub _prior_month30s {
1344 [ 1, 2592000 ], # 0-30 days ago
1345 [ 2592000, 5184000 ], # 30-60 days ago
1346 [ 5184000, 7776000 ], # 60-90 days ago
1347 [ 7776000, 0 ], # 90+ days ago
1350 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1351 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1356 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1358 Returns an postscript invoice, as a scalar.
1360 Options can be passed as a hashref (recommended) or as a list of time, template
1361 and then any key/value pairs for any other options.
1363 I<time> an optional value used to control the printing of overdue messages. The
1364 default is now. It isn't the date of the invoice; that's the `_date' field.
1365 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1366 L<Time::Local> and L<Date::Parse> for conversion functions.
1368 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1375 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1376 my $ps = generate_ps($file);
1378 unlink($barcodefile) if $barcodefile;
1383 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1385 Returns an PDF invoice, as a scalar.
1387 Options can be passed as a hashref (recommended) or as a list of time, template
1388 and then any key/value pairs for any other options.
1390 I<time> an optional value used to control the printing of overdue messages. The
1391 default is now. It isn't the date of the invoice; that's the `_date' field.
1392 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1393 L<Time::Local> and L<Date::Parse> for conversion functions.
1395 I<template>, if specified, is the name of a suffix for alternate invoices.
1397 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1404 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1405 my $pdf = generate_pdf($file);
1407 unlink($barcodefile) if $barcodefile;
1412 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1414 Returns an HTML invoice, as a scalar.
1416 I<time> an optional value used to control the printing of overdue messages. The
1417 default is now. It isn't the date of the invoice; that's the `_date' field.
1418 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1419 L<Time::Local> and L<Date::Parse> for conversion functions.
1421 I<template>, if specified, is the name of a suffix for alternate invoices.
1423 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1425 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1426 when emailing the invoice as part of a multipart/related MIME email.
1434 %params = %{ shift() };
1436 $params{'time'} = shift;
1437 $params{'template'} = shift;
1438 $params{'cid'} = shift;
1441 $params{'format'} = 'html';
1443 $self->print_generic( %params );
1446 # quick subroutine for print_latex
1448 # There are ten characters that LaTeX treats as special characters, which
1449 # means that they do not simply typeset themselves:
1450 # # $ % & ~ _ ^ \ { }
1452 # TeX ignores blanks following an escaped character; if you want a blank (as
1453 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1457 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1458 $value =~ s/([<>])/\$$1\$/g;
1464 encode_entities($value);
1468 sub _html_escape_nbsp {
1469 my $value = _html_escape(shift);
1470 $value =~ s/ +/ /g;
1474 #utility methods for print_*
1476 sub _translate_old_latex_format {
1477 warn "_translate_old_latex_format called\n"
1484 if ( $line =~ /^%%Detail\s*$/ ) {
1486 push @template, q![@--!,
1487 q! foreach my $_tr_line (@detail_items) {!,
1488 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1489 q! $_tr_line->{'description'} .= !,
1490 q! "\\tabularnewline\n~~".!,
1491 q! join( "\\tabularnewline\n~~",!,
1492 q! @{$_tr_line->{'ext_description'}}!,
1496 while ( ( my $line_item_line = shift )
1497 !~ /^%%EndDetail\s*$/ ) {
1498 $line_item_line =~ s/'/\\'/g; # nice LTS
1499 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1500 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1501 push @template, " \$OUT .= '$line_item_line';";
1504 push @template, '}',
1507 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1509 push @template, '[@--',
1510 ' foreach my $_tr_line (@total_items) {';
1512 while ( ( my $total_item_line = shift )
1513 !~ /^%%EndTotalDetails\s*$/ ) {
1514 $total_item_line =~ s/'/\\'/g; # nice LTS
1515 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1516 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1517 push @template, " \$OUT .= '$total_item_line';";
1520 push @template, '}',
1524 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1525 push @template, $line;
1531 warn "$_\n" foreach @template;
1539 my $conf = $self->conf;
1541 #check for an invoice-specific override
1542 return $self->invoice_terms if $self->invoice_terms;
1544 #check for a customer- specific override
1545 my $cust_main = $self->cust_main;
1546 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1548 #use configured default
1549 $conf->config('invoice_default_terms') || '';
1555 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1556 $duedate = $self->_date() + ( $1 * 86400 );
1563 $self->due_date ? time2str(shift, $self->due_date) : '';
1566 sub balance_due_msg {
1568 my $msg = $self->mt('Balance Due');
1569 return $msg unless $self->terms;
1570 if ( $self->due_date ) {
1571 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1572 $self->due_date2str($date_format);
1573 } elsif ( $self->terms ) {
1574 $msg .= ' - '. $self->terms;
1579 sub balance_due_date {
1581 my $conf = $self->conf;
1583 if ( $conf->exists('invoice_default_terms')
1584 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1585 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1590 sub credit_balance_msg {
1592 $self->mt('Credit Balance Remaining')
1597 Returns a string with the date, for example: "3/20/2008"
1603 time2str($date_format, $self->_date);
1606 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1608 Generate section information for all items appearing on this invoice.
1609 This will only be called for multi-section invoices.
1611 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1612 related display records (L<FS::cust_bill_pkg_display>) and organize
1613 them into two groups ("early" and "late" according to whether they come
1614 before or after the total), then into sections. A subtotal is calculated
1617 Section descriptions are returned in sort weight order. Each consists
1618 of a hash containing:
1620 description: the package category name, escaped
1621 subtotal: the total charges in that section
1622 tax_section: a flag indicating that the section contains only tax charges
1623 summarized: same as tax_section, for some reason
1624 sort_weight: the package category's sort weight
1626 If 'condense' is set on the display record, it also contains everything
1627 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1628 coderefs to generate parts of the invoice. This is not advised.
1632 LATE: an arrayref to push the "late" section hashes onto. The "early"
1633 group is simply returned from the method.
1635 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1636 Turning this on has the following effects:
1637 - Ignores display items with the 'summary' flag.
1638 - Combines all items into the "early" group.
1639 - Creates sections for all non-disabled package categories, even if they
1640 have no charges on this invoice, as well as a section with no name.
1642 ESCAPE: an escape function to use for section titles.
1644 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1645 sorted list. If there are any of these, section subtotals exclude
1648 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1649 passed through to C<_condense_section()>.
1653 use vars qw(%pkg_category_cache);
1654 sub _items_sections {
1657 my $summarypage = shift;
1659 my $extra_sections = shift;
1663 my %late_subtotal = ();
1666 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1669 my $usage = $cust_bill_pkg->usage;
1671 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1672 next if ( $display->summary && $summarypage );
1674 my $section = $display->section;
1675 my $type = $display->type;
1677 $not_tax{$section} = 1
1678 unless $cust_bill_pkg->pkgnum == 0;
1680 # there's actually a very important piece of logic buried in here:
1681 # incrementing $late_subtotal{$section} CREATES
1682 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1683 # to define the list of late sections, and likewise keys(%subtotal).
1684 # When _items_cust_bill_pkg is called to generate line items for
1685 # real, it will be called with 'section' => $section for each
1687 if ( $display->post_total && !$summarypage ) {
1688 if (! $type || $type eq 'S') {
1689 $late_subtotal{$section} += $cust_bill_pkg->setup
1690 if $cust_bill_pkg->setup != 0
1691 || $cust_bill_pkg->setup_show_zero;
1695 $late_subtotal{$section} += $cust_bill_pkg->recur
1696 if $cust_bill_pkg->recur != 0
1697 || $cust_bill_pkg->recur_show_zero;
1700 if ($type && $type eq 'R') {
1701 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1702 if $cust_bill_pkg->recur != 0
1703 || $cust_bill_pkg->recur_show_zero;
1706 if ($type && $type eq 'U') {
1707 $late_subtotal{$section} += $usage
1708 unless scalar(@$extra_sections);
1713 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1715 if (! $type || $type eq 'S') {
1716 $subtotal{$section} += $cust_bill_pkg->setup
1717 if $cust_bill_pkg->setup != 0
1718 || $cust_bill_pkg->setup_show_zero;
1722 $subtotal{$section} += $cust_bill_pkg->recur
1723 if $cust_bill_pkg->recur != 0
1724 || $cust_bill_pkg->recur_show_zero;
1727 if ($type && $type eq 'R') {
1728 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1729 if $cust_bill_pkg->recur != 0
1730 || $cust_bill_pkg->recur_show_zero;
1733 if ($type && $type eq 'U') {
1734 $subtotal{$section} += $usage
1735 unless scalar(@$extra_sections);
1744 %pkg_category_cache = ();
1746 push @$late, map { { 'description' => &{$escape}($_),
1747 'subtotal' => $late_subtotal{$_},
1749 'sort_weight' => ( _pkg_category($_)
1750 ? _pkg_category($_)->weight
1753 ((_pkg_category($_) && _pkg_category($_)->condense)
1754 ? $self->_condense_section($format)
1758 sort _sectionsort keys %late_subtotal;
1761 if ( $summarypage ) {
1762 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1763 map { $_->categoryname } qsearch('pkg_category', {});
1764 push @sections, '' if exists($subtotal{''});
1766 @sections = keys %subtotal;
1769 my @early = map { { 'description' => &{$escape}($_),
1770 'subtotal' => $subtotal{$_},
1771 'summarized' => $not_tax{$_} ? '' : 'Y',
1772 'tax_section' => $not_tax{$_} ? '' : 'Y',
1773 'sort_weight' => ( _pkg_category($_)
1774 ? _pkg_category($_)->weight
1777 ((_pkg_category($_) && _pkg_category($_)->condense)
1778 ? $self->_condense_section($format)
1783 push @early, @$extra_sections if $extra_sections;
1785 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1789 #helper subs for above
1792 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1796 my $categoryname = shift;
1797 $pkg_category_cache{$categoryname} ||=
1798 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1801 my %condensed_format = (
1802 'label' => [ qw( Description Qty Amount ) ],
1804 sub { shift->{description} },
1805 sub { shift->{quantity} },
1806 sub { my($href, %opt) = @_;
1807 ($opt{dollar} || ''). $href->{amount};
1810 'align' => [ qw( l r r ) ],
1811 'span' => [ qw( 5 1 1 ) ], # unitprices?
1812 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1815 sub _condense_section {
1816 my ( $self, $format ) = ( shift, shift );
1818 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1819 qw( description_generator
1822 total_line_generator
1827 sub _condensed_generator_defaults {
1828 my ( $self, $format ) = ( shift, shift );
1829 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1838 sub _condensed_header_generator {
1839 my ( $self, $format ) = ( shift, shift );
1841 my ( $f, $prefix, $suffix, $separator, $column ) =
1842 _condensed_generator_defaults($format);
1844 if ($format eq 'latex') {
1845 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1846 $suffix = "\\\\\n\\hline";
1849 sub { my ($d,$a,$s,$w) = @_;
1850 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1852 } elsif ( $format eq 'html' ) {
1853 $prefix = '<th></th>';
1857 sub { my ($d,$a,$s,$w) = @_;
1858 return qq!<th align="$html_align{$a}">$d</th>!;
1866 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1868 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1871 $prefix. join($separator, @result). $suffix;
1876 sub _condensed_description_generator {
1877 my ( $self, $format ) = ( shift, shift );
1879 my ( $f, $prefix, $suffix, $separator, $column ) =
1880 _condensed_generator_defaults($format);
1882 my $money_char = '$';
1883 if ($format eq 'latex') {
1884 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1886 $separator = " & \n";
1888 sub { my ($d,$a,$s,$w) = @_;
1889 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1891 $money_char = '\\dollar';
1892 }elsif ( $format eq 'html' ) {
1893 $prefix = '"><td align="center"></td>';
1897 sub { my ($d,$a,$s,$w) = @_;
1898 return qq!<td align="$html_align{$a}">$d</td>!;
1900 #$money_char = $conf->config('money_char') || '$';
1901 $money_char = ''; # this is madness
1909 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1911 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1913 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1914 map { $f->{$_}->[$i] } qw(align span width)
1918 $prefix. join( $separator, @result ). $suffix;
1923 sub _condensed_total_generator {
1924 my ( $self, $format ) = ( shift, shift );
1926 my ( $f, $prefix, $suffix, $separator, $column ) =
1927 _condensed_generator_defaults($format);
1930 if ($format eq 'latex') {
1933 $separator = " & \n";
1935 sub { my ($d,$a,$s,$w) = @_;
1936 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1938 }elsif ( $format eq 'html' ) {
1942 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1944 sub { my ($d,$a,$s,$w) = @_;
1945 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1954 # my $r = &{$f->{fields}->[$i]}(@args);
1955 # $r .= ' Total' unless $i;
1957 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1959 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1960 map { $f->{$_}->[$i] } qw(align span width)
1964 $prefix. join( $separator, @result ). $suffix;
1969 =item total_line_generator FORMAT
1971 Returns a coderef used for generation of invoice total line items for this
1972 usage_class. FORMAT is either html or latex
1976 # should not be used: will have issues with hash element names (description vs
1977 # total_item and amount vs total_amount -- another array of functions?
1979 sub _condensed_total_line_generator {
1980 my ( $self, $format ) = ( shift, shift );
1982 my ( $f, $prefix, $suffix, $separator, $column ) =
1983 _condensed_generator_defaults($format);
1986 if ($format eq 'latex') {
1989 $separator = " & \n";
1991 sub { my ($d,$a,$s,$w) = @_;
1992 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1994 }elsif ( $format eq 'html' ) {
1998 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2000 sub { my ($d,$a,$s,$w) = @_;
2001 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2010 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2012 &{$column}( &{$f->{fields}->[$i]}(@args),
2013 map { $f->{$_}->[$i] } qw(align span width)
2017 $prefix. join( $separator, @result ). $suffix;
2022 # sub _items { # seems to be unused
2025 # #my @display = scalar(@_)
2027 # # : qw( _items_previous _items_pkg );
2028 # # #: qw( _items_pkg );
2029 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2030 # my @display = qw( _items_previous _items_pkg );
2033 # foreach my $display ( @display ) {
2034 # push @b, $self->$display(@_);
2039 =item _items_pkg [ OPTIONS ]
2041 Return line item hashes for each package item on this invoice. Nearly
2044 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2046 The only OPTIONS accepted is 'section', which may point to a hashref
2047 with a key named 'condensed', which may have a true value. If it
2048 does, this method tries to merge identical items into items with
2049 'quantity' equal to the number of items (not the sum of their
2050 separate quantities, for some reason).
2056 grep { $_->pkgnum } $self->cust_bill_pkg;
2063 warn "$me _items_pkg searching for all package line items\n"
2066 my @cust_bill_pkg = $self->_items_nontax;
2068 warn "$me _items_pkg filtering line items\n"
2070 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2072 if ($options{section} && $options{section}->{condensed}) {
2074 warn "$me _items_pkg condensing section\n"
2078 local $Storable::canonical = 1;
2079 foreach ( @items ) {
2081 delete $item->{ref};
2082 delete $item->{ext_description};
2083 my $key = freeze($item);
2084 $itemshash{$key} ||= 0;
2085 $itemshash{$key} ++; # += $item->{quantity};
2087 @items = sort { $a->{description} cmp $b->{description} }
2088 map { my $i = thaw($_);
2089 $i->{quantity} = $itemshash{$_};
2091 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2097 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2104 return 0 unless $a->itemdesc cmp $b->itemdesc;
2105 return -1 if $b->itemdesc eq 'Tax';
2106 return 1 if $a->itemdesc eq 'Tax';
2107 return -1 if $b->itemdesc eq 'Other surcharges';
2108 return 1 if $a->itemdesc eq 'Other surcharges';
2109 $a->itemdesc cmp $b->itemdesc;
2114 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2115 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2118 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2120 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2121 list of hashrefs describing the line items they generate on the invoice.
2123 OPTIONS may include:
2125 format: the invoice format.
2127 escape_function: the function used to escape strings.
2129 DEPRECATED? (expensive, mostly unused?)
2130 format_function: the function used to format CDRs.
2132 section: a hashref containing 'description'; if this is present,
2133 cust_bill_pkg_display records not belonging to this section are
2136 multisection: a flag indicating that this is a multisection invoice,
2137 which does something complicated.
2139 Returns a list of hashrefs, each of which may contain:
2141 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2142 ext_description, which is an arrayref of detail lines to show below
2147 sub _items_cust_bill_pkg {
2149 my $conf = $self->conf;
2150 my $cust_bill_pkgs = shift;
2153 my $format = $opt{format} || '';
2154 my $escape_function = $opt{escape_function} || sub { shift };
2155 my $format_function = $opt{format_function} || '';
2156 my $no_usage = $opt{no_usage} || '';
2157 my $unsquelched = $opt{unsquelched} || ''; #unused
2158 my $section = $opt{section}->{description} if $opt{section};
2159 my $summary_page = $opt{summary_page} || ''; #unused
2160 my $multisection = $opt{multisection} || '';
2161 my $discount_show_always = 0;
2163 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2165 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2166 # and location labels
2169 my ($s, $r, $u) = ( undef, undef, undef );
2170 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2173 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2174 if ( $_ && !$cust_bill_pkg->hidden ) {
2175 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2176 $_->{amount} =~ s/^\-0\.00$/0.00/;
2177 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2179 if $_->{amount} != 0
2180 || $discount_show_always
2181 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2182 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2188 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2189 ? $cust_bill_pkg->cust_bill_pkg_display
2190 : ( $cust_bill_pkg );
2192 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2193 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2196 foreach my $display ( grep { defined($section)
2197 ? $_->section eq $section
2200 grep { !$_->summary || $multisection }
2201 @cust_bill_pkg_display
2205 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2206 $display->billpkgdisplaynum. "\n"
2209 my $type = $display->type;
2211 my $desc = $cust_bill_pkg->desc;
2212 $desc = substr($desc, 0, $maxlength). '...'
2213 if $format eq 'latex' && length($desc) > $maxlength;
2215 my %details_opt = ( 'format' => $format,
2216 'escape_function' => $escape_function,
2217 'format_function' => $format_function,
2218 'no_usage' => $opt{'no_usage'},
2221 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2223 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2226 if ( $cust_bill_pkg->setup != 0 ) {
2227 my $description = $desc;
2228 $description .= ' Setup'
2229 if $cust_bill_pkg->recur != 0
2230 || $discount_show_always
2231 || $cust_bill_pkg->recur_show_zero;
2233 'description' => $description,
2234 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2237 if ( $cust_bill_pkg->recur != 0 ) {
2239 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2240 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2244 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2246 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2249 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2251 # which pkgpart to show for display purposes?
2252 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2254 # start/end dates for invoice formats that do nonstandard
2256 my %item_dates = ();
2257 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2258 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2260 if ( (!$type || $type eq 'S')
2261 && ( $cust_bill_pkg->setup != 0
2262 || $cust_bill_pkg->setup_show_zero
2267 warn "$me _items_cust_bill_pkg adding setup\n"
2270 my $description = $desc;
2271 $description .= ' Setup'
2272 if $cust_bill_pkg->recur != 0
2273 || $discount_show_always
2274 || $cust_bill_pkg->recur_show_zero;
2278 unless ( $cust_pkg->part_pkg->hide_svc_detail
2279 || $cust_bill_pkg->hidden )
2282 my @svc_labels = map &{$escape_function}($_),
2283 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2284 push @d, @svc_labels
2285 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2286 $svc_label = $svc_labels[0];
2288 if ( ! $cust_pkg->locationnum or
2289 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2290 my $loc = $cust_pkg->location_label;
2291 $loc = substr($loc, 0, $maxlength). '...'
2292 if $format eq 'latex' && length($loc) > $maxlength;
2293 push @d, &{$escape_function}($loc);
2296 } #unless hiding service details
2298 push @d, $cust_bill_pkg->details(%details_opt)
2299 if $cust_bill_pkg->recur == 0;
2301 if ( $cust_bill_pkg->hidden ) {
2302 $s->{amount} += $cust_bill_pkg->setup;
2303 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2304 push @{ $s->{ext_description} }, @d;
2308 description => $description,
2309 pkgpart => $pkgpart,
2310 pkgnum => $cust_bill_pkg->pkgnum,
2311 amount => $cust_bill_pkg->setup,
2312 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2313 unit_amount => $cust_bill_pkg->unitsetup,
2314 quantity => $cust_bill_pkg->quantity,
2315 ext_description => \@d,
2316 svc_label => ($svc_label || ''),
2322 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2324 $cust_bill_pkg->recur != 0
2325 || $cust_bill_pkg->setup == 0
2326 || $discount_show_always
2327 || $cust_bill_pkg->recur_show_zero
2332 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2335 my $is_summary = $display->summary;
2336 my $description = ($is_summary && $type && $type eq 'U')
2337 ? "Usage charges" : $desc;
2339 my $part_pkg = $cust_pkg->part_pkg;
2341 #pry be a bit more efficient to look some of this conf stuff up
2344 $conf->exists('disable_line_item_date_ranges')
2345 || $part_pkg->option('disable_line_item_date_ranges',1)
2346 || ! $cust_bill_pkg->sdate
2347 || ! $cust_bill_pkg->edate
2350 my $date_style = '';
2351 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2352 $cust_main->agentnum
2354 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2355 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2356 $cust_main->agentnum
2358 if ( defined($date_style) && $date_style eq 'month_of' ) {
2359 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2360 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2361 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2362 $cust_main->agentnum
2364 $desc .= ' ' unless $desc =~ /\s$/;
2365 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2367 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2368 " - ". time2str($date_format, $cust_bill_pkg->edate);
2370 $description .= " ($time_period)";
2374 my @seconds = (); # for display of usage info
2377 #at least until cust_bill_pkg has "past" ranges in addition to
2378 #the "future" sdate/edate ones... see #3032
2379 my @dates = ( $self->_date );
2380 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2381 push @dates, $prev->sdate if $prev;
2382 push @dates, undef if !$prev;
2384 unless ( $part_pkg->hide_svc_detail
2385 || $cust_bill_pkg->itemdesc
2386 || $cust_bill_pkg->hidden
2387 || $is_summary && $type && $type eq 'U'
2391 warn "$me _items_cust_bill_pkg adding service details\n"
2394 my @svc_labels = map &{$escape_function}($_),
2395 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2396 push @d, @svc_labels
2397 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2398 $svc_label = $svc_labels[0];
2400 warn "$me _items_cust_bill_pkg done adding service details\n"
2403 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2404 my $loc = $cust_pkg->location_label;
2405 $loc = substr($loc, 0, $maxlength). '...'
2406 if $format eq 'latex' && length($loc) > $maxlength;
2407 push @d, &{$escape_function}($loc);
2410 # Display of seconds_since_sqlradacct:
2411 # On the invoice, when processing @detail_items, look for a field
2412 # named 'seconds'. This will contain total seconds for each
2413 # service, in the same order as @ext_description. For services
2414 # that don't support this it will show undef.
2415 if ( $conf->exists('svc_acct-usage_seconds')
2416 and ! $cust_bill_pkg->pkgpart_override ) {
2417 foreach my $cust_svc (
2418 $cust_pkg->h_cust_svc(@dates, 'I')
2421 # eval because not having any part_export_usage exports
2422 # is a fatal error, last_bill/_date because that's how
2423 # sqlradius_hour billing does it
2425 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2427 push @seconds, $sec;
2429 } #if svc_acct-usage_seconds
2433 unless ( $is_summary ) {
2434 warn "$me _items_cust_bill_pkg adding details\n"
2437 #instead of omitting details entirely in this case (unwanted side
2438 # effects), just omit CDRs
2439 $details_opt{'no_usage'} = 1
2440 if $type && $type eq 'R';
2442 push @d, $cust_bill_pkg->details(%details_opt);
2445 warn "$me _items_cust_bill_pkg calculating amount\n"
2450 $amount = $cust_bill_pkg->recur;
2451 } elsif ($type eq 'R') {
2452 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2453 } elsif ($type eq 'U') {
2454 $amount = $cust_bill_pkg->usage;
2458 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2461 if ( !$type || $type eq 'R' ) {
2463 warn "$me _items_cust_bill_pkg adding recur\n"
2466 if ( $cust_bill_pkg->hidden ) {
2467 $r->{amount} += $amount;
2468 $r->{unit_amount} += $unit_amount;
2469 push @{ $r->{ext_description} }, @d;
2472 description => $description,
2473 pkgpart => $pkgpart,
2474 pkgnum => $cust_bill_pkg->pkgnum,
2476 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2477 unit_amount => $unit_amount,
2478 quantity => $cust_bill_pkg->quantity,
2480 ext_description => \@d,
2481 svc_label => ($svc_label || ''),
2483 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2486 } else { # $type eq 'U'
2488 warn "$me _items_cust_bill_pkg adding usage\n"
2491 if ( $cust_bill_pkg->hidden ) {
2492 $u->{amount} += $amount;
2493 $u->{unit_amount} += $unit_amount,
2494 push @{ $u->{ext_description} }, @d;
2497 description => $description,
2498 pkgpart => $pkgpart,
2499 pkgnum => $cust_bill_pkg->pkgnum,
2501 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2502 unit_amount => $unit_amount,
2503 quantity => $cust_bill_pkg->quantity,
2505 ext_description => \@d,
2510 } # recurring or usage with recurring charge
2512 } else { #pkgnum tax or one-shot line item (??)
2514 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2517 if ( $cust_bill_pkg->setup != 0 ) {
2519 'description' => $desc,
2520 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2523 if ( $cust_bill_pkg->recur != 0 ) {
2525 'description' => "$desc (".
2526 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2527 time2str($date_format, $cust_bill_pkg->edate). ')',
2528 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2536 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2537 && $conf->exists('discount-show-always'));
2541 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2543 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2544 $_->{amount} =~ s/^\-0\.00$/0.00/;
2545 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2547 if $_->{amount} != 0
2548 || $discount_show_always
2549 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2550 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2554 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2561 =item _items_discounts_avail
2563 Returns an array of line item hashrefs representing available term discounts
2564 for this invoice. This makes the same assumptions that apply to term
2565 discounts in general: that the package is billed monthly, at a flat rate,
2566 with no usage charges. A prorated first month will be handled, as will
2567 a setup fee if the discount is allowed to apply to setup fees.
2571 sub _items_discounts_avail {
2574 #maybe move this method from cust_bill when quotations support discount_plans
2575 return () unless $self->can('discount_plans');
2576 my %plans = $self->discount_plans;
2578 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2579 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2583 my $plan = $plans{$months};
2585 my $term_total = sprintf('%.2f', $plan->discounted_total);
2586 my $percent = sprintf('%.0f',
2587 100 * (1 - $term_total / $plan->base_total) );
2588 my $permonth = sprintf('%.2f', $term_total / $months);
2589 my $detail = $self->mt('discount on item'). ' '.
2590 join(', ', map { "#$_" } $plan->pkgnums)
2593 # discounts for non-integer months don't work anyway
2594 $months = sprintf("%d", $months);
2597 description => $self->mt('Save [_1]% by paying for [_2] months',
2599 amount => $self->mt('[_1] ([_2] per month)',
2600 $term_total, $money_char.$permonth),
2601 ext_description => ($detail || ''),
2604 sort { $b <=> $a } keys %plans;