1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
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 );
19 use FS::Misc qw( generate_ps generate_pdf );
26 $me = '[FS::Template_Mixin]';
27 FS::UID->install_callback( sub {
28 my $conf = new FS::Conf; #global
29 $money_char = $conf->config('money_char') || '$';
34 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
36 If the "mode" pseudo-field is set on the object, the configuration handle
37 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
44 my $mode = $self->get('mode');
45 if ($self->{_conf} and !defined($mode)) {
46 return $self->{_conf};
49 my $cust_main = $self->cust_main;
50 my $locale = $cust_main ? $cust_main->locale : '';
53 if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
54 $mode = $mode->modenum;
55 } elsif ( $mode =~ /\D/ ) {
56 die "invalid invoice mode $mode";
58 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
60 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
61 # it doesn't have a locale, but system conf still might
62 $conf->set('locale' => $locale) if $conf;
65 # if $mode is unspecified, or if there is no invoice_conf matching this mode
66 # and locale, then use the system config only (but with the locale)
67 $conf ||= FS::Conf->new({ 'locale' => $locale });
69 return $self->{_conf} = $conf;
72 =item print_text OPTIONS
74 Returns an text invoice, as a list of lines.
76 Options can be passed as a hash.
78 I<time>, if specified, is used to control the printing of overdue messages. The
79 default is now. It isn't the date of the invoice; that's the `_date' field.
80 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
81 L<Time::Local> and L<Date::Parse> for conversion functions.
83 I<template>, if specified, is the name of a suffix for alternate invoices.
85 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
93 %params = %{ shift() };
98 $params{'format'} = 'template'; # for some reason
100 $self->print_generic( %params );
103 =item print_latex HASHREF
105 Internal method - returns a filename of a filled-in LaTeX template for this
106 invoice (Note: add ".tex" to get the actual filename), and a filename of
107 an associated logo (with the .eps extension included).
109 See print_ps and print_pdf for methods that return PostScript and PDF output.
111 Options can be passed as a hash.
113 I<time>, if specified, is used to control the printing of overdue messages. The
114 default is now. It isn't the date of the invoice; that's the `_date' field.
115 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
116 L<Time::Local> and L<Date::Parse> for conversion functions.
118 I<template>, if specified, is the name of a suffix for alternate invoices.
119 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
120 customize invoice templates for different purposes.
122 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
131 %params = %{ shift() };
136 $params{'format'} = 'latex';
137 my $conf = $self->conf;
139 # this needs to go away
140 my $template = $params{'template'};
141 # and this especially
142 $template ||= $self->_agent_template
143 if $self->can('_agent_template');
145 my $pkey = $self->primary_key;
146 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
148 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
149 my $lh = new File::Temp(
150 TEMPLATE => $tmp_template,
154 ) or die "can't open temp file: $!\n";
156 my $agentnum = $self->agentnum;
158 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
159 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
160 or die "can't write temp file: $!\n";
162 print $lh $conf->config_binary('logo.eps', $agentnum)
163 or die "can't write temp file: $!\n";
166 $params{'logo_file'} = $lh->filename;
168 if( $conf->exists('invoice-barcode')
169 && $self->can('invoice_barcode')
170 && $self->invnum ) { # don't try to barcode statements
171 my $png_file = $self->invoice_barcode($dir);
172 my $eps_file = $png_file;
173 $eps_file =~ s/\.png$/.eps/g;
174 $png_file =~ /(barcode.*png)/;
176 $eps_file =~ /(barcode.*eps)/;
179 my $curr_dir = cwd();
181 # after painfuly long experimentation, it was determined that sam2p won't
182 # accept : and other chars in the path, no matter how hard I tried to
183 # escape them, hence the chdir (and chdir back, just to be safe)
184 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
185 or die "sam2p failed: $!\n";
189 $params{'barcode_file'} = $eps_file;
192 my @filled_in = $self->print_generic( %params );
194 my $fh = new File::Temp( TEMPLATE => $tmp_template,
198 ) or die "can't open temp file: $!\n";
199 binmode($fh, ':utf8'); # language support
200 print $fh join('', @filled_in );
203 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
204 return ($1, $params{'logo_file'}, $params{'barcode_file'});
210 my $cust_main = $self->cust_main;
211 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
214 =item print_generic OPTION => VALUE ...
216 Internal method - returns a filled-in template for this invoice as a scalar.
218 See print_ps and print_pdf for methods that return PostScript and PDF output.
220 Non optional options include
221 format - latex, html, template
223 Optional options include
225 template - a value used as a suffix for a configuration template. Please
228 time - a value used to control the printing of overdue messages. The
229 default is now. It isn't the date of the invoice; that's the `_date' field.
230 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
231 L<Time::Local> and L<Date::Parse> for conversion functions.
235 unsquelch_cdr - overrides any per customer cdr squelching when true
237 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
239 locale - override customer's locale
243 #what's with all the sprintf('%10.2f')'s in here? will it cause any
244 # (alignment in text invoice?) problems to change them all to '%.2f' ?
245 # yes: fixed width/plain text printing will be borked
247 my( $self, %params ) = @_;
248 my $conf = $self->conf;
250 my $today = $params{today} ? $params{today} : time;
251 warn "$me print_generic called on $self with suffix $params{template}\n"
254 my $format = $params{format};
255 die "Unknown format: $format"
256 unless $format =~ /^(latex|html|template)$/;
258 my $cust_main = $self->cust_main || $self->prospect_main;
259 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
260 unless $cust_main->payname
261 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
263 my $locale = $params{'locale'} || $cust_main->locale;
265 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
266 'html' => [ '<%=', '%>' ],
267 'template' => [ '{', '}' ],
270 warn "$me print_generic creating template\n"
273 # set the notice name here, and nowhere else.
274 my $notice_name = $params{notice_name}
275 || $conf->config('notice_name')
276 || $self->notice_name;
279 my $template = $params{template} ? $params{template} : $self->_agent_template;
280 my $templatefile = $self->template_conf. $format;
281 $templatefile .= "_$template"
282 if length($template) && $conf->exists($templatefile."_$template");
285 my @invoice_template = map "$_\n", $conf->config($templatefile)
286 or die "cannot load config data $templatefile";
289 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
290 #change this to a die when the old code is removed
291 warn "old-style invoice template $templatefile; ".
292 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
294 @invoice_template = _translate_old_latex_format(@invoice_template);
297 warn "$me print_generic creating T:T object\n"
300 my $text_template = new Text::Template(
302 SOURCE => \@invoice_template,
303 DELIMITERS => $delimiters{$format},
306 warn "$me print_generic compiling T:T object\n"
309 $text_template->compile()
310 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
313 # additional substitution could possibly cause breakage in existing templates
316 'notes' => sub { map "$_", @_ },
317 'footer' => sub { map "$_", @_ },
318 'smallfooter' => sub { map "$_", @_ },
319 'returnaddress' => sub { map "$_", @_ },
320 'coupon' => sub { map "$_", @_ },
321 'summary' => sub { map "$_", @_ },
327 s/%%(.*)$/<!-- $1 -->/g;
328 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
329 s/\\begin\{enumerate\}/<ol>/g;
331 s/\\end\{enumerate\}/<\/ol>/g;
332 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
341 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
343 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
349 s/\\hyphenation\{[\w\s\-]+}//;
354 'coupon' => sub { "" },
355 'summary' => sub { "" },
362 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
363 s/\\begin\{enumerate\}//g;
365 s/\\end\{enumerate\}//g;
366 s/\\textbf\{(.*)\}/$1/g;
373 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
375 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
380 s/\\\\\*?\s*$/\n/; # dubious
381 s/\\hyphenation\{[\w\s\-]+}//;
385 'coupon' => sub { "" },
386 'summary' => sub { "" },
391 # hashes for differing output formats
392 my %nbsps = ( 'latex' => '~',
393 'html' => '', # '&nbps;' would be nice
394 'template' => '', # not used
396 my $nbsp = $nbsps{$format};
398 my %escape_functions = ( 'latex' => \&_latex_escape,
399 'html' => \&_html_escape_nbsp,#\&encode_entities,
400 'template' => sub { shift },
402 my $escape_function = $escape_functions{$format};
403 my $escape_function_nonbsp = ($format eq 'html')
404 ? \&_html_escape : $escape_function;
406 my %newline_tokens = ( 'latex' => '\\\\',
410 my $newline_token = $newline_tokens{$format};
412 warn "$me generating template variables\n"
415 # generate template variables
419 defined( $conf->config_orbase( "invoice_${format}returnaddress",
423 && length( $conf->config_orbase( "invoice_${format}returnaddress",
429 $returnaddress = join("\n",
430 $conf->config_orbase("invoice_${format}returnaddress", $template)
434 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
436 my $convert_map = $convert_maps{$format}{'returnaddress'};
439 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
444 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
446 my $convert_map = $convert_maps{$format}{'returnaddress'};
447 $returnaddress = join( "\n", &$convert_map(
448 map { s/( {2,})/'~' x length($1)/eg;
452 ( $conf->config('company_name', $cust_main->agentnum),
453 $conf->config('company_address', $cust_main->agentnum),
460 my $warning = "Couldn't find a return address; ".
461 "do you need to set the company_address configuration value?";
463 $returnaddress = $nbsp;
464 #$returnaddress = $warning;
468 warn "$me generating invoice data\n"
471 my $agentnum = $cust_main->agentnum;
476 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
477 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
478 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
479 'returnaddress' => $returnaddress,
480 'agent' => &$escape_function($cust_main->agent->agent),
482 #invoice/quotation info
483 'no_number' => $params{'no_number'},
484 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
485 'quotationnum' => $self->quotationnum,
486 'no_date' => $params{'no_date'},
487 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
488 'date' => ( $params{'no_date'}
490 : $self->time2str_local('long', $self->_date, $format)
492 'today' => $self->time2str_local('long', $today, $format),
493 'terms' => $self->terms,
494 'template' => $template, #params{'template'},
495 'notice_name' => $notice_name, # escape?
496 'current_charges' => sprintf("%.2f", $self->charged),
497 'duedate' => $self->due_date2str('rdate'), #date_format?
500 'custnum' => $cust_main->display_custnum,
501 'prospectnum' => $cust_main->prospectnum,
502 'agent_custid' => &$escape_function($cust_main->agent_custid),
503 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
504 payname company address1 address2 city state zip fax
508 'ship_enable' => $conf->exists('invoice-ship_address'),
509 'unitprices' => $conf->exists('invoice-unitprice'),
510 'smallernotes' => $conf->exists('invoice-smallernotes'),
511 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
512 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
514 #layout info -- would be fancy to calc some of this and bury the template
516 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
517 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
518 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
519 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
520 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
521 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
522 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
523 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
524 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
525 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
527 # better hang on to conf_dir for a while (for old templates)
528 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
530 #these are only used when doing paged plaintext
537 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
538 # prototype here to silence warnings
539 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
541 my $min_sdate = 999999999999;
543 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
544 next unless $cust_bill_pkg->pkgnum > 0;
545 $min_sdate = $cust_bill_pkg->sdate
546 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
547 $max_edate = $cust_bill_pkg->edate
548 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
551 $invoice_data{'bill_period'} = '';
552 $invoice_data{'bill_period'} =
553 $self->time2str_local('%e %h', $min_sdate, $format)
555 $self->time2str_local('%e %h', $max_edate, $format)
556 if ($max_edate != 0 && $min_sdate != 999999999999);
558 $invoice_data{finance_section} = '';
559 if ( $conf->config('finance_pkgclass') ) {
561 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
562 $invoice_data{finance_section} = $pkg_class->categoryname;
564 $invoice_data{finance_amount} = '0.00';
565 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
567 my $countrydefault = $conf->config('countrydefault') || 'US';
568 foreach ( qw( address1 address2 city state zip country fax) ){
569 my $method = 'ship_'.$_;
570 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
572 if ( length($cust_main->ship_company) ) {
573 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
575 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
577 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
578 $invoice_data{'ship_country'} = ''
579 if ( $invoice_data{'ship_country'} eq $countrydefault );
581 $invoice_data{'cid'} = $params{'cid'}
584 if ( $cust_main->country eq $countrydefault ) {
585 $invoice_data{'country'} = '';
587 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
591 $invoice_data{'address'} = \@address;
594 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
595 ? " (P.O. #". $cust_main->payinfo. ")"
599 push @address, $cust_main->company
600 if $cust_main->company;
601 push @address, $cust_main->address1;
602 push @address, $cust_main->address2
603 if $cust_main->address2;
605 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
606 push @address, $invoice_data{'country'}
607 if $invoice_data{'country'};
609 while (scalar(@address) < 5);
611 $invoice_data{'logo_file'} = $params{'logo_file'}
612 if $params{'logo_file'};
613 $invoice_data{'barcode_file'} = $params{'barcode_file'}
614 if $params{'barcode_file'};
615 $invoice_data{'barcode_img'} = $params{'barcode_img'}
616 if $params{'barcode_img'};
617 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
618 if $params{'barcode_cid'};
620 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
621 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
622 #my $balance_due = $self->owed + $pr_total - $cr_total;
623 my $balance_due = $self->owed + $pr_total;
625 #these are used on the summary page only
627 # the customer's current balance as shown on the invoice before this one
628 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
630 # the change in balance from that invoice to this one
631 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
633 # the sum of amount owed on all previous invoices
634 # ($pr_total is used elsewhere but not as $previous_balance)
635 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
637 # the sum of amount owed on all invoices
638 # (this is used in the summary & on the payment coupon)
639 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
641 # info from customer's last invoice before this one, for some
643 $invoice_data{'last_bill'} = {};
645 if ( $self->custnum && $self->invnum ) {
647 if ( $self->previous_bill ) {
648 my $last_bill = $self->previous_bill;
649 $invoice_data{'last_bill'} = {
650 '_date' => $last_bill->_date, #unformatted
652 my (@payments, @credits);
653 # for formats that itemize previous payments
654 foreach my $cust_pay ( qsearch('cust_pay', {
655 'custnum' => $self->custnum,
656 '_date' => { op => '>=',
657 value => $last_bill->_date }
660 next if $cust_pay->_date > $self->_date;
662 '_date' => $cust_pay->_date,
663 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
664 'payinfo' => $cust_pay->payby_payinfo_pretty,
665 'amount' => sprintf('%.2f', $cust_pay->paid),
667 # not concerned about applications
669 foreach my $cust_credit ( qsearch('cust_credit', {
670 'custnum' => $self->custnum,
671 '_date' => { op => '>=',
672 value => $last_bill->_date }
675 next if $cust_credit->_date > $self->_date;
677 '_date' => $cust_credit->_date,
678 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
679 'creditreason'=> $cust_credit->reason,
680 'amount' => sprintf('%.2f', $cust_credit->amount),
683 $invoice_data{'previous_payments'} = \@payments;
684 $invoice_data{'previous_credits'} = \@credits;
689 my $summarypage = '';
690 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
693 $invoice_data{'summarypage'} = $summarypage;
695 warn "$me substituting variables in notes, footer, smallfooter\n"
698 my $tc = $self->template_conf;
699 my @include = ( [ $tc, 'notes' ],
700 [ 'invoice_', 'footer' ],
701 [ 'invoice_', 'smallfooter', ],
703 push @include, [ $tc, 'coupon', ]
704 unless $params{'no_coupon'};
706 foreach my $i (@include) {
708 my($base, $include) = @$i;
710 my $inc_file = $conf->key_orbase("$base$format$include", $template);
713 if ( $conf->exists($inc_file, $agentnum)
714 && length( $conf->config($inc_file, $agentnum) ) ) {
716 @inc_src = $conf->config($inc_file, $agentnum);
720 $inc_file = $conf->key_orbase("${base}latex$include", $template);
722 my $convert_map = $convert_maps{$format}{$include};
724 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
725 s/--\@\]/$delimiters{$format}[1]/g;
728 &$convert_map( $conf->config($inc_file, $agentnum) );
732 my $inc_tt = new Text::Template (
734 SOURCE => [ map "$_\n", @inc_src ],
735 DELIMITERS => $delimiters{$format},
736 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
738 unless ( $inc_tt->compile() ) {
739 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
740 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
744 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
746 $invoice_data{$include} =~ s/\n+$//
747 if ($format eq 'latex');
750 # let invoices use either of these as needed
751 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
752 ? $cust_main->payinfo : '';
753 $invoice_data{'po_line'} =
754 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
755 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
758 my %money_chars = ( 'latex' => '',
759 'html' => $conf->config('money_char') || '$',
762 my $money_char = $money_chars{$format};
764 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
765 'html' => $conf->config('money_char') || '$',
768 my $other_money_char = $other_money_chars{$format};
769 $invoice_data{'dollar'} = $other_money_char;
771 my %minus_signs = ( 'latex' => '$-$',
773 'template' => '- ' );
774 my $minus = $minus_signs{$format};
776 my @detail_items = ();
777 my @total_items = ();
781 $invoice_data{'detail_items'} = \@detail_items;
782 $invoice_data{'total_items'} = \@total_items;
783 $invoice_data{'buf'} = \@buf;
784 $invoice_data{'sections'} = \@sections;
786 warn "$me generating sections\n"
790 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
791 'subtotal' => $taxtotal, # adjusted below
794 my $tax_weight = _pkg_category($tax_section->{description})
795 ? _pkg_category($tax_section->{description})->weight
797 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
798 $tax_section->{'sort_weight'} = $tax_weight;
801 my $adjust_section = {
802 'description' => $self->mt('Credits, Payments, and Adjustments'),
803 'adjust_section' => 1,
804 'subtotal' => 0, # adjusted below
806 my $adjust_weight = _pkg_category($adjust_section->{description})
807 ? _pkg_category($adjust_section->{description})->weight
809 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
810 $adjust_section->{'sort_weight'} = $adjust_weight;
812 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
813 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
814 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
815 $invoice_data{'multisection'} = $multisection;
817 my $extra_sections = [];
818 my $extra_lines = ();
820 # default section ('Charges')
821 my $default_section = { 'description' => '',
826 # Previous Charges section
827 # subtotal is the first return value from $self->previous
828 my $previous_section;
829 # if the invoice has major sections, or if we're summarizing previous
830 # charges with a single line, or if we've been specifically told to put them
831 # in a section, create a section for previous charges:
832 if ( $multisection or
833 $conf->exists('previous_balance-summary_only') or
834 $conf->exists('previous_balance-section') ) {
836 $previous_section = { 'description' => $self->mt('Previous Charges'),
837 'subtotal' => $other_money_char.
838 sprintf('%.2f', $pr_total),
839 'summarized' => '', #why? $summarypage ? 'Y' : '',
841 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
842 join(' / ', map { $cust_main->balance_date_range(@$_) }
843 $self->_prior_month30s
845 if $conf->exists('invoice_include_aging');
848 # otherwise put them in the main section
849 $previous_section = $default_section;
852 if ( $multisection ) {
853 ($extra_sections, $extra_lines) =
854 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
855 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
856 && $self->can('_items_extra_usage_sections');
858 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
860 push @detail_items, @$extra_lines if $extra_lines;
862 # the code is written so that both methods can be used together, but
863 # we haven't yet changed the template to take advantage of that, so for
864 # now, treat them as mutually exclusive.
865 my %section_method = ( by_category => 1 );
866 if ( $conf->exists($tc.'sections_by_location') ) {
867 %section_method = ( by_location => 1 );
870 $self->_items_sections( 'summary' => $summarypage,
871 'escape' => $escape_function_nonbsp,
872 'extra_sections' => $extra_sections,
876 push @sections, @$early;
877 $late_sections = $late;
879 if ( $conf->exists('svc_phone_sections')
880 && $self->can('_items_svc_phone_sections')
883 my ($phone_sections, $phone_lines) =
884 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
885 push @{$late_sections}, @$phone_sections;
886 push @detail_items, @$phone_lines;
888 if ( $conf->exists('voip-cust_accountcode_cdr')
889 && $cust_main->accountcode_cdr
890 && $self->can('_items_accountcode_cdr')
893 my ($accountcode_section, $accountcode_lines) =
894 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
895 if ( scalar(@$accountcode_lines) ) {
896 push @{$late_sections}, $accountcode_section;
897 push @detail_items, @$accountcode_lines;
900 } else {# not multisection
901 # make a default section
902 push @sections, $default_section;
903 # and calculate the finance charge total, since it won't get done otherwise.
904 # and the default section total
905 # XXX possibly finance_pkgclass should not be used in this manner?
908 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
909 if ( $invoice_data{finance_section} and
910 grep { $_->section eq $invoice_data{finance_section} }
911 $cust_bill_pkg->cust_bill_pkg_display ) {
912 # I think these are always setup fees, but just to be sure...
913 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
915 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
918 $invoice_data{finance_amount} =
919 sprintf('%.2f', sum( @finance_charges ) || 0);
920 $default_section->{subtotal} = $other_money_char.
921 sprintf('%.2f', sum( @charges ) || 0);
924 # previous invoice balances in the Previous Charges section if there
925 # is one, otherwise in the main detail section
926 # (except if summary_only is enabled, don't show them at all)
927 if ( $self->can('_items_previous') &&
928 $self->enable_previous &&
929 ! $conf->exists('previous_balance-summary_only') ) {
931 warn "$me adding previous balances\n"
934 foreach my $line_item ( $self->_items_previous ) {
937 ref => $line_item->{'pkgnum'},
938 pkgpart => $line_item->{'pkgpart'},
940 section => $previous_section, # which might be $default_section
941 description => &$escape_function($line_item->{'description'}),
942 ext_description => [ map { &$escape_function($_) }
943 @{ $line_item->{'ext_description'} || [] }
945 amount => ( $old_latex ? '' : $money_char).
946 $line_item->{'amount'},
947 product_code => $line_item->{'pkgpart'} || 'N/A',
950 push @detail_items, $detail;
951 push @buf, [ $detail->{'description'},
952 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
958 if ( @pr_cust_bill && $self->enable_previous ) {
959 push @buf, ['','-----------'];
960 push @buf, [ $self->mt('Total Previous Balance'),
961 $money_char. sprintf("%10.2f", $pr_total) ];
965 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
966 warn "$me adding DID summary\n"
969 my ($didsummary,$minutes) = $self->_did_summary;
970 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
972 { 'description' => $didsummary_desc,
973 'ext_description' => [ $didsummary, $minutes ],
977 foreach my $section (@sections, @$late_sections) {
979 # begin some normalization
980 $section->{'subtotal'} = $section->{'amount'}
982 && !exists($section->{subtotal})
983 && exists($section->{amount});
985 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
986 if ( $invoice_data{finance_section} &&
987 $section->{'description'} eq $invoice_data{finance_section} );
989 $section->{'subtotal'} = $other_money_char.
990 sprintf('%.2f', $section->{'subtotal'})
993 # continue some normalization
994 $section->{'amount'} = $section->{'subtotal'}
998 if ( $section->{'description'} ) {
999 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1004 warn "$me setting options\n"
1008 $options{'section'} = $section if $multisection;
1009 $options{'format'} = $format;
1010 $options{'escape_function'} = $escape_function;
1011 $options{'no_usage'} = 1 unless $unsquelched;
1012 $options{'unsquelched'} = $unsquelched;
1013 $options{'summary_page'} = $summarypage;
1014 $options{'skip_usage'} =
1015 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1017 warn "$me searching for line items\n"
1020 foreach my $line_item ( $self->_items_pkg(%options) ) {
1022 warn "$me adding line item $line_item\n"
1026 ext_description => [],
1028 $detail->{'ref'} = $line_item->{'pkgnum'};
1029 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1030 $detail->{'quantity'} = $line_item->{'quantity'};
1031 $detail->{'section'} = $section;
1032 $detail->{'description'} = &$escape_function($line_item->{'description'});
1033 if ( exists $line_item->{'ext_description'} ) {
1034 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1036 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1037 $line_item->{'amount'};
1038 if ( exists $line_item->{'unit_amount'} ) {
1039 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1040 $line_item->{'unit_amount'};
1042 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1044 $detail->{'sdate'} = $line_item->{'sdate'};
1045 $detail->{'edate'} = $line_item->{'edate'};
1046 $detail->{'seconds'} = $line_item->{'seconds'};
1047 $detail->{'svc_label'} = $line_item->{'svc_label'};
1049 push @detail_items, $detail;
1050 push @buf, ( [ $detail->{'description'},
1051 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1053 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1057 if ( $section->{'description'} ) {
1058 push @buf, ( ['','-----------'],
1059 [ $section->{'description'}. ' sub-total',
1060 $section->{'subtotal'} # already formatted this
1069 $invoice_data{current_less_finance} =
1070 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1072 # if there's anything in the Previous Charges section, prepend it to the list
1073 if ( $pr_total and $previous_section ne $default_section ) {
1074 unshift @sections, $previous_section;
1077 warn "$me adding taxes\n"
1080 my @items_tax = $self->_items_tax;
1081 foreach my $tax ( @items_tax ) {
1083 $taxtotal += $tax->{'amount'};
1085 my $description = &$escape_function( $tax->{'description'} );
1086 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1088 if ( $multisection ) {
1090 my $money = $old_latex ? '' : $money_char;
1091 push @detail_items, {
1092 ext_description => [],
1095 description => $description,
1096 amount => $money. $amount,
1098 section => $tax_section,
1103 push @total_items, {
1104 'total_item' => $description,
1105 'total_amount' => $other_money_char. $amount,
1110 push @buf,[ $description,
1111 $money_char. $amount,
1118 $total->{'total_item'} = $self->mt('Sub-total');
1119 $total->{'total_amount'} =
1120 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1122 if ( $multisection ) {
1123 $tax_section->{'subtotal'} = $other_money_char.
1124 sprintf('%.2f', $taxtotal);
1125 $tax_section->{'pretotal'} = 'New charges sub-total '.
1126 $total->{'total_amount'};
1127 push @sections, $tax_section if $taxtotal;
1129 unshift @total_items, $total;
1132 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1134 push @buf,['','-----------'];
1135 push @buf,[$self->mt(
1136 (!$self->enable_previous)
1138 : 'Total New Charges'
1140 $money_char. sprintf("%10.2f",$self->charged) ];
1148 my %embolden_functions = (
1149 'latex' => sub { return '\textbf{'. shift(). '}' },
1150 'html' => sub { return '<b>'. shift(). '</b>' },
1151 'template' => sub { shift },
1153 my $embolden_function = $embolden_functions{$format};
1155 if ( $self->can('_items_total') ) { # quotations
1157 $self->_items_total(\@total_items);
1159 foreach ( @total_items ) {
1160 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1161 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1162 $_->{'total_amount'}
1166 } else { #normal invoice case
1168 # calculate total, possibly including total owed on previous
1172 $item = $conf->config('previous_balance-exclude_from_total')
1173 || 'Total New Charges'
1174 if $conf->exists('previous_balance-exclude_from_total');
1175 my $amount = $self->charged;
1176 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1177 $amount += $pr_total;
1180 $total->{'total_item'} = &$embolden_function($self->mt($item));
1181 $total->{'total_amount'} =
1182 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1183 if ( $multisection ) {
1184 if ( $adjust_section->{'sort_weight'} ) {
1185 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1186 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1188 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1189 $other_money_char. sprintf('%.2f', $self->charged );
1192 push @total_items, $total;
1194 push @buf,['','-----------'];
1197 sprintf( '%10.2f', $amount )
1201 # if we're showing previous invoices, also show previous
1202 # credits and payments
1203 if ( $self->enable_previous
1204 and $self->can('_items_credits')
1205 and $self->can('_items_payments') )
1207 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1210 my $credittotal = 0;
1211 foreach my $credit (
1212 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1216 $total->{'total_item'} = &$escape_function($credit->{'description'});
1217 $credittotal += $credit->{'amount'};
1218 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1219 $adjusttotal += $credit->{'amount'};
1220 if ( $multisection ) {
1221 my $money = $old_latex ? '' : $money_char;
1222 push @detail_items, {
1223 ext_description => [],
1226 description => &$escape_function($credit->{'description'}),
1227 amount => $money. $credit->{'amount'},
1229 section => $adjust_section,
1232 push @total_items, $total;
1236 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1239 foreach my $credit (
1240 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1242 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1246 my $paymenttotal = 0;
1247 foreach my $payment (
1248 $self->_items_payments( 'template' => $template )
1251 $total->{'total_item'} = &$escape_function($payment->{'description'});
1252 $paymenttotal += $payment->{'amount'};
1253 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1254 $adjusttotal += $payment->{'amount'};
1255 if ( $multisection ) {
1256 my $money = $old_latex ? '' : $money_char;
1257 push @detail_items, {
1258 ext_description => [],
1261 description => &$escape_function($payment->{'description'}),
1262 amount => $money. $payment->{'amount'},
1264 section => $adjust_section,
1267 push @total_items, $total;
1269 push @buf, [ $payment->{'description'},
1270 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1273 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1275 if ( $multisection ) {
1276 $adjust_section->{'subtotal'} = $other_money_char.
1277 sprintf('%.2f', $adjusttotal);
1278 push @sections, $adjust_section
1279 unless $adjust_section->{sort_weight};
1282 # create Balance Due message
1285 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1286 $total->{'total_amount'} =
1287 &$embolden_function(
1288 $other_money_char. sprintf('%.2f', #why? $summarypage
1289 # ? $self->charged +
1290 # $self->billing_balance
1292 $self->owed + $pr_total
1295 if ( $multisection && !$adjust_section->{sort_weight} ) {
1296 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1297 $total->{'total_amount'};
1299 push @total_items, $total;
1301 push @buf,['','-----------'];
1302 push @buf,[$self->balance_due_msg, $money_char.
1303 sprintf("%10.2f", $balance_due ) ];
1306 if ( $conf->exists('previous_balance-show_credit')
1307 and $cust_main->balance < 0 ) {
1308 my $credit_total = {
1309 'total_item' => &$embolden_function($self->credit_balance_msg),
1310 'total_amount' => &$embolden_function(
1311 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1314 if ( $multisection ) {
1315 $adjust_section->{'posttotal'} .= $newline_token .
1316 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1319 push @total_items, $credit_total;
1321 push @buf,['','-----------'];
1322 push @buf,[$self->credit_balance_msg, $money_char.
1323 sprintf("%10.2f", -$cust_main->balance ) ];
1327 } #end of default total adding ! can('_items_total')
1329 if ( $multisection ) {
1330 if ( $conf->exists('svc_phone_sections')
1331 && $self->can('_items_svc_phone_sections')
1335 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1336 $total->{'total_amount'} =
1337 &$embolden_function(
1338 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1340 my $last_section = pop @sections;
1341 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1342 $total->{'total_amount'};
1343 push @sections, $last_section;
1345 push @sections, @$late_sections
1349 # make a discounts-available section, even without multisection
1350 if ( $conf->exists('discount-show_available')
1351 and my @discounts_avail = $self->_items_discounts_avail ) {
1352 my $discount_section = {
1353 'description' => $self->mt('Discounts Available'),
1358 push @sections, $discount_section;
1359 push @detail_items, map { +{
1360 'ref' => '', #should this be something else?
1361 'section' => $discount_section,
1362 'description' => &$escape_function( $_->{description} ),
1363 'amount' => $money_char . &$escape_function( $_->{amount} ),
1364 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1365 } } @discounts_avail;
1368 my @summary_subtotals;
1369 # the templates say "$_->{tax_section} || !$_->{summarized}"
1370 # except 'summarized' is only true when tax_section is true, so this
1371 # is always true, so what's the deal?
1372 foreach my $s (@sections) {
1373 # not to include in the "summary of new charges" block:
1374 # finance charges, adjustments, previous charges,
1375 # and itemized phone usage sections
1376 if ( $s eq $adjust_section or
1377 ($s eq $previous_section and $s ne $default_section) or
1378 ($invoice_data{'finance_section'} and
1379 $invoice_data{'finance_section'} eq $s->{description}) or
1380 $s->{'description'} =~ /^\d+ $/ ) {
1383 push @summary_subtotals, $s;
1385 $invoice_data{summary_subtotals} = \@summary_subtotals;
1387 # debugging hook: call this with 'diag' => 1 to just get a hash of
1388 # the invoice variables
1389 return \%invoice_data if ( $params{'diag'} );
1391 # All sections and items are built; now fill in templates.
1392 my @includelist = ();
1393 push @includelist, 'summary' if $summarypage;
1394 foreach my $include ( @includelist ) {
1396 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1399 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1401 @inc_src = $conf->config($inc_file, $agentnum);
1405 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1407 my $convert_map = $convert_maps{$format}{$include};
1409 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1410 s/--\@\]/$delimiters{$format}[1]/g;
1413 &$convert_map( $conf->config($inc_file, $agentnum) );
1417 my $inc_tt = new Text::Template (
1419 SOURCE => [ map "$_\n", @inc_src ],
1420 DELIMITERS => $delimiters{$format},
1421 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1423 unless ( $inc_tt->compile() ) {
1424 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1425 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1429 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1431 $invoice_data{$include} =~ s/\n+$//
1432 if ($format eq 'latex');
1437 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1438 /invoice_lines\((\d*)\)/;
1439 $invoice_lines += $1 || scalar(@buf);
1442 die "no invoice_lines() functions in template?"
1443 if ( $format eq 'template' && !$wasfunc );
1445 if ($format eq 'template') {
1447 if ( $invoice_lines ) {
1448 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1449 $invoice_data{'total_pages'}++
1450 if scalar(@buf) % $invoice_lines;
1453 #setup subroutine for the template
1454 $invoice_data{invoice_lines} = sub {
1455 my $lines = shift || scalar(@buf);
1467 push @collect, split("\n",
1468 $text_template->fill_in( HASH => \%invoice_data )
1470 $invoice_data{'page'}++;
1472 map "$_\n", @collect;
1474 } else { # this is where we actually create the invoice
1476 warn "filling in template for invoice ". $self->invnum. "\n"
1478 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1481 $text_template->fill_in(HASH => \%invoice_data);
1485 sub notice_name { '('.shift->table.')'; }
1487 sub template_conf { 'invoice_'; }
1489 # helper routine for generating date ranges
1490 sub _prior_month30s {
1493 [ 1, 2592000 ], # 0-30 days ago
1494 [ 2592000, 5184000 ], # 30-60 days ago
1495 [ 5184000, 7776000 ], # 60-90 days ago
1496 [ 7776000, 0 ], # 90+ days ago
1499 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1500 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1505 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1507 Returns an postscript invoice, as a scalar.
1509 Options can be passed as a hashref (recommended) or as a list of time, template
1510 and then any key/value pairs for any other options.
1512 I<time> an optional value used to control the printing of overdue messages. The
1513 default is now. It isn't the date of the invoice; that's the `_date' field.
1514 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1515 L<Time::Local> and L<Date::Parse> for conversion functions.
1517 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1524 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1525 my $ps = generate_ps($file);
1527 unlink($barcodefile) if $barcodefile;
1532 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1534 Returns an PDF invoice, as a scalar.
1536 Options can be passed as a hashref (recommended) or as a list of time, template
1537 and then any key/value pairs for any other options.
1539 I<time> an optional value used to control the printing of overdue messages. The
1540 default is now. It isn't the date of the invoice; that's the `_date' field.
1541 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1542 L<Time::Local> and L<Date::Parse> for conversion functions.
1544 I<template>, if specified, is the name of a suffix for alternate invoices.
1546 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1553 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1554 my $pdf = generate_pdf($file);
1556 unlink($barcodefile) if $barcodefile;
1561 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1563 Returns an HTML invoice, as a scalar.
1565 I<time> an optional value used to control the printing of overdue messages. The
1566 default is now. It isn't the date of the invoice; that's the `_date' field.
1567 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1568 L<Time::Local> and L<Date::Parse> for conversion functions.
1570 I<template>, if specified, is the name of a suffix for alternate invoices.
1572 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1574 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1575 when emailing the invoice as part of a multipart/related MIME email.
1583 %params = %{ shift() };
1587 $params{'format'} = 'html';
1589 $self->print_generic( %params );
1592 # quick subroutine for print_latex
1594 # There are ten characters that LaTeX treats as special characters, which
1595 # means that they do not simply typeset themselves:
1596 # # $ % & ~ _ ^ \ { }
1598 # TeX ignores blanks following an escaped character; if you want a blank (as
1599 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1603 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1604 $value =~ s/([<>])/\$$1\$/g;
1610 encode_entities($value);
1614 sub _html_escape_nbsp {
1615 my $value = _html_escape(shift);
1616 $value =~ s/ +/ /g;
1620 #utility methods for print_*
1622 sub _translate_old_latex_format {
1623 warn "_translate_old_latex_format called\n"
1630 if ( $line =~ /^%%Detail\s*$/ ) {
1632 push @template, q![@--!,
1633 q! foreach my $_tr_line (@detail_items) {!,
1634 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1635 q! $_tr_line->{'description'} .= !,
1636 q! "\\tabularnewline\n~~".!,
1637 q! join( "\\tabularnewline\n~~",!,
1638 q! @{$_tr_line->{'ext_description'}}!,
1642 while ( ( my $line_item_line = shift )
1643 !~ /^%%EndDetail\s*$/ ) {
1644 $line_item_line =~ s/'/\\'/g; # nice LTS
1645 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1646 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1647 push @template, " \$OUT .= '$line_item_line';";
1650 push @template, '}',
1653 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1655 push @template, '[@--',
1656 ' foreach my $_tr_line (@total_items) {';
1658 while ( ( my $total_item_line = shift )
1659 !~ /^%%EndTotalDetails\s*$/ ) {
1660 $total_item_line =~ s/'/\\'/g; # nice LTS
1661 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1662 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1663 push @template, " \$OUT .= '$total_item_line';";
1666 push @template, '}',
1670 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1671 push @template, $line;
1677 warn "$_\n" foreach @template;
1685 my $conf = $self->conf;
1687 #check for an invoice-specific override
1688 return $self->invoice_terms if $self->invoice_terms;
1690 #check for a customer- specific override
1691 my $cust_main = $self->cust_main;
1692 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1694 #use configured default
1695 $conf->config('invoice_default_terms') || '';
1701 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1702 $duedate = $self->_date() + ( $1 * 86400 );
1709 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1712 sub balance_due_msg {
1714 my $msg = $self->mt('Balance Due');
1715 return $msg unless $self->terms;
1716 if ( $self->due_date ) {
1717 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1718 $self->due_date2str('short');
1719 } elsif ( $self->terms ) {
1720 $msg .= ' - '. $self->terms;
1725 sub balance_due_date {
1727 my $conf = $self->conf;
1729 if ( $conf->exists('invoice_default_terms')
1730 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1731 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1736 sub credit_balance_msg {
1738 $self->mt('Credit Balance Remaining')
1743 Returns a string with the date, for example: "3/20/2008"
1749 $self->time2str_local('short', $self->_date);
1752 =item _items_sections OPTIONS
1754 Generate section information for all items appearing on this invoice.
1755 This will only be called for multi-section invoices.
1757 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1758 related display records (L<FS::cust_bill_pkg_display>) and organize
1759 them into two groups ("early" and "late" according to whether they come
1760 before or after the total), then into sections. A subtotal is calculated
1763 Section descriptions are returned in sort weight order. Each consists
1764 of a hash containing:
1766 description: the package category name, escaped
1767 subtotal: the total charges in that section
1768 tax_section: a flag indicating that the section contains only tax charges
1769 summarized: same as tax_section, for some reason
1770 sort_weight: the package category's sort weight
1772 If 'condense' is set on the display record, it also contains everything
1773 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1774 coderefs to generate parts of the invoice. This is not advised.
1776 The method returns two arrayrefs, one of "early" sections and one of "late"
1779 OPTIONS may include:
1781 by_location: a flag to divide the invoice into sections by location.
1782 Each section hash will have a 'location' element containing a hashref of
1783 the location fields (see L<FS::cust_location>). The section description
1784 will be the location label, but the template can use any of the location
1785 fields to create a suitable label.
1787 by_category: a flag to divide the invoice into sections using display
1788 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1789 behavior. Each section hash will have a 'category' element containing
1790 the section name from the display record (which probably equals the
1791 category name of the package, but may not in some cases).
1793 summary: a flag indicating that this is a summary-format invoice.
1794 Turning this on has the following effects:
1795 - Ignores display items with the 'summary' flag.
1796 - Places all sections in the "early" group even if they have post_total.
1797 - Creates sections for all non-disabled package categories, even if they
1798 have no charges on this invoice, as well as a section with no name.
1800 escape: an escape function to use for section titles.
1802 extra_sections: an arrayref of additional sections to return after the
1803 sorted list. If there are any of these, section subtotals exclude
1806 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1807 passed through to C<_condense_section()>.
1811 use vars qw(%pkg_category_cache);
1812 sub _items_sections {
1816 my $escape = $opt{escape};
1817 my @extra_sections = @{ $opt{extra_sections} || [] };
1819 # $subtotal{$locationnum}{$categoryname} = amount.
1820 # if we're not using by_location, $locationnum is undef.
1821 # if we're not using by_category, you guessed it, $categoryname is undef.
1822 # if we're not using either one, we shouldn't be here in the first place...
1824 my %late_subtotal = ();
1827 # About tax items + multisection invoices:
1828 # If either invoice_*summary option is enabled, AND there is a
1829 # package category with the name of the tax, then there will be
1830 # a display record assigning the tax item to that category.
1832 # However, the taxes are always placed in the "Taxes, Surcharges,
1833 # and Fees" section regardless of that. The only effect of the
1834 # display record is to create a subtotal for the summary page.
1837 my $pkg_hash = $self->cust_pkg_hash;
1839 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1842 my $usage = $cust_bill_pkg->usage;
1845 if ( $opt{by_location} ) {
1846 if ( $cust_bill_pkg->pkgnum ) {
1847 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1852 $locationnum = undef;
1855 # as in _items_cust_pkg, if a line item has no display records,
1856 # cust_bill_pkg_display() returns a default record for it
1858 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1859 next if ( $display->summary && $opt{summary} );
1861 my $section = $display->section;
1862 my $type = $display->type;
1863 $section = undef unless $opt{by_category};
1865 $not_tax{$locationnum}{$section} = 1
1866 unless $cust_bill_pkg->pkgnum == 0;
1868 # there's actually a very important piece of logic buried in here:
1869 # incrementing $late_subtotal{$section} CREATES
1870 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1871 # to define the list of late sections, and likewise keys(%subtotal).
1872 # When _items_cust_bill_pkg is called to generate line items for
1873 # real, it will be called with 'section' => $section for each
1875 if ( $display->post_total && !$opt{summary} ) {
1876 if (! $type || $type eq 'S') {
1877 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1878 if $cust_bill_pkg->setup != 0
1879 || $cust_bill_pkg->setup_show_zero;
1883 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1884 if $cust_bill_pkg->recur != 0
1885 || $cust_bill_pkg->recur_show_zero;
1888 if ($type && $type eq 'R') {
1889 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1890 if $cust_bill_pkg->recur != 0
1891 || $cust_bill_pkg->recur_show_zero;
1894 if ($type && $type eq 'U') {
1895 $late_subtotal{$locationnum}{$section} += $usage
1896 unless scalar(@extra_sections);
1899 } else { # it's a pre-total (normal) section
1901 # skip tax items unless they're explicitly included in a section
1902 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1904 if (! $type || $type eq 'S') {
1905 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1906 if $cust_bill_pkg->setup != 0
1907 || $cust_bill_pkg->setup_show_zero;
1911 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1912 if $cust_bill_pkg->recur != 0
1913 || $cust_bill_pkg->recur_show_zero;
1916 if ($type && $type eq 'R') {
1917 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1918 if $cust_bill_pkg->recur != 0
1919 || $cust_bill_pkg->recur_show_zero;
1922 if ($type && $type eq 'U') {
1923 $subtotal{$locationnum}{$section} += $usage
1924 unless scalar(@extra_sections);
1933 %pkg_category_cache = ();
1935 # summary invoices need subtotals for all non-disabled package categories,
1936 # even if they're zero
1937 # but currently assume that there are no location sections, or at least
1938 # that the summary page doesn't care about them
1939 if ( $opt{summary} ) {
1940 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1941 $subtotal{''}{$category->categoryname} ||= 0;
1943 $subtotal{''}{''} ||= 0;
1947 foreach my $post_total (0,1) {
1949 my $s = $post_total ? \%late_subtotal : \%subtotal;
1950 foreach my $locationnum (keys %$s) {
1951 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1953 'subtotal' => $s->{$locationnum}{$sectionname},
1954 'post_total' => $post_total,
1957 if ( $locationnum ) {
1958 $section->{'locationnum'} = $locationnum;
1959 my $location = FS::cust_location->by_key($locationnum);
1960 $section->{'description'} = &{ $escape }($location->location_label);
1961 # Better ideas? This will roughly group them by proximity,
1962 # which alpha sorting on any of the address fields won't.
1963 # Sorting by locationnum is meaningless.
1964 # We have to sort on _something_ or the order may change
1965 # randomly from one invoice to the next, which will confuse
1967 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1969 $section->{'location'} = {
1970 map { $_ => &{ $escape }($location->get($_)) }
1974 $section->{'category'} = $sectionname;
1975 $section->{'description'} = &{ $escape }($sectionname);
1976 if ( _pkg_category($_) ) {
1977 $section->{'sort_weight'} = _pkg_category($_)->weight;
1978 if ( _pkg_category($_)->condense ) {
1979 $section = { %$section, $self->_condense_section($opt{format}) };
1983 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1984 # then it's a tax-only section
1985 $section->{'summarized'} = 'Y';
1986 $section->{'tax_section'} = 'Y';
1988 push @these, $section;
1989 } # foreach $sectionname
1990 } #foreach $locationnum
1991 push @these, @extra_sections if $post_total == 0;
1992 # need an alpha sort for location sections, because postal codes can
1994 $sections[ $post_total ] = [ sort {
1995 $opt{'by_location'} ?
1996 ($a->{sort_weight} cmp $b->{sort_weight}) :
1997 ($a->{sort_weight} <=> $b->{sort_weight})
1999 } #foreach $post_total
2001 return @sections; # early, late
2004 #helper subs for above
2008 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2012 my $categoryname = shift;
2013 $pkg_category_cache{$categoryname} ||=
2014 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2017 my %condensed_format = (
2018 'label' => [ qw( Description Qty Amount ) ],
2020 sub { shift->{description} },
2021 sub { shift->{quantity} },
2022 sub { my($href, %opt) = @_;
2023 ($opt{dollar} || ''). $href->{amount};
2026 'align' => [ qw( l r r ) ],
2027 'span' => [ qw( 5 1 1 ) ], # unitprices?
2028 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2031 sub _condense_section {
2032 my ( $self, $format ) = ( shift, shift );
2034 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2035 qw( description_generator
2038 total_line_generator
2043 sub _condensed_generator_defaults {
2044 my ( $self, $format ) = ( shift, shift );
2045 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2054 sub _condensed_header_generator {
2055 my ( $self, $format ) = ( shift, shift );
2057 my ( $f, $prefix, $suffix, $separator, $column ) =
2058 _condensed_generator_defaults($format);
2060 if ($format eq 'latex') {
2061 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2062 $suffix = "\\\\\n\\hline";
2065 sub { my ($d,$a,$s,$w) = @_;
2066 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2068 } elsif ( $format eq 'html' ) {
2069 $prefix = '<th></th>';
2073 sub { my ($d,$a,$s,$w) = @_;
2074 return qq!<th align="$html_align{$a}">$d</th>!;
2082 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2084 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2087 $prefix. join($separator, @result). $suffix;
2092 sub _condensed_description_generator {
2093 my ( $self, $format ) = ( shift, shift );
2095 my ( $f, $prefix, $suffix, $separator, $column ) =
2096 _condensed_generator_defaults($format);
2098 my $money_char = '$';
2099 if ($format eq 'latex') {
2100 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2102 $separator = " & \n";
2104 sub { my ($d,$a,$s,$w) = @_;
2105 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2107 $money_char = '\\dollar';
2108 }elsif ( $format eq 'html' ) {
2109 $prefix = '"><td align="center"></td>';
2113 sub { my ($d,$a,$s,$w) = @_;
2114 return qq!<td align="$html_align{$a}">$d</td>!;
2116 #$money_char = $conf->config('money_char') || '$';
2117 $money_char = ''; # this is madness
2125 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2127 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2129 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2130 map { $f->{$_}->[$i] } qw(align span width)
2134 $prefix. join( $separator, @result ). $suffix;
2139 sub _condensed_total_generator {
2140 my ( $self, $format ) = ( shift, shift );
2142 my ( $f, $prefix, $suffix, $separator, $column ) =
2143 _condensed_generator_defaults($format);
2146 if ($format eq 'latex') {
2149 $separator = " & \n";
2151 sub { my ($d,$a,$s,$w) = @_;
2152 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2154 }elsif ( $format eq 'html' ) {
2158 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2160 sub { my ($d,$a,$s,$w) = @_;
2161 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2170 # my $r = &{$f->{fields}->[$i]}(@args);
2171 # $r .= ' Total' unless $i;
2173 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2175 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2176 map { $f->{$_}->[$i] } qw(align span width)
2180 $prefix. join( $separator, @result ). $suffix;
2185 =item total_line_generator FORMAT
2187 Returns a coderef used for generation of invoice total line items for this
2188 usage_class. FORMAT is either html or latex
2192 # should not be used: will have issues with hash element names (description vs
2193 # total_item and amount vs total_amount -- another array of functions?
2195 sub _condensed_total_line_generator {
2196 my ( $self, $format ) = ( shift, shift );
2198 my ( $f, $prefix, $suffix, $separator, $column ) =
2199 _condensed_generator_defaults($format);
2202 if ($format eq 'latex') {
2205 $separator = " & \n";
2207 sub { my ($d,$a,$s,$w) = @_;
2208 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2210 }elsif ( $format eq 'html' ) {
2214 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2216 sub { my ($d,$a,$s,$w) = @_;
2217 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2226 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2228 &{$column}( &{$f->{fields}->[$i]}(@args),
2229 map { $f->{$_}->[$i] } qw(align span width)
2233 $prefix. join( $separator, @result ). $suffix;
2238 =item _items_pkg [ OPTIONS ]
2240 Return line item hashes for each package item on this invoice. Nearly
2243 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2245 The only OPTIONS accepted is 'section', which may point to a hashref
2246 with a key named 'condensed', which may have a true value. If it
2247 does, this method tries to merge identical items into items with
2248 'quantity' equal to the number of items (not the sum of their
2249 separate quantities, for some reason).
2255 grep { $_->pkgnum } $self->cust_bill_pkg;
2262 warn "$me _items_pkg searching for all package line items\n"
2265 my @cust_bill_pkg = $self->_items_nontax;
2267 warn "$me _items_pkg filtering line items\n"
2269 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2271 if ($options{section} && $options{section}->{condensed}) {
2273 warn "$me _items_pkg condensing section\n"
2277 local $Storable::canonical = 1;
2278 foreach ( @items ) {
2280 delete $item->{ref};
2281 delete $item->{ext_description};
2282 my $key = freeze($item);
2283 $itemshash{$key} ||= 0;
2284 $itemshash{$key} ++; # += $item->{quantity};
2286 @items = sort { $a->{description} cmp $b->{description} }
2287 map { my $i = thaw($_);
2288 $i->{quantity} = $itemshash{$_};
2290 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2296 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2303 return 0 unless $a->itemdesc cmp $b->itemdesc;
2304 return -1 if $b->itemdesc eq 'Tax';
2305 return 1 if $a->itemdesc eq 'Tax';
2306 return -1 if $b->itemdesc eq 'Other surcharges';
2307 return 1 if $a->itemdesc eq 'Other surcharges';
2308 $a->itemdesc cmp $b->itemdesc;
2313 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2314 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2316 if ( $self->conf->exists('always_show_tax') ) {
2317 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2318 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2320 { 'description' => $itemdesc,
2327 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2329 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2330 list of hashrefs describing the line items they generate on the invoice.
2332 OPTIONS may include:
2334 format: the invoice format.
2336 escape_function: the function used to escape strings.
2338 DEPRECATED? (expensive, mostly unused?)
2339 format_function: the function used to format CDRs.
2341 section: a hashref containing 'category' and/or 'locationnum'; if this
2342 is present, only returns line items that belong to that category and/or
2343 location (whichever is defined).
2345 multisection: a flag indicating that this is a multisection invoice,
2346 which does something complicated.
2348 Returns a list of hashrefs, each of which may contain:
2350 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2351 ext_description, which is an arrayref of detail lines to show below
2356 sub _items_cust_bill_pkg {
2358 my $conf = $self->conf;
2359 my $cust_bill_pkgs = shift;
2362 my $format = $opt{format} || '';
2363 my $escape_function = $opt{escape_function} || sub { shift };
2364 my $format_function = $opt{format_function} || '';
2365 my $no_usage = $opt{no_usage} || '';
2366 my $unsquelched = $opt{unsquelched} || ''; #unused
2367 my ($section, $locationnum, $category);
2368 if ( $opt{section} ) {
2369 $category = $opt{section}->{category};
2370 $locationnum = $opt{section}->{locationnum};
2372 my $summary_page = $opt{summary_page} || ''; #unused
2373 my $multisection = defined($category) || defined($locationnum);
2374 my $discount_show_always = 0;
2376 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2378 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2379 # and location labels
2382 my ($s, $r, $u) = ( undef, undef, undef );
2383 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2386 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2387 if ( $_ && !$cust_bill_pkg->hidden ) {
2388 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2389 $_->{amount} =~ s/^\-0\.00$/0.00/;
2390 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2392 if $_->{amount} != 0
2393 || $discount_show_always
2394 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2395 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2401 if ( $locationnum ) {
2402 # this is a location section; skip packages that aren't at this
2404 next if $cust_bill_pkg->pkgnum == 0;
2405 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2409 # Consider display records for this item to determine if it belongs
2410 # in this section. Note that if there are no display records, there
2411 # will be a default pseudo-record that includes all charge types
2412 # and has no section name.
2413 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2414 ? $cust_bill_pkg->cust_bill_pkg_display
2415 : ( $cust_bill_pkg );
2417 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2418 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2421 if ( defined($category) ) {
2422 # then this is a package category section; process all display records
2423 # that belong to this section.
2424 @cust_bill_pkg_display = grep { $_->section eq $category }
2425 @cust_bill_pkg_display;
2427 # otherwise, process all display records that aren't usage summaries
2428 # (I don't think there should be usage summaries if you aren't using
2429 # category sections, but this is the historical behavior)
2430 @cust_bill_pkg_display = grep { !$_->summary }
2431 @cust_bill_pkg_display;
2433 foreach my $display (@cust_bill_pkg_display) {
2435 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2436 $display->billpkgdisplaynum. "\n"
2439 my $type = $display->type;
2441 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2442 $desc = substr($desc, 0, $maxlength). '...'
2443 if $format eq 'latex' && length($desc) > $maxlength;
2445 my %details_opt = ( 'format' => $format,
2446 'escape_function' => $escape_function,
2447 'format_function' => $format_function,
2448 'no_usage' => $opt{'no_usage'},
2451 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2453 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2456 if ( $cust_bill_pkg->setup != 0 ) {
2457 my $description = $desc;
2458 $description .= ' Setup'
2459 if $cust_bill_pkg->recur != 0
2460 || $discount_show_always
2461 || $cust_bill_pkg->recur_show_zero;
2463 'description' => $description,
2464 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2467 if ( $cust_bill_pkg->recur != 0 ) {
2469 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2470 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2474 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2476 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2479 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2480 my $part_pkg = $cust_pkg->part_pkg;
2482 # which pkgpart to show for display purposes?
2483 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2485 # start/end dates for invoice formats that do nonstandard
2487 my %item_dates = ();
2488 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2489 unless $part_pkg->option('disable_line_item_date_ranges',1);
2491 if ( (!$type || $type eq 'S')
2492 && ( $cust_bill_pkg->setup != 0
2493 || $cust_bill_pkg->setup_show_zero
2498 warn "$me _items_cust_bill_pkg adding setup\n"
2501 my $description = $desc;
2502 $description .= ' Setup'
2503 if $cust_bill_pkg->recur != 0
2504 || $discount_show_always
2505 || $cust_bill_pkg->recur_show_zero;
2507 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2509 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2510 # triggered by the recurring charge freq
2512 && $cust_bill_pkg->recur == 0
2513 && ! $cust_bill_pkg->recur_show_zero;
2517 unless ( $cust_pkg->part_pkg->hide_svc_detail
2518 || $cust_bill_pkg->hidden )
2521 my @svc_labels = map &{$escape_function}($_),
2522 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2523 push @d, @svc_labels
2524 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2525 $svc_label = $svc_labels[0];
2527 my $lnum = $cust_main ? $cust_main->ship_locationnum
2528 : $self->prospect_main->locationnum;
2529 # show the location label if it's not the customer's default
2530 # location, and we're not grouping items by location already
2531 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2532 my $loc = $cust_pkg->location_label;
2533 $loc = substr($loc, 0, $maxlength). '...'
2534 if $format eq 'latex' && length($loc) > $maxlength;
2535 push @d, &{$escape_function}($loc);
2538 } #unless hiding service details
2540 push @d, $cust_bill_pkg->details(%details_opt)
2541 if $cust_bill_pkg->recur == 0;
2543 if ( $cust_bill_pkg->hidden ) {
2544 $s->{amount} += $cust_bill_pkg->setup;
2545 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2546 push @{ $s->{ext_description} }, @d;
2550 description => $description,
2551 pkgpart => $pkgpart,
2552 pkgnum => $cust_bill_pkg->pkgnum,
2553 amount => $cust_bill_pkg->setup,
2554 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2555 unit_amount => $cust_bill_pkg->unitsetup,
2556 quantity => $cust_bill_pkg->quantity,
2557 ext_description => \@d,
2558 svc_label => ($svc_label || ''),
2564 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2566 $cust_bill_pkg->recur != 0
2567 || $cust_bill_pkg->setup == 0
2568 || $discount_show_always
2569 || $cust_bill_pkg->recur_show_zero
2574 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2577 my $is_summary = $display->summary;
2578 my $description = $desc;
2579 if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2580 $description = $self->mt('Usage charges');
2583 my $part_pkg = $cust_pkg->part_pkg;
2585 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2589 my @seconds = (); # for display of usage info
2592 #at least until cust_bill_pkg has "past" ranges in addition to
2593 #the "future" sdate/edate ones... see #3032
2594 my @dates = ( $self->_date );
2595 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2596 push @dates, $prev->sdate if $prev;
2597 push @dates, undef if !$prev;
2599 unless ( $part_pkg->hide_svc_detail
2600 || $cust_bill_pkg->itemdesc
2601 || $cust_bill_pkg->hidden
2602 || $is_summary && $type && $type eq 'U'
2606 warn "$me _items_cust_bill_pkg adding service details\n"
2609 my @svc_labels = map &{$escape_function}($_),
2610 $cust_pkg->h_labels_short(@dates, 'I');
2611 push @d, @svc_labels
2612 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2613 $svc_label = $svc_labels[0];
2615 warn "$me _items_cust_bill_pkg done adding service details\n"
2618 my $lnum = $cust_main ? $cust_main->ship_locationnum
2619 : $self->prospect_main->locationnum;
2620 # show the location label if it's not the customer's default
2621 # location, and we're not grouping items by location already
2622 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2623 my $loc = $cust_pkg->location_label;
2624 $loc = substr($loc, 0, $maxlength). '...'
2625 if $format eq 'latex' && length($loc) > $maxlength;
2626 push @d, &{$escape_function}($loc);
2629 # Display of seconds_since_sqlradacct:
2630 # On the invoice, when processing @detail_items, look for a field
2631 # named 'seconds'. This will contain total seconds for each
2632 # service, in the same order as @ext_description. For services
2633 # that don't support this it will show undef.
2634 if ( $conf->exists('svc_acct-usage_seconds')
2635 and ! $cust_bill_pkg->pkgpart_override ) {
2636 foreach my $cust_svc (
2637 $cust_pkg->h_cust_svc(@dates, 'I')
2640 # eval because not having any part_export_usage exports
2641 # is a fatal error, last_bill/_date because that's how
2642 # sqlradius_hour billing does it
2644 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2646 push @seconds, $sec;
2648 } #if svc_acct-usage_seconds
2652 unless ( $is_summary ) {
2653 warn "$me _items_cust_bill_pkg adding details\n"
2656 #instead of omitting details entirely in this case (unwanted side
2657 # effects), just omit CDRs
2658 $details_opt{'no_usage'} = 1
2659 if $type && $type eq 'R';
2661 push @d, $cust_bill_pkg->details(%details_opt);
2664 warn "$me _items_cust_bill_pkg calculating amount\n"
2669 $amount = $cust_bill_pkg->recur;
2670 } elsif ($type eq 'R') {
2671 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2672 } elsif ($type eq 'U') {
2673 $amount = $cust_bill_pkg->usage;
2677 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2680 if ( !$type || $type eq 'R' ) {
2682 warn "$me _items_cust_bill_pkg adding recur\n"
2685 if ( $cust_bill_pkg->hidden ) {
2686 $r->{amount} += $amount;
2687 $r->{unit_amount} += $unit_amount;
2688 push @{ $r->{ext_description} }, @d;
2691 description => $description,
2692 pkgpart => $pkgpart,
2693 pkgnum => $cust_bill_pkg->pkgnum,
2695 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2696 unit_amount => $unit_amount,
2697 quantity => $cust_bill_pkg->quantity,
2699 ext_description => \@d,
2700 svc_label => ($svc_label || ''),
2702 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2705 } else { # $type eq 'U'
2707 warn "$me _items_cust_bill_pkg adding usage\n"
2710 if ( $cust_bill_pkg->hidden and defined($u) ) {
2711 # if this is a hidden package and there's already a usage
2712 # line for the bundle, add this package's total amount and
2713 # usage details to it
2714 $u->{amount} += $amount;
2715 $u->{unit_amount} += $unit_amount,
2716 push @{ $u->{ext_description} }, @d;
2717 } elsif ( $amount ) {
2718 # create a new usage line
2720 description => $description,
2721 pkgpart => $pkgpart,
2722 pkgnum => $cust_bill_pkg->pkgnum,
2724 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2725 unit_amount => $unit_amount,
2726 quantity => $cust_bill_pkg->quantity,
2728 ext_description => \@d,
2730 } # else this has no usage, so don't create a usage section
2733 } # recurring or usage with recurring charge
2735 } else { #pkgnum tax or one-shot line item (??)
2737 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2740 if ( $cust_bill_pkg->setup != 0 ) {
2742 'description' => $desc,
2743 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2746 if ( $cust_bill_pkg->recur != 0 ) {
2748 'description' => "$desc (".
2749 $self->time2str_local('short', $cust_bill_pkg->sdate). ' - '.
2750 $self->time2str_local('short', $cust_bill_pkg->edate). ')',
2751 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2759 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2760 && $conf->exists('discount-show-always'));
2764 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2766 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2767 $_->{amount} =~ s/^\-0\.00$/0.00/;
2768 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2770 if $_->{amount} != 0
2771 || $discount_show_always
2772 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2773 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2777 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2784 =item _items_discounts_avail
2786 Returns an array of line item hashrefs representing available term discounts
2787 for this invoice. This makes the same assumptions that apply to term
2788 discounts in general: that the package is billed monthly, at a flat rate,
2789 with no usage charges. A prorated first month will be handled, as will
2790 a setup fee if the discount is allowed to apply to setup fees.
2794 sub _items_discounts_avail {
2797 #maybe move this method from cust_bill when quotations support discount_plans
2798 return () unless $self->can('discount_plans');
2799 my %plans = $self->discount_plans;
2801 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2802 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2806 my $plan = $plans{$months};
2808 my $term_total = sprintf('%.2f', $plan->discounted_total);
2809 my $percent = sprintf('%.0f',
2810 100 * (1 - $term_total / $plan->base_total) );
2811 my $permonth = sprintf('%.2f', $term_total / $months);
2812 my $detail = $self->mt('discount on item'). ' '.
2813 join(', ', map { "#$_" } $plan->pkgnums)
2816 # discounts for non-integer months don't work anyway
2817 $months = sprintf("%d", $months);
2820 description => $self->mt('Save [_1]% by paying for [_2] months',
2822 amount => $self->mt('[_1] ([_2] per month)',
2823 $term_total, $money_char.$permonth),
2824 ext_description => ($detail || ''),
2827 sort { $b <=> $a } keys %plans;