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 %newline_tokens = ( 'latex' => '\\\\',
372 my $newline_token = $newline_tokens{$format};
374 warn "$me generating template variables\n"
377 # generate template variables
380 defined( $conf->config_orbase( "invoice_${format}returnaddress",
384 && length( $conf->config_orbase( "invoice_${format}returnaddress",
390 $returnaddress = join("\n",
391 $conf->config_orbase("invoice_${format}returnaddress", $template)
395 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
397 my $convert_map = $convert_maps{$format}{'returnaddress'};
400 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
405 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
407 my $convert_map = $convert_maps{$format}{'returnaddress'};
408 $returnaddress = join( "\n", &$convert_map(
409 map { s/( {2,})/'~' x length($1)/eg;
413 ( $conf->config('company_name', $cust_main->agentnum),
414 $conf->config('company_address', $cust_main->agentnum),
421 my $warning = "Couldn't find a return address; ".
422 "do you need to set the company_address configuration value?";
424 $returnaddress = $nbsp;
425 #$returnaddress = $warning;
429 warn "$me generating invoice data\n"
432 my $agentnum = $cust_main->agentnum;
437 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
438 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
439 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
440 'returnaddress' => $returnaddress,
441 'agent' => &$escape_function($cust_main->agent->agent),
443 #invoice/quotation info
444 'invnum' => $self->invnum,
445 'quotationnum' => $self->quotationnum,
446 'date' => time2str($date_format, $self->_date),
447 'today' => time2str($date_format_long, $today),
448 'terms' => $self->terms,
449 'template' => $template, #params{'template'},
450 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
451 'current_charges' => sprintf("%.2f", $self->charged),
452 'duedate' => $self->due_date2str($rdate_format), #date_format?
455 'custnum' => $cust_main->display_custnum,
456 'prospectnum' => $cust_main->prospectnum,
457 'agent_custid' => &$escape_function($cust_main->agent_custid),
458 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
459 payname company address1 address2 city state zip fax
463 'ship_enable' => $conf->exists('invoice-ship_address'),
464 'unitprices' => $conf->exists('invoice-unitprice'),
465 'smallernotes' => $conf->exists('invoice-smallernotes'),
466 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
467 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
469 #layout info -- would be fancy to calc some of this and bury the template
471 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
472 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
473 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
474 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
475 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
476 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
477 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
478 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
479 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
480 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
482 # better hang on to conf_dir for a while (for old templates)
483 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
485 #these are only used when doing paged plaintext
492 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
493 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
494 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
495 # eval to avoid death for unimplemented languages
496 my $dh = eval { Date::Language->new($info{'name'}) } ||
497 Date::Language->new(); # fall back to English
498 # prototype here to silence warnings
499 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
500 # eventually use this date handle everywhere in here, too
502 my $min_sdate = 999999999999;
504 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
505 next unless $cust_bill_pkg->pkgnum > 0;
506 $min_sdate = $cust_bill_pkg->sdate
507 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
508 $max_edate = $cust_bill_pkg->edate
509 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
512 $invoice_data{'bill_period'} = '';
513 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
514 . " to " . time2str('%e %h', $max_edate)
515 if ($max_edate != 0 && $min_sdate != 999999999999);
517 $invoice_data{finance_section} = '';
518 if ( $conf->config('finance_pkgclass') ) {
520 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
521 $invoice_data{finance_section} = $pkg_class->categoryname;
523 $invoice_data{finance_amount} = '0.00';
524 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
526 my $countrydefault = $conf->config('countrydefault') || 'US';
527 foreach ( qw( address1 address2 city state zip country fax) ){
528 my $method = 'ship_'.$_;
529 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
531 foreach ( qw( contact company ) ) { #compatibility
532 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
534 $invoice_data{'ship_country'} = ''
535 if ( $invoice_data{'ship_country'} eq $countrydefault );
537 $invoice_data{'cid'} = $params{'cid'}
540 if ( $cust_main->country eq $countrydefault ) {
541 $invoice_data{'country'} = '';
543 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
547 $invoice_data{'address'} = \@address;
550 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
551 ? " (P.O. #". $cust_main->payinfo. ")"
555 push @address, $cust_main->company
556 if $cust_main->company;
557 push @address, $cust_main->address1;
558 push @address, $cust_main->address2
559 if $cust_main->address2;
561 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
562 push @address, $invoice_data{'country'}
563 if $invoice_data{'country'};
565 while (scalar(@address) < 5);
567 $invoice_data{'logo_file'} = $params{'logo_file'}
568 if $params{'logo_file'};
569 $invoice_data{'barcode_file'} = $params{'barcode_file'}
570 if $params{'barcode_file'};
571 $invoice_data{'barcode_img'} = $params{'barcode_img'}
572 if $params{'barcode_img'};
573 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
574 if $params{'barcode_cid'};
576 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
577 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
578 #my $balance_due = $self->owed + $pr_total - $cr_total;
579 my $balance_due = $self->owed + $pr_total;
581 #these are used on the summary page only
583 # the customer's current balance as shown on the invoice before this one
584 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
586 # the change in balance from that invoice to this one
587 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
589 # the sum of amount owed on all previous invoices
590 # ($pr_total is used elsewhere but not as $previous_balance)
591 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
593 # the sum of amount owed on all invoices
594 # (this is used in the summary & on the payment coupon)
595 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
597 # info from customer's last invoice before this one, for some
599 $invoice_data{'last_bill'} = {};
600 my $last_bill = $pr_cust_bill[-1];
602 $invoice_data{'last_bill'} = {
603 '_date' => $last_bill->_date, #unformatted
604 # all we need for now
608 my $summarypage = '';
609 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
612 $invoice_data{'summarypage'} = $summarypage;
614 warn "$me substituting variables in notes, footer, smallfooter\n"
617 my $tc = $self->template_conf;
618 my @include = ( [ $tc, 'notes' ],
619 [ 'invoice_', 'footer' ],
620 [ 'invoice_', 'smallfooter', ],
622 push @include, [ $tc, 'coupon', ]
623 unless $params{'no_coupon'};
625 foreach my $i (@include) {
627 my($base, $include) = @$i;
629 my $inc_file = $conf->key_orbase("$base$format$include", $template);
632 if ( $conf->exists($inc_file, $agentnum)
633 && length( $conf->config($inc_file, $agentnum) ) ) {
635 @inc_src = $conf->config($inc_file, $agentnum);
639 $inc_file = $conf->key_orbase("${base}latex$include", $template);
641 my $convert_map = $convert_maps{$format}{$include};
643 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
644 s/--\@\]/$delimiters{$format}[1]/g;
647 &$convert_map( $conf->config($inc_file, $agentnum) );
651 my $inc_tt = new Text::Template (
653 SOURCE => [ map "$_\n", @inc_src ],
654 DELIMITERS => $delimiters{$format},
655 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
657 unless ( $inc_tt->compile() ) {
658 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
659 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
663 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
665 $invoice_data{$include} =~ s/\n+$//
666 if ($format eq 'latex');
669 # let invoices use either of these as needed
670 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
671 ? $cust_main->payinfo : '';
672 $invoice_data{'po_line'} =
673 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
674 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
677 my %money_chars = ( 'latex' => '',
678 'html' => $conf->config('money_char') || '$',
681 my $money_char = $money_chars{$format};
683 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
684 'html' => $conf->config('money_char') || '$',
687 my $other_money_char = $other_money_chars{$format};
688 $invoice_data{'dollar'} = $other_money_char;
690 my @detail_items = ();
691 my @total_items = ();
695 $invoice_data{'detail_items'} = \@detail_items;
696 $invoice_data{'total_items'} = \@total_items;
697 $invoice_data{'buf'} = \@buf;
698 $invoice_data{'sections'} = \@sections;
700 warn "$me generating sections\n"
703 # Previous Charges section
704 # subtotal is the first return value from $self->previous
705 my $previous_section = { 'description' => $self->mt('Previous Charges'),
706 'subtotal' => $other_money_char.
707 sprintf('%.2f', $pr_total),
708 'summarized' => '', #why? $summarypage ? 'Y' : '',
710 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
711 join(' / ', map { $cust_main->balance_date_range(@$_) }
712 $self->_prior_month30s
714 if $conf->exists('invoice_include_aging');
717 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
718 'subtotal' => $taxtotal, # adjusted below
720 my $tax_weight = _pkg_category($tax_section->{description})
721 ? _pkg_category($tax_section->{description})->weight
723 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
724 $tax_section->{'sort_weight'} = $tax_weight;
728 my $adjust_section = {
729 'description' => $self->mt('Credits, Payments, and Adjustments'),
730 'adjust_section' => 1,
731 'subtotal' => 0, # adjusted below
733 my $adjust_weight = _pkg_category($adjust_section->{description})
734 ? _pkg_category($adjust_section->{description})->weight
736 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
737 $adjust_section->{'sort_weight'} = $adjust_weight;
739 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
740 my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum);
741 $invoice_data{'multisection'} = $multisection;
742 my $late_sections = [];
743 my $extra_sections = [];
744 my $extra_lines = ();
746 my $default_section = { 'description' => '',
751 if ( $multisection ) {
752 ($extra_sections, $extra_lines) =
753 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
754 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
755 && $self->can('_items_extra_usage_sections');
757 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
759 push @detail_items, @$extra_lines if $extra_lines;
761 $self->_items_sections( $late_sections, # this could stand a refactor
763 $escape_function_nonbsp,
767 if ( $conf->exists('svc_phone_sections')
768 && $self->can('_items_svc_phone_sections')
771 my ($phone_sections, $phone_lines) =
772 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
773 push @{$late_sections}, @$phone_sections;
774 push @detail_items, @$phone_lines;
776 if ( $conf->exists('voip-cust_accountcode_cdr')
777 && $cust_main->accountcode_cdr
778 && $self->can('_items_accountcode_cdr')
781 my ($accountcode_section, $accountcode_lines) =
782 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
783 if ( scalar(@$accountcode_lines) ) {
784 push @{$late_sections}, $accountcode_section;
785 push @detail_items, @$accountcode_lines;
788 } else {# not multisection
789 # make a default section
790 push @sections, $default_section;
791 # and calculate the finance charge total, since it won't get done otherwise.
792 # XXX possibly other totals?
793 # XXX possibly finance_pkgclass should not be used in this manner?
794 if ( $conf->exists('finance_pkgclass') ) {
796 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
797 if ( grep { $_->section eq $invoice_data{finance_section} }
798 $cust_bill_pkg->cust_bill_pkg_display ) {
799 # I think these are always setup fees, but just to be sure...
800 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
803 $invoice_data{finance_amount} =
804 sprintf('%.2f', sum( @finance_charges ) || 0);
808 # previous invoice balances in the Previous Charges section if there
809 # is one, otherwise in the main detail section
810 if ( $self->can('_items_previous') &&
811 $self->enable_previous &&
812 ! $conf->exists('previous_balance-summary_only') ) {
814 warn "$me adding previous balances\n"
817 foreach my $line_item ( $self->_items_previous ) {
820 ext_description => [],
822 $detail->{'ref'} = $line_item->{'pkgnum'};
823 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
824 $detail->{'quantity'} = 1;
825 $detail->{'section'} = $multisection ? $previous_section
827 $detail->{'description'} = &$escape_function($line_item->{'description'});
828 if ( exists $line_item->{'ext_description'} ) {
829 @{$detail->{'ext_description'}} = map {
830 &$escape_function($_);
831 } @{$line_item->{'ext_description'}};
833 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
834 $line_item->{'amount'};
835 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
837 push @detail_items, $detail;
838 push @buf, [ $detail->{'description'},
839 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
845 if ( @pr_cust_bill && $self->enable_previous ) {
846 push @buf, ['','-----------'];
847 push @buf, [ $self->mt('Total Previous Balance'),
848 $money_char. sprintf("%10.2f", $pr_total) ];
852 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
853 warn "$me adding DID summary\n"
856 my ($didsummary,$minutes) = $self->_did_summary;
857 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
859 { 'description' => $didsummary_desc,
860 'ext_description' => [ $didsummary, $minutes ],
864 foreach my $section (@sections, @$late_sections) {
866 warn "$me adding section \n". Dumper($section)
869 # begin some normalization
870 $section->{'subtotal'} = $section->{'amount'}
872 && !exists($section->{subtotal})
873 && exists($section->{amount});
875 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
876 if ( $invoice_data{finance_section} &&
877 $section->{'description'} eq $invoice_data{finance_section} );
879 $section->{'subtotal'} = $other_money_char.
880 sprintf('%.2f', $section->{'subtotal'})
883 # continue some normalization
884 $section->{'amount'} = $section->{'subtotal'}
888 if ( $section->{'description'} ) {
889 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
894 warn "$me setting options\n"
898 $options{'section'} = $section if $multisection;
899 $options{'format'} = $format;
900 $options{'escape_function'} = $escape_function;
901 $options{'no_usage'} = 1 unless $unsquelched;
902 $options{'unsquelched'} = $unsquelched;
903 $options{'summary_page'} = $summarypage;
904 $options{'skip_usage'} =
905 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
906 $options{'multisection'} = $multisection;
908 warn "$me searching for line items\n"
911 foreach my $line_item ( $self->_items_pkg(%options) ) {
913 warn "$me adding line item $line_item\n"
917 ext_description => [],
919 $detail->{'ref'} = $line_item->{'pkgnum'};
920 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
921 $detail->{'quantity'} = $line_item->{'quantity'};
922 $detail->{'section'} = $section;
923 $detail->{'description'} = &$escape_function($line_item->{'description'});
924 if ( exists $line_item->{'ext_description'} ) {
925 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
927 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
928 $line_item->{'amount'};
929 if ( exists $line_item->{'unit_amount'} ) {
930 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
931 $line_item->{'unit_amount'};
933 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
935 $detail->{'sdate'} = $line_item->{'sdate'};
936 $detail->{'edate'} = $line_item->{'edate'};
937 $detail->{'seconds'} = $line_item->{'seconds'};
938 $detail->{'svc_label'} = $line_item->{'svc_label'};
940 push @detail_items, $detail;
941 push @buf, ( [ $detail->{'description'},
942 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
944 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
948 if ( $section->{'description'} ) {
949 push @buf, ( ['','-----------'],
950 [ $section->{'description'}. ' sub-total',
951 $section->{'subtotal'} # already formatted this
960 $invoice_data{current_less_finance} =
961 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
963 # create a major section for previous balance if we have major sections,
964 # or if previous_section is in summary form
965 if ( ( $multisection && $self->enable_previous )
966 || $conf->exists('previous_balance-summary_only') )
968 unshift @sections, $previous_section if $pr_total;
971 warn "$me adding taxes\n"
974 foreach my $tax ( $self->_items_tax ) {
976 $taxtotal += $tax->{'amount'};
978 my $description = &$escape_function( $tax->{'description'} );
979 my $amount = sprintf( '%.2f', $tax->{'amount'} );
981 if ( $multisection ) {
983 my $money = $old_latex ? '' : $money_char;
984 push @detail_items, {
985 ext_description => [],
988 description => $description,
989 amount => $money. $amount,
991 section => $tax_section,
997 'total_item' => $description,
998 'total_amount' => $other_money_char. $amount,
1003 push @buf,[ $description,
1004 $money_char. $amount,
1011 $total->{'total_item'} = $self->mt('Sub-total');
1012 $total->{'total_amount'} =
1013 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1015 if ( $multisection ) {
1016 $tax_section->{'subtotal'} = $other_money_char.
1017 sprintf('%.2f', $taxtotal);
1018 $tax_section->{'pretotal'} = 'New charges sub-total '.
1019 $total->{'total_amount'};
1020 push @sections, $tax_section if $taxtotal;
1022 unshift @total_items, $total;
1025 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1027 push @buf,['','-----------'];
1028 push @buf,[$self->mt(
1029 (!$self->enable_previous)
1031 : 'Total New Charges'
1033 $money_char. sprintf("%10.2f",$self->charged) ];
1041 my %embolden_functions = (
1042 'latex' => sub { return '\textbf{'. shift(). '}' },
1043 'html' => sub { return '<b>'. shift(). '</b>' },
1044 'template' => sub { shift },
1046 my $embolden_function = $embolden_functions{$format};
1048 if ( $self->can('_items_total') ) { # quotations
1050 $self->_items_total(\@total_items);
1052 foreach ( @total_items ) {
1053 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1054 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1055 $_->{'total_amount'}
1059 } else { #normal invoice case
1061 # calculate total, possibly including total owed on previous
1065 $item = $conf->config('previous_balance-exclude_from_total')
1066 || 'Total New Charges'
1067 if $conf->exists('previous_balance-exclude_from_total');
1068 my $amount = $self->charged;
1069 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1070 $amount += $pr_total;
1073 $total->{'total_item'} = &$embolden_function($self->mt($item));
1074 $total->{'total_amount'} =
1075 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1076 if ( $multisection ) {
1077 if ( $adjust_section->{'sort_weight'} ) {
1078 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1079 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1081 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1082 $other_money_char. sprintf('%.2f', $self->charged );
1085 push @total_items, $total;
1087 push @buf,['','-----------'];
1090 sprintf( '%10.2f', $amount )
1094 # if we're showing previous invoices, also show previous
1095 # credits and payments
1096 if ( $self->enable_previous
1097 and $self->can('_items_credits')
1098 and $self->can('_items_payments') )
1100 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1103 my $credittotal = 0;
1104 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1107 $total->{'total_item'} = &$escape_function($credit->{'description'});
1108 $credittotal += $credit->{'amount'};
1109 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1110 $adjusttotal += $credit->{'amount'};
1111 if ( $multisection ) {
1112 my $money = $old_latex ? '' : $money_char;
1113 push @detail_items, {
1114 ext_description => [],
1117 description => &$escape_function($credit->{'description'}),
1118 amount => $money. $credit->{'amount'},
1120 section => $adjust_section,
1123 push @total_items, $total;
1127 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1130 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1131 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1135 my $paymenttotal = 0;
1136 foreach my $payment ( $self->_items_payments ) {
1138 $total->{'total_item'} = &$escape_function($payment->{'description'});
1139 $paymenttotal += $payment->{'amount'};
1140 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1141 $adjusttotal += $payment->{'amount'};
1142 if ( $multisection ) {
1143 my $money = $old_latex ? '' : $money_char;
1144 push @detail_items, {
1145 ext_description => [],
1148 description => &$escape_function($payment->{'description'}),
1149 amount => $money. $payment->{'amount'},
1151 section => $adjust_section,
1154 push @total_items, $total;
1156 push @buf, [ $payment->{'description'},
1157 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1160 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1162 if ( $multisection ) {
1163 $adjust_section->{'subtotal'} = $other_money_char.
1164 sprintf('%.2f', $adjusttotal);
1165 push @sections, $adjust_section
1166 unless $adjust_section->{sort_weight};
1169 # create Balance Due message
1172 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1173 $total->{'total_amount'} =
1174 &$embolden_function(
1175 $other_money_char. sprintf('%.2f', #why? $summarypage
1176 # ? $self->charged +
1177 # $self->billing_balance
1179 $self->owed + $pr_total
1182 if ( $multisection && !$adjust_section->{sort_weight} ) {
1183 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1184 $total->{'total_amount'};
1186 push @total_items, $total;
1188 push @buf,['','-----------'];
1189 push @buf,[$self->balance_due_msg, $money_char.
1190 sprintf("%10.2f", $balance_due ) ];
1193 if ( $conf->exists('previous_balance-show_credit')
1194 and $cust_main->balance < 0 ) {
1195 my $credit_total = {
1196 'total_item' => &$embolden_function($self->credit_balance_msg),
1197 'total_amount' => &$embolden_function(
1198 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1201 if ( $multisection ) {
1202 $adjust_section->{'posttotal'} .= $newline_token .
1203 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1206 push @total_items, $credit_total;
1208 push @buf,['','-----------'];
1209 push @buf,[$self->credit_balance_msg, $money_char.
1210 sprintf("%10.2f", -$cust_main->balance ) ];
1214 } #end of default total adding ! can('_items_total')
1216 if ( $multisection ) {
1217 if ( $conf->exists('svc_phone_sections')
1218 && $self->can('_items_svc_phone_sections')
1222 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1223 $total->{'total_amount'} =
1224 &$embolden_function(
1225 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1227 my $last_section = pop @sections;
1228 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1229 $total->{'total_amount'};
1230 push @sections, $last_section;
1232 push @sections, @$late_sections
1236 # make a discounts-available section, even without multisection
1237 if ( $conf->exists('discount-show_available')
1238 and my @discounts_avail = $self->_items_discounts_avail ) {
1239 my $discount_section = {
1240 'description' => $self->mt('Discounts Available'),
1245 push @sections, $discount_section;
1246 push @detail_items, map { +{
1247 'ref' => '', #should this be something else?
1248 'section' => $discount_section,
1249 'description' => &$escape_function( $_->{description} ),
1250 'amount' => $money_char . &$escape_function( $_->{amount} ),
1251 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1252 } } @discounts_avail;
1255 # debugging hook: call this with 'diag' => 1 to just get a hash of
1256 # the invoice variables
1257 return \%invoice_data if ( $params{'diag'} );
1259 # All sections and items are built; now fill in templates.
1260 my @includelist = ();
1261 push @includelist, 'summary' if $summarypage;
1262 foreach my $include ( @includelist ) {
1264 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1267 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1269 @inc_src = $conf->config($inc_file, $agentnum);
1273 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1275 my $convert_map = $convert_maps{$format}{$include};
1277 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1278 s/--\@\]/$delimiters{$format}[1]/g;
1281 &$convert_map( $conf->config($inc_file, $agentnum) );
1285 my $inc_tt = new Text::Template (
1287 SOURCE => [ map "$_\n", @inc_src ],
1288 DELIMITERS => $delimiters{$format},
1289 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1291 unless ( $inc_tt->compile() ) {
1292 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1293 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1297 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1299 $invoice_data{$include} =~ s/\n+$//
1300 if ($format eq 'latex');
1305 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1306 /invoice_lines\((\d*)\)/;
1307 $invoice_lines += $1 || scalar(@buf);
1310 die "no invoice_lines() functions in template?"
1311 if ( $format eq 'template' && !$wasfunc );
1313 if ($format eq 'template') {
1315 if ( $invoice_lines ) {
1316 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1317 $invoice_data{'total_pages'}++
1318 if scalar(@buf) % $invoice_lines;
1321 #setup subroutine for the template
1322 $invoice_data{invoice_lines} = sub {
1323 my $lines = shift || scalar(@buf);
1335 push @collect, split("\n",
1336 $text_template->fill_in( HASH => \%invoice_data )
1338 $invoice_data{'page'}++;
1340 map "$_\n", @collect;
1342 } else { # this is where we actually create the invoice
1344 warn "filling in template for invoice ". $self->invnum. "\n"
1346 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1349 $text_template->fill_in(HASH => \%invoice_data);
1353 sub notice_name { '('.shift->table.')'; }
1355 sub template_conf { 'invoice_'; }
1357 # helper routine for generating date ranges
1358 sub _prior_month30s {
1361 [ 1, 2592000 ], # 0-30 days ago
1362 [ 2592000, 5184000 ], # 30-60 days ago
1363 [ 5184000, 7776000 ], # 60-90 days ago
1364 [ 7776000, 0 ], # 90+ days ago
1367 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1368 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1373 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1375 Returns an postscript invoice, as a scalar.
1377 Options can be passed as a hashref (recommended) or as a list of time, template
1378 and then any key/value pairs for any other options.
1380 I<time> an optional value used to control the printing of overdue messages. The
1381 default is now. It isn't the date of the invoice; that's the `_date' field.
1382 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1383 L<Time::Local> and L<Date::Parse> for conversion functions.
1385 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1392 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1393 my $ps = generate_ps($file);
1395 unlink($barcodefile) if $barcodefile;
1400 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1402 Returns an PDF invoice, as a scalar.
1404 Options can be passed as a hashref (recommended) or as a list of time, template
1405 and then any key/value pairs for any other options.
1407 I<time> an optional value used to control the printing of overdue messages. The
1408 default is now. It isn't the date of the invoice; that's the `_date' field.
1409 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1410 L<Time::Local> and L<Date::Parse> for conversion functions.
1412 I<template>, if specified, is the name of a suffix for alternate invoices.
1414 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1421 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1422 my $pdf = generate_pdf($file);
1424 unlink($barcodefile) if $barcodefile;
1429 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1431 Returns an HTML invoice, as a scalar.
1433 I<time> an optional value used to control the printing of overdue messages. The
1434 default is now. It isn't the date of the invoice; that's the `_date' field.
1435 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1436 L<Time::Local> and L<Date::Parse> for conversion functions.
1438 I<template>, if specified, is the name of a suffix for alternate invoices.
1440 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1442 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1443 when emailing the invoice as part of a multipart/related MIME email.
1451 %params = %{ shift() };
1453 $params{'time'} = shift;
1454 $params{'template'} = shift;
1455 $params{'cid'} = shift;
1458 $params{'format'} = 'html';
1460 $self->print_generic( %params );
1463 # quick subroutine for print_latex
1465 # There are ten characters that LaTeX treats as special characters, which
1466 # means that they do not simply typeset themselves:
1467 # # $ % & ~ _ ^ \ { }
1469 # TeX ignores blanks following an escaped character; if you want a blank (as
1470 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1474 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1475 $value =~ s/([<>])/\$$1\$/g;
1481 encode_entities($value);
1485 sub _html_escape_nbsp {
1486 my $value = _html_escape(shift);
1487 $value =~ s/ +/ /g;
1491 #utility methods for print_*
1493 sub _translate_old_latex_format {
1494 warn "_translate_old_latex_format called\n"
1501 if ( $line =~ /^%%Detail\s*$/ ) {
1503 push @template, q![@--!,
1504 q! foreach my $_tr_line (@detail_items) {!,
1505 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1506 q! $_tr_line->{'description'} .= !,
1507 q! "\\tabularnewline\n~~".!,
1508 q! join( "\\tabularnewline\n~~",!,
1509 q! @{$_tr_line->{'ext_description'}}!,
1513 while ( ( my $line_item_line = shift )
1514 !~ /^%%EndDetail\s*$/ ) {
1515 $line_item_line =~ s/'/\\'/g; # nice LTS
1516 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1517 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1518 push @template, " \$OUT .= '$line_item_line';";
1521 push @template, '}',
1524 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1526 push @template, '[@--',
1527 ' foreach my $_tr_line (@total_items) {';
1529 while ( ( my $total_item_line = shift )
1530 !~ /^%%EndTotalDetails\s*$/ ) {
1531 $total_item_line =~ s/'/\\'/g; # nice LTS
1532 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1533 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1534 push @template, " \$OUT .= '$total_item_line';";
1537 push @template, '}',
1541 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1542 push @template, $line;
1548 warn "$_\n" foreach @template;
1556 my $conf = $self->conf;
1558 #check for an invoice-specific override
1559 return $self->invoice_terms if $self->invoice_terms;
1561 #check for a customer- specific override
1562 my $cust_main = $self->cust_main;
1563 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1565 #use configured default
1566 $conf->config('invoice_default_terms') || '';
1572 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1573 $duedate = $self->_date() + ( $1 * 86400 );
1580 $self->due_date ? time2str(shift, $self->due_date) : '';
1583 sub balance_due_msg {
1585 my $msg = $self->mt('Balance Due');
1586 return $msg unless $self->terms;
1587 if ( $self->due_date ) {
1588 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1589 $self->due_date2str($date_format);
1590 } elsif ( $self->terms ) {
1591 $msg .= ' - '. $self->terms;
1596 sub balance_due_date {
1598 my $conf = $self->conf;
1600 if ( $conf->exists('invoice_default_terms')
1601 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1602 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1607 sub credit_balance_msg {
1609 $self->mt('Credit Balance Remaining')
1614 Returns a string with the date, for example: "3/20/2008"
1620 time2str($date_format, $self->_date);
1623 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1625 Generate section information for all items appearing on this invoice.
1626 This will only be called for multi-section invoices.
1628 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1629 related display records (L<FS::cust_bill_pkg_display>) and organize
1630 them into two groups ("early" and "late" according to whether they come
1631 before or after the total), then into sections. A subtotal is calculated
1634 Section descriptions are returned in sort weight order. Each consists
1635 of a hash containing:
1637 description: the package category name, escaped
1638 subtotal: the total charges in that section
1639 tax_section: a flag indicating that the section contains only tax charges
1640 summarized: same as tax_section, for some reason
1641 sort_weight: the package category's sort weight
1643 If 'condense' is set on the display record, it also contains everything
1644 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1645 coderefs to generate parts of the invoice. This is not advised.
1649 LATE: an arrayref to push the "late" section hashes onto. The "early"
1650 group is simply returned from the method.
1652 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1653 Turning this on has the following effects:
1654 - Ignores display items with the 'summary' flag.
1655 - Combines all items into the "early" group.
1656 - Creates sections for all non-disabled package categories, even if they
1657 have no charges on this invoice, as well as a section with no name.
1659 ESCAPE: an escape function to use for section titles.
1661 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1662 sorted list. If there are any of these, section subtotals exclude
1665 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1666 passed through to C<_condense_section()>.
1670 use vars qw(%pkg_category_cache);
1671 sub _items_sections {
1674 my $summarypage = shift;
1676 my $extra_sections = shift;
1680 my %late_subtotal = ();
1683 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1686 my $usage = $cust_bill_pkg->usage;
1688 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1689 next if ( $display->summary && $summarypage );
1691 my $section = $display->section;
1692 my $type = $display->type;
1694 $not_tax{$section} = 1
1695 unless $cust_bill_pkg->pkgnum == 0;
1697 # there's actually a very important piece of logic buried in here:
1698 # incrementing $late_subtotal{$section} CREATES
1699 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1700 # to define the list of late sections, and likewise keys(%subtotal).
1701 # When _items_cust_bill_pkg is called to generate line items for
1702 # real, it will be called with 'section' => $section for each
1704 if ( $display->post_total && !$summarypage ) {
1705 if (! $type || $type eq 'S') {
1706 $late_subtotal{$section} += $cust_bill_pkg->setup
1707 if $cust_bill_pkg->setup != 0
1708 || $cust_bill_pkg->setup_show_zero;
1712 $late_subtotal{$section} += $cust_bill_pkg->recur
1713 if $cust_bill_pkg->recur != 0
1714 || $cust_bill_pkg->recur_show_zero;
1717 if ($type && $type eq 'R') {
1718 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1719 if $cust_bill_pkg->recur != 0
1720 || $cust_bill_pkg->recur_show_zero;
1723 if ($type && $type eq 'U') {
1724 $late_subtotal{$section} += $usage
1725 unless scalar(@$extra_sections);
1730 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1732 if (! $type || $type eq 'S') {
1733 $subtotal{$section} += $cust_bill_pkg->setup
1734 if $cust_bill_pkg->setup != 0
1735 || $cust_bill_pkg->setup_show_zero;
1739 $subtotal{$section} += $cust_bill_pkg->recur
1740 if $cust_bill_pkg->recur != 0
1741 || $cust_bill_pkg->recur_show_zero;
1744 if ($type && $type eq 'R') {
1745 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1746 if $cust_bill_pkg->recur != 0
1747 || $cust_bill_pkg->recur_show_zero;
1750 if ($type && $type eq 'U') {
1751 $subtotal{$section} += $usage
1752 unless scalar(@$extra_sections);
1761 %pkg_category_cache = ();
1763 push @$late, map { { 'description' => &{$escape}($_),
1764 'subtotal' => $late_subtotal{$_},
1766 'sort_weight' => ( _pkg_category($_)
1767 ? _pkg_category($_)->weight
1770 ((_pkg_category($_) && _pkg_category($_)->condense)
1771 ? $self->_condense_section($format)
1775 sort _sectionsort keys %late_subtotal;
1778 if ( $summarypage ) {
1779 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1780 map { $_->categoryname } qsearch('pkg_category', {});
1781 push @sections, '' if exists($subtotal{''});
1783 @sections = keys %subtotal;
1786 my @early = map { { 'description' => &{$escape}($_),
1787 'subtotal' => $subtotal{$_},
1788 'summarized' => $not_tax{$_} ? '' : 'Y',
1789 'tax_section' => $not_tax{$_} ? '' : 'Y',
1790 'sort_weight' => ( _pkg_category($_)
1791 ? _pkg_category($_)->weight
1794 ((_pkg_category($_) && _pkg_category($_)->condense)
1795 ? $self->_condense_section($format)
1800 push @early, @$extra_sections if $extra_sections;
1802 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1806 #helper subs for above
1809 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1813 my $categoryname = shift;
1814 $pkg_category_cache{$categoryname} ||=
1815 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1818 my %condensed_format = (
1819 'label' => [ qw( Description Qty Amount ) ],
1821 sub { shift->{description} },
1822 sub { shift->{quantity} },
1823 sub { my($href, %opt) = @_;
1824 ($opt{dollar} || ''). $href->{amount};
1827 'align' => [ qw( l r r ) ],
1828 'span' => [ qw( 5 1 1 ) ], # unitprices?
1829 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1832 sub _condense_section {
1833 my ( $self, $format ) = ( shift, shift );
1835 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1836 qw( description_generator
1839 total_line_generator
1844 sub _condensed_generator_defaults {
1845 my ( $self, $format ) = ( shift, shift );
1846 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1855 sub _condensed_header_generator {
1856 my ( $self, $format ) = ( shift, shift );
1858 my ( $f, $prefix, $suffix, $separator, $column ) =
1859 _condensed_generator_defaults($format);
1861 if ($format eq 'latex') {
1862 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1863 $suffix = "\\\\\n\\hline";
1866 sub { my ($d,$a,$s,$w) = @_;
1867 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1869 } elsif ( $format eq 'html' ) {
1870 $prefix = '<th></th>';
1874 sub { my ($d,$a,$s,$w) = @_;
1875 return qq!<th align="$html_align{$a}">$d</th>!;
1883 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1885 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1888 $prefix. join($separator, @result). $suffix;
1893 sub _condensed_description_generator {
1894 my ( $self, $format ) = ( shift, shift );
1896 my ( $f, $prefix, $suffix, $separator, $column ) =
1897 _condensed_generator_defaults($format);
1899 my $money_char = '$';
1900 if ($format eq 'latex') {
1901 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1903 $separator = " & \n";
1905 sub { my ($d,$a,$s,$w) = @_;
1906 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1908 $money_char = '\\dollar';
1909 }elsif ( $format eq 'html' ) {
1910 $prefix = '"><td align="center"></td>';
1914 sub { my ($d,$a,$s,$w) = @_;
1915 return qq!<td align="$html_align{$a}">$d</td>!;
1917 #$money_char = $conf->config('money_char') || '$';
1918 $money_char = ''; # this is madness
1926 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1928 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1930 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1931 map { $f->{$_}->[$i] } qw(align span width)
1935 $prefix. join( $separator, @result ). $suffix;
1940 sub _condensed_total_generator {
1941 my ( $self, $format ) = ( shift, shift );
1943 my ( $f, $prefix, $suffix, $separator, $column ) =
1944 _condensed_generator_defaults($format);
1947 if ($format eq 'latex') {
1950 $separator = " & \n";
1952 sub { my ($d,$a,$s,$w) = @_;
1953 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1955 }elsif ( $format eq 'html' ) {
1959 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1961 sub { my ($d,$a,$s,$w) = @_;
1962 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1971 # my $r = &{$f->{fields}->[$i]}(@args);
1972 # $r .= ' Total' unless $i;
1974 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1976 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1977 map { $f->{$_}->[$i] } qw(align span width)
1981 $prefix. join( $separator, @result ). $suffix;
1986 =item total_line_generator FORMAT
1988 Returns a coderef used for generation of invoice total line items for this
1989 usage_class. FORMAT is either html or latex
1993 # should not be used: will have issues with hash element names (description vs
1994 # total_item and amount vs total_amount -- another array of functions?
1996 sub _condensed_total_line_generator {
1997 my ( $self, $format ) = ( shift, shift );
1999 my ( $f, $prefix, $suffix, $separator, $column ) =
2000 _condensed_generator_defaults($format);
2003 if ($format eq 'latex') {
2006 $separator = " & \n";
2008 sub { my ($d,$a,$s,$w) = @_;
2009 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2011 }elsif ( $format eq 'html' ) {
2015 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2017 sub { my ($d,$a,$s,$w) = @_;
2018 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2027 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2029 &{$column}( &{$f->{fields}->[$i]}(@args),
2030 map { $f->{$_}->[$i] } qw(align span width)
2034 $prefix. join( $separator, @result ). $suffix;
2039 # sub _items { # seems to be unused
2042 # #my @display = scalar(@_)
2044 # # : qw( _items_previous _items_pkg );
2045 # # #: qw( _items_pkg );
2046 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2047 # my @display = qw( _items_previous _items_pkg );
2050 # foreach my $display ( @display ) {
2051 # push @b, $self->$display(@_);
2056 =item _items_pkg [ OPTIONS ]
2058 Return line item hashes for each package item on this invoice. Nearly
2061 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2063 The only OPTIONS accepted is 'section', which may point to a hashref
2064 with a key named 'condensed', which may have a true value. If it
2065 does, this method tries to merge identical items into items with
2066 'quantity' equal to the number of items (not the sum of their
2067 separate quantities, for some reason).
2073 grep { $_->pkgnum } $self->cust_bill_pkg;
2080 warn "$me _items_pkg searching for all package line items\n"
2083 my @cust_bill_pkg = $self->_items_nontax;
2085 warn "$me _items_pkg filtering line items\n"
2087 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2089 if ($options{section} && $options{section}->{condensed}) {
2091 warn "$me _items_pkg condensing section\n"
2095 local $Storable::canonical = 1;
2096 foreach ( @items ) {
2098 delete $item->{ref};
2099 delete $item->{ext_description};
2100 my $key = freeze($item);
2101 $itemshash{$key} ||= 0;
2102 $itemshash{$key} ++; # += $item->{quantity};
2104 @items = sort { $a->{description} cmp $b->{description} }
2105 map { my $i = thaw($_);
2106 $i->{quantity} = $itemshash{$_};
2108 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2114 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2121 return 0 unless $a->itemdesc cmp $b->itemdesc;
2122 return -1 if $b->itemdesc eq 'Tax';
2123 return 1 if $a->itemdesc eq 'Tax';
2124 return -1 if $b->itemdesc eq 'Other surcharges';
2125 return 1 if $a->itemdesc eq 'Other surcharges';
2126 $a->itemdesc cmp $b->itemdesc;
2131 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2132 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2135 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2137 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2138 list of hashrefs describing the line items they generate on the invoice.
2140 OPTIONS may include:
2142 format: the invoice format.
2144 escape_function: the function used to escape strings.
2146 DEPRECATED? (expensive, mostly unused?)
2147 format_function: the function used to format CDRs.
2149 section: a hashref containing 'description'; if this is present,
2150 cust_bill_pkg_display records not belonging to this section are
2153 multisection: a flag indicating that this is a multisection invoice,
2154 which does something complicated.
2156 Returns a list of hashrefs, each of which may contain:
2158 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2159 ext_description, which is an arrayref of detail lines to show below
2164 sub _items_cust_bill_pkg {
2166 my $conf = $self->conf;
2167 my $cust_bill_pkgs = shift;
2170 my $format = $opt{format} || '';
2171 my $escape_function = $opt{escape_function} || sub { shift };
2172 my $format_function = $opt{format_function} || '';
2173 my $no_usage = $opt{no_usage} || '';
2174 my $unsquelched = $opt{unsquelched} || ''; #unused
2175 my $section = $opt{section}->{description} if $opt{section};
2176 my $summary_page = $opt{summary_page} || ''; #unused
2177 my $multisection = $opt{multisection} || '';
2178 my $discount_show_always = 0;
2180 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2182 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2183 # and location labels
2184 my $locale = $cust_main->locale;
2187 my ($s, $r, $u) = ( undef, undef, undef );
2188 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2191 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2192 if ( $_ && !$cust_bill_pkg->hidden ) {
2193 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2194 $_->{amount} =~ s/^\-0\.00$/0.00/;
2195 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2197 if $_->{amount} != 0
2198 || $discount_show_always
2199 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2200 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2206 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2207 ? $cust_bill_pkg->cust_bill_pkg_display
2208 : ( $cust_bill_pkg );
2210 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2211 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2214 foreach my $display ( grep { defined($section)
2215 ? $_->section eq $section
2218 grep { !$_->summary || $multisection }
2219 @cust_bill_pkg_display
2223 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2224 $display->billpkgdisplaynum. "\n"
2227 my $type = $display->type;
2229 my $desc = $cust_bill_pkg->desc( $cust_main->locale );
2230 $desc = substr($desc, 0, $maxlength). '...'
2231 if $format eq 'latex' && length($desc) > $maxlength;
2233 my %details_opt = ( 'format' => $format,
2234 'escape_function' => $escape_function,
2235 'format_function' => $format_function,
2236 'no_usage' => $opt{'no_usage'},
2239 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2241 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2244 if ( $cust_bill_pkg->setup != 0 ) {
2245 my $description = $desc;
2246 $description .= ' Setup'
2247 if $cust_bill_pkg->recur != 0
2248 || $discount_show_always
2249 || $cust_bill_pkg->recur_show_zero;
2251 'description' => $description,
2252 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2255 if ( $cust_bill_pkg->recur != 0 ) {
2257 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2258 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2262 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2264 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2267 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2269 # which pkgpart to show for display purposes?
2270 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2272 # start/end dates for invoice formats that do nonstandard
2274 my %item_dates = ();
2275 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2276 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2278 if ( (!$type || $type eq 'S')
2279 && ( $cust_bill_pkg->setup != 0
2280 || $cust_bill_pkg->setup_show_zero
2285 warn "$me _items_cust_bill_pkg adding setup\n"
2288 my $description = $desc;
2289 $description .= ' Setup'
2290 if $cust_bill_pkg->recur != 0
2291 || $discount_show_always
2292 || $cust_bill_pkg->recur_show_zero;
2296 unless ( $cust_pkg->part_pkg->hide_svc_detail
2297 || $cust_bill_pkg->hidden )
2300 my @svc_labels = map &{$escape_function}($_),
2301 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2302 push @d, @svc_labels
2303 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2304 $svc_label = $svc_labels[0];
2306 if ( ! $cust_pkg->locationnum or
2307 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2308 my $loc = $cust_pkg->location_label;
2309 $loc = substr($loc, 0, $maxlength). '...'
2310 if $format eq 'latex' && length($loc) > $maxlength;
2311 push @d, &{$escape_function}($loc);
2314 } #unless hiding service details
2316 push @d, $cust_bill_pkg->details(%details_opt)
2317 if $cust_bill_pkg->recur == 0;
2319 if ( $cust_bill_pkg->hidden ) {
2320 $s->{amount} += $cust_bill_pkg->setup;
2321 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2322 push @{ $s->{ext_description} }, @d;
2326 description => $description,
2327 pkgpart => $pkgpart,
2328 pkgnum => $cust_bill_pkg->pkgnum,
2329 amount => $cust_bill_pkg->setup,
2330 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2331 unit_amount => $cust_bill_pkg->unitsetup,
2332 quantity => $cust_bill_pkg->quantity,
2333 ext_description => \@d,
2334 svc_label => ($svc_label || ''),
2340 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2342 $cust_bill_pkg->recur != 0
2343 || $cust_bill_pkg->setup == 0
2344 || $discount_show_always
2345 || $cust_bill_pkg->recur_show_zero
2350 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2353 my $is_summary = $display->summary;
2354 my $description = ($is_summary && $type && $type eq 'U')
2355 ? "Usage charges" : $desc;
2357 my $part_pkg = $cust_pkg->part_pkg;
2359 #pry be a bit more efficient to look some of this conf stuff up
2362 $conf->exists('disable_line_item_date_ranges')
2363 || $part_pkg->option('disable_line_item_date_ranges',1)
2364 || ! $cust_bill_pkg->sdate
2365 || ! $cust_bill_pkg->edate
2368 my $date_style = '';
2369 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2370 $cust_main->agentnum
2372 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2373 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2374 $cust_main->agentnum
2376 if ( defined($date_style) && $date_style eq 'month_of' ) {
2377 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2378 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2379 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2380 $cust_main->agentnum
2382 $desc .= ' ' unless $desc =~ /\s$/;
2383 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2385 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2386 " - ". time2str($date_format, $cust_bill_pkg->edate);
2388 $description .= " ($time_period)";
2392 my @seconds = (); # for display of usage info
2395 #at least until cust_bill_pkg has "past" ranges in addition to
2396 #the "future" sdate/edate ones... see #3032
2397 my @dates = ( $self->_date );
2398 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2399 push @dates, $prev->sdate if $prev;
2400 push @dates, undef if !$prev;
2402 unless ( $part_pkg->hide_svc_detail
2403 || $cust_bill_pkg->itemdesc
2404 || $cust_bill_pkg->hidden
2405 || $is_summary && $type && $type eq 'U'
2409 warn "$me _items_cust_bill_pkg adding service details\n"
2412 my @svc_labels = map &{$escape_function}($_),
2413 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2414 push @d, @svc_labels
2415 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2416 $svc_label = $svc_labels[0];
2418 warn "$me _items_cust_bill_pkg done adding service details\n"
2421 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2422 my $loc = $cust_pkg->location_label;
2423 $loc = substr($loc, 0, $maxlength). '...'
2424 if $format eq 'latex' && length($loc) > $maxlength;
2425 push @d, &{$escape_function}($loc);
2428 # Display of seconds_since_sqlradacct:
2429 # On the invoice, when processing @detail_items, look for a field
2430 # named 'seconds'. This will contain total seconds for each
2431 # service, in the same order as @ext_description. For services
2432 # that don't support this it will show undef.
2433 if ( $conf->exists('svc_acct-usage_seconds')
2434 and ! $cust_bill_pkg->pkgpart_override ) {
2435 foreach my $cust_svc (
2436 $cust_pkg->h_cust_svc(@dates, 'I')
2439 # eval because not having any part_export_usage exports
2440 # is a fatal error, last_bill/_date because that's how
2441 # sqlradius_hour billing does it
2443 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2445 push @seconds, $sec;
2447 } #if svc_acct-usage_seconds
2451 unless ( $is_summary ) {
2452 warn "$me _items_cust_bill_pkg adding details\n"
2455 #instead of omitting details entirely in this case (unwanted side
2456 # effects), just omit CDRs
2457 $details_opt{'no_usage'} = 1
2458 if $type && $type eq 'R';
2460 push @d, $cust_bill_pkg->details(%details_opt);
2463 warn "$me _items_cust_bill_pkg calculating amount\n"
2468 $amount = $cust_bill_pkg->recur;
2469 } elsif ($type eq 'R') {
2470 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2471 } elsif ($type eq 'U') {
2472 $amount = $cust_bill_pkg->usage;
2476 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2479 if ( !$type || $type eq 'R' ) {
2481 warn "$me _items_cust_bill_pkg adding recur\n"
2484 if ( $cust_bill_pkg->hidden ) {
2485 $r->{amount} += $amount;
2486 $r->{unit_amount} += $unit_amount;
2487 push @{ $r->{ext_description} }, @d;
2490 description => $description,
2491 pkgpart => $pkgpart,
2492 pkgnum => $cust_bill_pkg->pkgnum,
2494 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2495 unit_amount => $unit_amount,
2496 quantity => $cust_bill_pkg->quantity,
2498 ext_description => \@d,
2499 svc_label => ($svc_label || ''),
2501 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2504 } else { # $type eq 'U'
2506 warn "$me _items_cust_bill_pkg adding usage\n"
2509 if ( $cust_bill_pkg->hidden ) {
2510 $u->{amount} += $amount;
2511 $u->{unit_amount} += $unit_amount,
2512 push @{ $u->{ext_description} }, @d;
2515 description => $description,
2516 pkgpart => $pkgpart,
2517 pkgnum => $cust_bill_pkg->pkgnum,
2519 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2520 unit_amount => $unit_amount,
2521 quantity => $cust_bill_pkg->quantity,
2523 ext_description => \@d,
2528 } # recurring or usage with recurring charge
2530 } else { #pkgnum tax or one-shot line item (??)
2532 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2535 if ( $cust_bill_pkg->setup != 0 ) {
2537 'description' => $desc,
2538 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2541 if ( $cust_bill_pkg->recur != 0 ) {
2543 'description' => "$desc (".
2544 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2545 time2str($date_format, $cust_bill_pkg->edate). ')',
2546 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2554 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2555 && $conf->exists('discount-show-always'));
2559 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2561 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2562 $_->{amount} =~ s/^\-0\.00$/0.00/;
2563 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2565 if $_->{amount} != 0
2566 || $discount_show_always
2567 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2568 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2572 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2579 =item _items_discounts_avail
2581 Returns an array of line item hashrefs representing available term discounts
2582 for this invoice. This makes the same assumptions that apply to term
2583 discounts in general: that the package is billed monthly, at a flat rate,
2584 with no usage charges. A prorated first month will be handled, as will
2585 a setup fee if the discount is allowed to apply to setup fees.
2589 sub _items_discounts_avail {
2592 #maybe move this method from cust_bill when quotations support discount_plans
2593 return () unless $self->can('discount_plans');
2594 my %plans = $self->discount_plans;
2596 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2597 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2601 my $plan = $plans{$months};
2603 my $term_total = sprintf('%.2f', $plan->discounted_total);
2604 my $percent = sprintf('%.0f',
2605 100 * (1 - $term_total / $plan->base_total) );
2606 my $permonth = sprintf('%.2f', $term_total / $months);
2607 my $detail = $self->mt('discount on item'). ' '.
2608 join(', ', map { "#$_" } $plan->pkgnums)
2611 # discounts for non-integer months don't work anyway
2612 $months = sprintf("%d", $months);
2615 description => $self->mt('Save [_1]% by paying for [_2] months',
2617 amount => $self->mt('[_1] ([_2] per month)',
2618 $term_total, $money_char.$permonth),
2619 ext_description => ($detail || ''),
2622 sort { $b <=> $a } keys %plans;