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 # workaround for inconsistent behavior in the early plain text
489 # templates; see RT#28271
490 'date' => ( $params{'no_date'}
492 : ($format eq 'template'
494 : $self->time2str_local('long', $self->_date, $format)
497 'today' => $self->time2str_local('long', $today, $format),
498 'terms' => $self->terms,
499 'template' => $template, #params{'template'},
500 'notice_name' => $notice_name, # escape?
501 'current_charges' => sprintf("%.2f", $self->charged),
502 'duedate' => $self->due_date2str('rdate'), #date_format?
505 'custnum' => $cust_main->display_custnum,
506 'prospectnum' => $cust_main->prospectnum,
507 'agent_custid' => &$escape_function($cust_main->agent_custid),
508 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
509 payname company address1 address2 city state zip fax
513 'ship_enable' => $conf->exists('invoice-ship_address'),
514 'unitprices' => $conf->exists('invoice-unitprice'),
515 'smallernotes' => $conf->exists('invoice-smallernotes'),
516 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
517 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
519 #layout info -- would be fancy to calc some of this and bury the template
521 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
522 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
523 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
524 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
525 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
526 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
527 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
528 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
529 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
530 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
532 # better hang on to conf_dir for a while (for old templates)
533 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
535 #these are only used when doing paged plaintext
542 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
543 # prototype here to silence warnings
544 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
546 my $min_sdate = 999999999999;
548 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
549 next unless $cust_bill_pkg->pkgnum > 0;
550 $min_sdate = $cust_bill_pkg->sdate
551 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
552 $max_edate = $cust_bill_pkg->edate
553 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
556 $invoice_data{'bill_period'} = '';
557 $invoice_data{'bill_period'} =
558 $self->time2str_local('%e %h', $min_sdate, $format)
560 $self->time2str_local('%e %h', $max_edate, $format)
561 if ($max_edate != 0 && $min_sdate != 999999999999);
563 $invoice_data{finance_section} = '';
564 if ( $conf->config('finance_pkgclass') ) {
566 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
567 $invoice_data{finance_section} = $pkg_class->categoryname;
569 $invoice_data{finance_amount} = '0.00';
570 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
572 my $countrydefault = $conf->config('countrydefault') || 'US';
573 foreach ( qw( address1 address2 city state zip country fax) ){
574 my $method = 'ship_'.$_;
575 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
577 if ( length($cust_main->ship_company) ) {
578 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
580 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
582 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
583 $invoice_data{'ship_country'} = ''
584 if ( $invoice_data{'ship_country'} eq $countrydefault );
586 $invoice_data{'cid'} = $params{'cid'}
589 if ( $cust_main->country eq $countrydefault ) {
590 $invoice_data{'country'} = '';
592 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
596 $invoice_data{'address'} = \@address;
599 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
600 ? " (P.O. #". $cust_main->payinfo. ")"
604 push @address, $cust_main->company
605 if $cust_main->company;
606 push @address, $cust_main->address1;
607 push @address, $cust_main->address2
608 if $cust_main->address2;
610 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
611 push @address, $invoice_data{'country'}
612 if $invoice_data{'country'};
614 while (scalar(@address) < 5);
616 $invoice_data{'logo_file'} = $params{'logo_file'}
617 if $params{'logo_file'};
618 $invoice_data{'barcode_file'} = $params{'barcode_file'}
619 if $params{'barcode_file'};
620 $invoice_data{'barcode_img'} = $params{'barcode_img'}
621 if $params{'barcode_img'};
622 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
623 if $params{'barcode_cid'};
625 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
626 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
627 #my $balance_due = $self->owed + $pr_total - $cr_total;
628 my $balance_due = $self->owed + $pr_total;
630 #these are used on the summary page only
632 # the customer's current balance as shown on the invoice before this one
633 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
635 # the change in balance from that invoice to this one
636 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
638 # the sum of amount owed on all previous invoices
639 # ($pr_total is used elsewhere but not as $previous_balance)
640 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
642 # the sum of amount owed on all invoices
643 # (this is used in the summary & on the payment coupon)
644 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
646 # info from customer's last invoice before this one, for some
648 $invoice_data{'last_bill'} = {};
650 if ( $self->custnum && $self->invnum ) {
652 if ( $self->previous_bill ) {
653 my $last_bill = $self->previous_bill;
654 $invoice_data{'last_bill'} = {
655 '_date' => $last_bill->_date, #unformatted
657 my (@payments, @credits);
658 # for formats that itemize previous payments
659 foreach my $cust_pay ( qsearch('cust_pay', {
660 'custnum' => $self->custnum,
661 '_date' => { op => '>=',
662 value => $last_bill->_date }
665 next if $cust_pay->_date > $self->_date;
667 '_date' => $cust_pay->_date,
668 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
669 'payinfo' => $cust_pay->payby_payinfo_pretty,
670 'amount' => sprintf('%.2f', $cust_pay->paid),
672 # not concerned about applications
674 foreach my $cust_credit ( qsearch('cust_credit', {
675 'custnum' => $self->custnum,
676 '_date' => { op => '>=',
677 value => $last_bill->_date }
680 next if $cust_credit->_date > $self->_date;
682 '_date' => $cust_credit->_date,
683 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
684 'creditreason'=> $cust_credit->reason,
685 'amount' => sprintf('%.2f', $cust_credit->amount),
688 $invoice_data{'previous_payments'} = \@payments;
689 $invoice_data{'previous_credits'} = \@credits;
694 my $summarypage = '';
695 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
698 $invoice_data{'summarypage'} = $summarypage;
700 warn "$me substituting variables in notes, footer, smallfooter\n"
703 my $tc = $self->template_conf;
704 my @include = ( [ $tc, 'notes' ],
705 [ 'invoice_', 'footer' ],
706 [ 'invoice_', 'smallfooter', ],
708 push @include, [ $tc, 'coupon', ]
709 unless $params{'no_coupon'};
711 foreach my $i (@include) {
713 my($base, $include) = @$i;
715 my $inc_file = $conf->key_orbase("$base$format$include", $template);
718 if ( $conf->exists($inc_file, $agentnum)
719 && length( $conf->config($inc_file, $agentnum) ) ) {
721 @inc_src = $conf->config($inc_file, $agentnum);
725 $inc_file = $conf->key_orbase("${base}latex$include", $template);
727 my $convert_map = $convert_maps{$format}{$include};
729 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
730 s/--\@\]/$delimiters{$format}[1]/g;
733 &$convert_map( $conf->config($inc_file, $agentnum) );
737 my $inc_tt = new Text::Template (
739 SOURCE => [ map "$_\n", @inc_src ],
740 DELIMITERS => $delimiters{$format},
741 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
743 unless ( $inc_tt->compile() ) {
744 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
745 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
749 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
751 $invoice_data{$include} =~ s/\n+$//
752 if ($format eq 'latex');
755 # let invoices use either of these as needed
756 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
757 ? $cust_main->payinfo : '';
758 $invoice_data{'po_line'} =
759 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
760 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
763 my %money_chars = ( 'latex' => '',
764 'html' => $conf->config('money_char') || '$',
767 my $money_char = $money_chars{$format};
769 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
770 'html' => $conf->config('money_char') || '$',
773 my $other_money_char = $other_money_chars{$format};
774 $invoice_data{'dollar'} = $other_money_char;
776 my %minus_signs = ( 'latex' => '$-$',
778 'template' => '- ' );
779 my $minus = $minus_signs{$format};
781 my @detail_items = ();
782 my @total_items = ();
786 $invoice_data{'detail_items'} = \@detail_items;
787 $invoice_data{'total_items'} = \@total_items;
788 $invoice_data{'buf'} = \@buf;
789 $invoice_data{'sections'} = \@sections;
791 warn "$me generating sections\n"
795 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
796 'subtotal' => $taxtotal, # adjusted below
799 my $tax_weight = _pkg_category($tax_section->{description})
800 ? _pkg_category($tax_section->{description})->weight
802 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
803 $tax_section->{'sort_weight'} = $tax_weight;
806 my $adjust_section = {
807 'description' => $self->mt('Credits, Payments, and Adjustments'),
808 'adjust_section' => 1,
809 'subtotal' => 0, # adjusted below
811 my $adjust_weight = _pkg_category($adjust_section->{description})
812 ? _pkg_category($adjust_section->{description})->weight
814 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
815 $adjust_section->{'sort_weight'} = $adjust_weight;
817 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
818 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
819 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
820 $invoice_data{'multisection'} = $multisection;
822 my $extra_sections = [];
823 my $extra_lines = ();
825 # default section ('Charges')
826 my $default_section = { 'description' => '',
831 # Previous Charges section
832 # subtotal is the first return value from $self->previous
833 my $previous_section;
834 # if the invoice has major sections, or if we're summarizing previous
835 # charges with a single line, or if we've been specifically told to put them
836 # in a section, create a section for previous charges:
837 if ( $multisection or
838 $conf->exists('previous_balance-summary_only') or
839 $conf->exists('previous_balance-section') ) {
841 $previous_section = { 'description' => $self->mt('Previous Charges'),
842 'subtotal' => $other_money_char.
843 sprintf('%.2f', $pr_total),
844 'summarized' => '', #why? $summarypage ? 'Y' : '',
846 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
847 join(' / ', map { $cust_main->balance_date_range(@$_) }
848 $self->_prior_month30s
850 if $conf->exists('invoice_include_aging');
853 # otherwise put them in the main section
854 $previous_section = $default_section;
857 if ( $multisection ) {
858 ($extra_sections, $extra_lines) =
859 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
860 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
861 && $self->can('_items_extra_usage_sections');
863 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
865 push @detail_items, @$extra_lines if $extra_lines;
867 # the code is written so that both methods can be used together, but
868 # we haven't yet changed the template to take advantage of that, so for
869 # now, treat them as mutually exclusive.
870 my %section_method = ( by_category => 1 );
871 if ( $conf->exists($tc.'sections_by_location') ) {
872 %section_method = ( by_location => 1 );
875 $self->_items_sections( 'summary' => $summarypage,
876 'escape' => $escape_function_nonbsp,
877 'extra_sections' => $extra_sections,
881 push @sections, @$early;
882 $late_sections = $late;
884 if ( $conf->exists('svc_phone_sections')
885 && $self->can('_items_svc_phone_sections')
888 my ($phone_sections, $phone_lines) =
889 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
890 push @{$late_sections}, @$phone_sections;
891 push @detail_items, @$phone_lines;
893 if ( $conf->exists('voip-cust_accountcode_cdr')
894 && $cust_main->accountcode_cdr
895 && $self->can('_items_accountcode_cdr')
898 my ($accountcode_section, $accountcode_lines) =
899 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
900 if ( scalar(@$accountcode_lines) ) {
901 push @{$late_sections}, $accountcode_section;
902 push @detail_items, @$accountcode_lines;
905 } else {# not multisection
906 # make a default section
907 push @sections, $default_section;
908 # and calculate the finance charge total, since it won't get done otherwise.
909 # and the default section total
910 # XXX possibly finance_pkgclass should not be used in this manner?
913 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
914 if ( $invoice_data{finance_section} and
915 grep { $_->section eq $invoice_data{finance_section} }
916 $cust_bill_pkg->cust_bill_pkg_display ) {
917 # I think these are always setup fees, but just to be sure...
918 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
920 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
923 $invoice_data{finance_amount} =
924 sprintf('%.2f', sum( @finance_charges ) || 0);
925 $default_section->{subtotal} = $other_money_char.
926 sprintf('%.2f', sum( @charges ) || 0);
929 # previous invoice balances in the Previous Charges section if there
930 # is one, otherwise in the main detail section
931 # (except if summary_only is enabled, don't show them at all)
932 if ( $self->can('_items_previous') &&
933 $self->enable_previous &&
934 ! $conf->exists('previous_balance-summary_only') ) {
936 warn "$me adding previous balances\n"
939 foreach my $line_item ( $self->_items_previous ) {
942 ref => $line_item->{'pkgnum'},
943 pkgpart => $line_item->{'pkgpart'},
944 #quantity => 1, # not really correct
945 section => $previous_section, # which might be $default_section
946 description => &$escape_function($line_item->{'description'}),
947 ext_description => [ map { &$escape_function($_) }
948 @{ $line_item->{'ext_description'} || [] }
950 amount => ( $old_latex ? '' : $money_char).
951 $line_item->{'amount'},
952 product_code => $line_item->{'pkgpart'} || 'N/A',
955 push @detail_items, $detail;
956 push @buf, [ $detail->{'description'},
957 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
963 if ( @pr_cust_bill && $self->enable_previous ) {
964 push @buf, ['','-----------'];
965 push @buf, [ $self->mt('Total Previous Balance'),
966 $money_char. sprintf("%10.2f", $pr_total) ];
970 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
971 warn "$me adding DID summary\n"
974 my ($didsummary,$minutes) = $self->_did_summary;
975 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
977 { 'description' => $didsummary_desc,
978 'ext_description' => [ $didsummary, $minutes ],
982 foreach my $section (@sections, @$late_sections) {
984 # begin some normalization
985 $section->{'subtotal'} = $section->{'amount'}
987 && !exists($section->{subtotal})
988 && exists($section->{amount});
990 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
991 if ( $invoice_data{finance_section} &&
992 $section->{'description'} eq $invoice_data{finance_section} );
994 $section->{'subtotal'} = $other_money_char.
995 sprintf('%.2f', $section->{'subtotal'})
998 # continue some normalization
999 $section->{'amount'} = $section->{'subtotal'}
1003 if ( $section->{'description'} ) {
1004 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1009 warn "$me setting options\n"
1013 $options{'section'} = $section if $multisection;
1014 $options{'format'} = $format;
1015 $options{'escape_function'} = $escape_function;
1016 $options{'no_usage'} = 1 unless $unsquelched;
1017 $options{'unsquelched'} = $unsquelched;
1018 $options{'summary_page'} = $summarypage;
1019 $options{'skip_usage'} =
1020 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1022 warn "$me searching for line items\n"
1025 foreach my $line_item ( $self->_items_pkg(%options) ) {
1027 warn "$me adding line item $line_item\n"
1031 ext_description => [],
1033 $detail->{'ref'} = $line_item->{'pkgnum'};
1034 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1035 $detail->{'quantity'} = $line_item->{'quantity'};
1036 $detail->{'section'} = $section;
1037 $detail->{'description'} = &$escape_function($line_item->{'description'});
1038 if ( exists $line_item->{'ext_description'} ) {
1039 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1041 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1042 $line_item->{'amount'};
1043 if ( exists $line_item->{'unit_amount'} ) {
1044 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1045 $line_item->{'unit_amount'};
1047 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1049 $detail->{'sdate'} = $line_item->{'sdate'};
1050 $detail->{'edate'} = $line_item->{'edate'};
1051 $detail->{'seconds'} = $line_item->{'seconds'};
1052 $detail->{'svc_label'} = $line_item->{'svc_label'};
1054 push @detail_items, $detail;
1055 push @buf, ( [ $detail->{'description'},
1056 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1058 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1062 if ( $section->{'description'} ) {
1063 push @buf, ( ['','-----------'],
1064 [ $section->{'description'}. ' sub-total',
1065 $section->{'subtotal'} # already formatted this
1074 $invoice_data{current_less_finance} =
1075 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1077 # if there's anything in the Previous Charges section, prepend it to the list
1078 if ( $pr_total and $previous_section ne $default_section ) {
1079 unshift @sections, $previous_section;
1082 warn "$me adding taxes\n"
1085 my @items_tax = $self->_items_tax;
1086 foreach my $tax ( @items_tax ) {
1088 $taxtotal += $tax->{'amount'};
1090 my $description = &$escape_function( $tax->{'description'} );
1091 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1093 if ( $multisection ) {
1095 my $money = $old_latex ? '' : $money_char;
1096 push @detail_items, {
1097 ext_description => [],
1100 description => $description,
1101 amount => $money. $amount,
1103 section => $tax_section,
1108 push @total_items, {
1109 'total_item' => $description,
1110 'total_amount' => $other_money_char. $amount,
1115 push @buf,[ $description,
1116 $money_char. $amount,
1123 $total->{'total_item'} = $self->mt('Sub-total');
1124 $total->{'total_amount'} =
1125 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1127 if ( $multisection ) {
1128 $tax_section->{'subtotal'} = $other_money_char.
1129 sprintf('%.2f', $taxtotal);
1130 $tax_section->{'pretotal'} = 'New charges sub-total '.
1131 $total->{'total_amount'};
1132 push @sections, $tax_section if $taxtotal;
1134 unshift @total_items, $total;
1137 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1139 push @buf,['','-----------'];
1140 push @buf,[$self->mt(
1141 (!$self->enable_previous)
1143 : 'Total New Charges'
1145 $money_char. sprintf("%10.2f",$self->charged) ];
1153 my %embolden_functions = (
1154 'latex' => sub { return '\textbf{'. shift(). '}' },
1155 'html' => sub { return '<b>'. shift(). '</b>' },
1156 'template' => sub { shift },
1158 my $embolden_function = $embolden_functions{$format};
1160 if ( $self->can('_items_total') ) { # quotations
1162 $self->_items_total(\@total_items);
1164 foreach ( @total_items ) {
1165 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1166 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1167 $_->{'total_amount'}
1171 } else { #normal invoice case
1173 # calculate total, possibly including total owed on previous
1177 $item = $conf->config('previous_balance-exclude_from_total')
1178 || 'Total New Charges'
1179 if $conf->exists('previous_balance-exclude_from_total');
1180 my $amount = $self->charged;
1181 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1182 $amount += $pr_total;
1185 $total->{'total_item'} = &$embolden_function($self->mt($item));
1186 $total->{'total_amount'} =
1187 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1188 if ( $multisection ) {
1189 if ( $adjust_section->{'sort_weight'} ) {
1190 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1191 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1193 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1194 $other_money_char. sprintf('%.2f', $self->charged );
1197 push @total_items, $total;
1199 push @buf,['','-----------'];
1202 sprintf( '%10.2f', $amount )
1206 # if we're showing previous invoices, also show previous
1207 # credits and payments
1208 if ( $self->enable_previous
1209 and $self->can('_items_credits')
1210 and $self->can('_items_payments') )
1212 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1215 my $credittotal = 0;
1216 foreach my $credit (
1217 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1221 $total->{'total_item'} = &$escape_function($credit->{'description'});
1222 $credittotal += $credit->{'amount'};
1223 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1224 $adjusttotal += $credit->{'amount'};
1225 if ( $multisection ) {
1226 my $money = $old_latex ? '' : $money_char;
1227 push @detail_items, {
1228 ext_description => [],
1231 description => &$escape_function($credit->{'description'}),
1232 amount => $money. $credit->{'amount'},
1234 section => $adjust_section,
1237 push @total_items, $total;
1241 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1244 foreach my $credit (
1245 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1247 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1251 my $paymenttotal = 0;
1252 foreach my $payment (
1253 $self->_items_payments( 'template' => $template )
1256 $total->{'total_item'} = &$escape_function($payment->{'description'});
1257 $paymenttotal += $payment->{'amount'};
1258 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1259 $adjusttotal += $payment->{'amount'};
1260 if ( $multisection ) {
1261 my $money = $old_latex ? '' : $money_char;
1262 push @detail_items, {
1263 ext_description => [],
1266 description => &$escape_function($payment->{'description'}),
1267 amount => $money. $payment->{'amount'},
1269 section => $adjust_section,
1272 push @total_items, $total;
1274 push @buf, [ $payment->{'description'},
1275 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1278 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1280 if ( $multisection ) {
1281 $adjust_section->{'subtotal'} = $other_money_char.
1282 sprintf('%.2f', $adjusttotal);
1283 push @sections, $adjust_section
1284 unless $adjust_section->{sort_weight};
1287 # create Balance Due message
1290 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1291 $total->{'total_amount'} =
1292 &$embolden_function(
1293 $other_money_char. sprintf('%.2f', #why? $summarypage
1294 # ? $self->charged +
1295 # $self->billing_balance
1297 $self->owed + $pr_total
1300 if ( $multisection && !$adjust_section->{sort_weight} ) {
1301 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1302 $total->{'total_amount'};
1304 push @total_items, $total;
1306 push @buf,['','-----------'];
1307 push @buf,[$self->balance_due_msg, $money_char.
1308 sprintf("%10.2f", $balance_due ) ];
1311 if ( $conf->exists('previous_balance-show_credit')
1312 and $cust_main->balance < 0 ) {
1313 my $credit_total = {
1314 'total_item' => &$embolden_function($self->credit_balance_msg),
1315 'total_amount' => &$embolden_function(
1316 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1319 if ( $multisection ) {
1320 $adjust_section->{'posttotal'} .= $newline_token .
1321 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1324 push @total_items, $credit_total;
1326 push @buf,['','-----------'];
1327 push @buf,[$self->credit_balance_msg, $money_char.
1328 sprintf("%10.2f", -$cust_main->balance ) ];
1332 } #end of default total adding ! can('_items_total')
1334 if ( $multisection ) {
1335 if ( $conf->exists('svc_phone_sections')
1336 && $self->can('_items_svc_phone_sections')
1340 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1341 $total->{'total_amount'} =
1342 &$embolden_function(
1343 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1345 my $last_section = pop @sections;
1346 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1347 $total->{'total_amount'};
1348 push @sections, $last_section;
1350 push @sections, @$late_sections
1354 # make a discounts-available section, even without multisection
1355 if ( $conf->exists('discount-show_available')
1356 and my @discounts_avail = $self->_items_discounts_avail ) {
1357 my $discount_section = {
1358 'description' => $self->mt('Discounts Available'),
1363 push @sections, $discount_section;
1364 push @detail_items, map { +{
1365 'ref' => '', #should this be something else?
1366 'section' => $discount_section,
1367 'description' => &$escape_function( $_->{description} ),
1368 'amount' => $money_char . &$escape_function( $_->{amount} ),
1369 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1370 } } @discounts_avail;
1373 my @summary_subtotals;
1374 # the templates say "$_->{tax_section} || !$_->{summarized}"
1375 # except 'summarized' is only true when tax_section is true, so this
1376 # is always true, so what's the deal?
1377 foreach my $s (@sections) {
1378 # not to include in the "summary of new charges" block:
1379 # finance charges, adjustments, previous charges,
1380 # and itemized phone usage sections
1381 if ( $s eq $adjust_section or
1382 ($s eq $previous_section and $s ne $default_section) or
1383 ($invoice_data{'finance_section'} and
1384 $invoice_data{'finance_section'} eq $s->{description}) or
1385 $s->{'description'} =~ /^\d+ $/ ) {
1388 push @summary_subtotals, $s;
1390 $invoice_data{summary_subtotals} = \@summary_subtotals;
1392 # debugging hook: call this with 'diag' => 1 to just get a hash of
1393 # the invoice variables
1394 return \%invoice_data if ( $params{'diag'} );
1396 # All sections and items are built; now fill in templates.
1397 my @includelist = ();
1398 push @includelist, 'summary' if $summarypage;
1399 foreach my $include ( @includelist ) {
1401 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1404 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1406 @inc_src = $conf->config($inc_file, $agentnum);
1410 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1412 my $convert_map = $convert_maps{$format}{$include};
1414 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1415 s/--\@\]/$delimiters{$format}[1]/g;
1418 &$convert_map( $conf->config($inc_file, $agentnum) );
1422 my $inc_tt = new Text::Template (
1424 SOURCE => [ map "$_\n", @inc_src ],
1425 DELIMITERS => $delimiters{$format},
1426 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1428 unless ( $inc_tt->compile() ) {
1429 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1430 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1434 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1436 $invoice_data{$include} =~ s/\n+$//
1437 if ($format eq 'latex');
1442 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1443 /invoice_lines\((\d*)\)/;
1444 $invoice_lines += $1 || scalar(@buf);
1447 die "no invoice_lines() functions in template?"
1448 if ( $format eq 'template' && !$wasfunc );
1450 if ($format eq 'template') {
1452 if ( $invoice_lines ) {
1453 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1454 $invoice_data{'total_pages'}++
1455 if scalar(@buf) % $invoice_lines;
1458 #setup subroutine for the template
1459 $invoice_data{invoice_lines} = sub {
1460 my $lines = shift || scalar(@buf);
1472 push @collect, split("\n",
1473 $text_template->fill_in( HASH => \%invoice_data )
1475 $invoice_data{'page'}++;
1477 map "$_\n", @collect;
1479 } else { # this is where we actually create the invoice
1481 warn "filling in template for invoice ". $self->invnum. "\n"
1483 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1486 $text_template->fill_in(HASH => \%invoice_data);
1490 sub notice_name { '('.shift->table.')'; }
1492 sub template_conf { 'invoice_'; }
1494 # helper routine for generating date ranges
1495 sub _prior_month30s {
1498 [ 1, 2592000 ], # 0-30 days ago
1499 [ 2592000, 5184000 ], # 30-60 days ago
1500 [ 5184000, 7776000 ], # 60-90 days ago
1501 [ 7776000, 0 ], # 90+ days ago
1504 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1505 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1510 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1512 Returns an postscript invoice, as a scalar.
1514 Options can be passed as a hashref (recommended) or as a list of time, template
1515 and then any key/value pairs for any other options.
1517 I<time> an optional value used to control the printing of overdue messages. The
1518 default is now. It isn't the date of the invoice; that's the `_date' field.
1519 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1520 L<Time::Local> and L<Date::Parse> for conversion functions.
1522 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1529 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1530 my $ps = generate_ps($file);
1532 unlink($barcodefile) if $barcodefile;
1537 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1539 Returns an PDF invoice, as a scalar.
1541 Options can be passed as a hashref (recommended) or as a list of time, template
1542 and then any key/value pairs for any other options.
1544 I<time> an optional value used to control the printing of overdue messages. The
1545 default is now. It isn't the date of the invoice; that's the `_date' field.
1546 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1547 L<Time::Local> and L<Date::Parse> for conversion functions.
1549 I<template>, if specified, is the name of a suffix for alternate invoices.
1551 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1558 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1559 my $pdf = generate_pdf($file);
1561 unlink($barcodefile) if $barcodefile;
1566 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1568 Returns an HTML invoice, as a scalar.
1570 I<time> an optional value used to control the printing of overdue messages. The
1571 default is now. It isn't the date of the invoice; that's the `_date' field.
1572 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1573 L<Time::Local> and L<Date::Parse> for conversion functions.
1575 I<template>, if specified, is the name of a suffix for alternate invoices.
1577 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1579 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1580 when emailing the invoice as part of a multipart/related MIME email.
1588 %params = %{ shift() };
1592 $params{'format'} = 'html';
1594 $self->print_generic( %params );
1597 # quick subroutine for print_latex
1599 # There are ten characters that LaTeX treats as special characters, which
1600 # means that they do not simply typeset themselves:
1601 # # $ % & ~ _ ^ \ { }
1603 # TeX ignores blanks following an escaped character; if you want a blank (as
1604 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1608 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1609 $value =~ s/([<>])/\$$1\$/g;
1615 encode_entities($value);
1619 sub _html_escape_nbsp {
1620 my $value = _html_escape(shift);
1621 $value =~ s/ +/ /g;
1625 #utility methods for print_*
1627 sub _translate_old_latex_format {
1628 warn "_translate_old_latex_format called\n"
1635 if ( $line =~ /^%%Detail\s*$/ ) {
1637 push @template, q![@--!,
1638 q! foreach my $_tr_line (@detail_items) {!,
1639 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1640 q! $_tr_line->{'description'} .= !,
1641 q! "\\tabularnewline\n~~".!,
1642 q! join( "\\tabularnewline\n~~",!,
1643 q! @{$_tr_line->{'ext_description'}}!,
1647 while ( ( my $line_item_line = shift )
1648 !~ /^%%EndDetail\s*$/ ) {
1649 $line_item_line =~ s/'/\\'/g; # nice LTS
1650 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1651 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1652 push @template, " \$OUT .= '$line_item_line';";
1655 push @template, '}',
1658 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1660 push @template, '[@--',
1661 ' foreach my $_tr_line (@total_items) {';
1663 while ( ( my $total_item_line = shift )
1664 !~ /^%%EndTotalDetails\s*$/ ) {
1665 $total_item_line =~ s/'/\\'/g; # nice LTS
1666 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1667 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1668 push @template, " \$OUT .= '$total_item_line';";
1671 push @template, '}',
1675 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1676 push @template, $line;
1682 warn "$_\n" foreach @template;
1690 my $conf = $self->conf;
1692 #check for an invoice-specific override
1693 return $self->invoice_terms if $self->invoice_terms;
1695 #check for a customer- specific override
1696 my $cust_main = $self->cust_main;
1697 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1699 #use configured default
1700 $conf->config('invoice_default_terms') || '';
1706 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1707 $duedate = $self->_date() + ( $1 * 86400 );
1714 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1717 sub balance_due_msg {
1719 my $msg = $self->mt('Balance Due');
1720 return $msg unless $self->terms;
1721 if ( $self->due_date ) {
1722 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1723 $self->due_date2str('short');
1724 } elsif ( $self->terms ) {
1725 $msg .= ' - '. $self->terms;
1730 sub balance_due_date {
1732 my $conf = $self->conf;
1734 if ( $conf->exists('invoice_default_terms')
1735 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1736 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1741 sub credit_balance_msg {
1743 $self->mt('Credit Balance Remaining')
1748 Returns a string with the date, for example: "3/20/2008"
1754 $self->time2str_local('short', $self->_date);
1757 =item _items_sections OPTIONS
1759 Generate section information for all items appearing on this invoice.
1760 This will only be called for multi-section invoices.
1762 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1763 related display records (L<FS::cust_bill_pkg_display>) and organize
1764 them into two groups ("early" and "late" according to whether they come
1765 before or after the total), then into sections. A subtotal is calculated
1768 Section descriptions are returned in sort weight order. Each consists
1769 of a hash containing:
1771 description: the package category name, escaped
1772 subtotal: the total charges in that section
1773 tax_section: a flag indicating that the section contains only tax charges
1774 summarized: same as tax_section, for some reason
1775 sort_weight: the package category's sort weight
1777 If 'condense' is set on the display record, it also contains everything
1778 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1779 coderefs to generate parts of the invoice. This is not advised.
1781 The method returns two arrayrefs, one of "early" sections and one of "late"
1784 OPTIONS may include:
1786 by_location: a flag to divide the invoice into sections by location.
1787 Each section hash will have a 'location' element containing a hashref of
1788 the location fields (see L<FS::cust_location>). The section description
1789 will be the location label, but the template can use any of the location
1790 fields to create a suitable label.
1792 by_category: a flag to divide the invoice into sections using display
1793 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1794 behavior. Each section hash will have a 'category' element containing
1795 the section name from the display record (which probably equals the
1796 category name of the package, but may not in some cases).
1798 summary: a flag indicating that this is a summary-format invoice.
1799 Turning this on has the following effects:
1800 - Ignores display items with the 'summary' flag.
1801 - Places all sections in the "early" group even if they have post_total.
1802 - Creates sections for all non-disabled package categories, even if they
1803 have no charges on this invoice, as well as a section with no name.
1805 escape: an escape function to use for section titles.
1807 extra_sections: an arrayref of additional sections to return after the
1808 sorted list. If there are any of these, section subtotals exclude
1811 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1812 passed through to C<_condense_section()>.
1816 use vars qw(%pkg_category_cache);
1817 sub _items_sections {
1821 my $escape = $opt{escape};
1822 my @extra_sections = @{ $opt{extra_sections} || [] };
1824 # $subtotal{$locationnum}{$categoryname} = amount.
1825 # if we're not using by_location, $locationnum is undef.
1826 # if we're not using by_category, you guessed it, $categoryname is undef.
1827 # if we're not using either one, we shouldn't be here in the first place...
1829 my %late_subtotal = ();
1832 # About tax items + multisection invoices:
1833 # If either invoice_*summary option is enabled, AND there is a
1834 # package category with the name of the tax, then there will be
1835 # a display record assigning the tax item to that category.
1837 # However, the taxes are always placed in the "Taxes, Surcharges,
1838 # and Fees" section regardless of that. The only effect of the
1839 # display record is to create a subtotal for the summary page.
1842 my $pkg_hash = $self->cust_pkg_hash;
1844 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1847 my $usage = $cust_bill_pkg->usage;
1850 if ( $opt{by_location} ) {
1851 if ( $cust_bill_pkg->pkgnum ) {
1852 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1857 $locationnum = undef;
1860 # as in _items_cust_pkg, if a line item has no display records,
1861 # cust_bill_pkg_display() returns a default record for it
1863 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1864 next if ( $display->summary && $opt{summary} );
1866 my $section = $display->section;
1867 my $type = $display->type;
1868 $section = undef unless $opt{by_category};
1870 $not_tax{$locationnum}{$section} = 1
1871 unless $cust_bill_pkg->pkgnum == 0;
1873 # there's actually a very important piece of logic buried in here:
1874 # incrementing $late_subtotal{$section} CREATES
1875 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1876 # to define the list of late sections, and likewise keys(%subtotal).
1877 # When _items_cust_bill_pkg is called to generate line items for
1878 # real, it will be called with 'section' => $section for each
1880 if ( $display->post_total && !$opt{summary} ) {
1881 if (! $type || $type eq 'S') {
1882 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1883 if $cust_bill_pkg->setup != 0
1884 || $cust_bill_pkg->setup_show_zero;
1888 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1889 if $cust_bill_pkg->recur != 0
1890 || $cust_bill_pkg->recur_show_zero;
1893 if ($type && $type eq 'R') {
1894 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1895 if $cust_bill_pkg->recur != 0
1896 || $cust_bill_pkg->recur_show_zero;
1899 if ($type && $type eq 'U') {
1900 $late_subtotal{$locationnum}{$section} += $usage
1901 unless scalar(@extra_sections);
1904 } else { # it's a pre-total (normal) section
1906 # skip tax items unless they're explicitly included in a section
1907 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1909 if (! $type || $type eq 'S') {
1910 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1911 if $cust_bill_pkg->setup != 0
1912 || $cust_bill_pkg->setup_show_zero;
1916 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1917 if $cust_bill_pkg->recur != 0
1918 || $cust_bill_pkg->recur_show_zero;
1921 if ($type && $type eq 'R') {
1922 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1923 if $cust_bill_pkg->recur != 0
1924 || $cust_bill_pkg->recur_show_zero;
1927 if ($type && $type eq 'U') {
1928 $subtotal{$locationnum}{$section} += $usage
1929 unless scalar(@extra_sections);
1938 %pkg_category_cache = ();
1940 # summary invoices need subtotals for all non-disabled package categories,
1941 # even if they're zero
1942 # but currently assume that there are no location sections, or at least
1943 # that the summary page doesn't care about them
1944 if ( $opt{summary} ) {
1945 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1946 $subtotal{''}{$category->categoryname} ||= 0;
1948 $subtotal{''}{''} ||= 0;
1952 foreach my $post_total (0,1) {
1954 my $s = $post_total ? \%late_subtotal : \%subtotal;
1955 foreach my $locationnum (keys %$s) {
1956 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1958 'subtotal' => $s->{$locationnum}{$sectionname},
1959 'post_total' => $post_total,
1962 if ( $locationnum ) {
1963 $section->{'locationnum'} = $locationnum;
1964 my $location = FS::cust_location->by_key($locationnum);
1965 $section->{'description'} = &{ $escape }($location->location_label);
1966 # Better ideas? This will roughly group them by proximity,
1967 # which alpha sorting on any of the address fields won't.
1968 # Sorting by locationnum is meaningless.
1969 # We have to sort on _something_ or the order may change
1970 # randomly from one invoice to the next, which will confuse
1972 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1974 $section->{'location'} = {
1975 map { $_ => &{ $escape }($location->get($_)) }
1979 $section->{'category'} = $sectionname;
1980 $section->{'description'} = &{ $escape }($sectionname);
1981 if ( _pkg_category($_) ) {
1982 $section->{'sort_weight'} = _pkg_category($_)->weight;
1983 if ( _pkg_category($_)->condense ) {
1984 $section = { %$section, $self->_condense_section($opt{format}) };
1988 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1989 # then it's a tax-only section
1990 $section->{'summarized'} = 'Y';
1991 $section->{'tax_section'} = 'Y';
1993 push @these, $section;
1994 } # foreach $sectionname
1995 } #foreach $locationnum
1996 push @these, @extra_sections if $post_total == 0;
1997 # need an alpha sort for location sections, because postal codes can
1999 $sections[ $post_total ] = [ sort {
2000 $opt{'by_location'} ?
2001 ($a->{sort_weight} cmp $b->{sort_weight}) :
2002 ($a->{sort_weight} <=> $b->{sort_weight})
2004 } #foreach $post_total
2006 return @sections; # early, late
2009 #helper subs for above
2013 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2017 my $categoryname = shift;
2018 $pkg_category_cache{$categoryname} ||=
2019 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2022 my %condensed_format = (
2023 'label' => [ qw( Description Qty Amount ) ],
2025 sub { shift->{description} },
2026 sub { shift->{quantity} },
2027 sub { my($href, %opt) = @_;
2028 ($opt{dollar} || ''). $href->{amount};
2031 'align' => [ qw( l r r ) ],
2032 'span' => [ qw( 5 1 1 ) ], # unitprices?
2033 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2036 sub _condense_section {
2037 my ( $self, $format ) = ( shift, shift );
2039 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2040 qw( description_generator
2043 total_line_generator
2048 sub _condensed_generator_defaults {
2049 my ( $self, $format ) = ( shift, shift );
2050 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2059 sub _condensed_header_generator {
2060 my ( $self, $format ) = ( shift, shift );
2062 my ( $f, $prefix, $suffix, $separator, $column ) =
2063 _condensed_generator_defaults($format);
2065 if ($format eq 'latex') {
2066 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2067 $suffix = "\\\\\n\\hline";
2070 sub { my ($d,$a,$s,$w) = @_;
2071 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2073 } elsif ( $format eq 'html' ) {
2074 $prefix = '<th></th>';
2078 sub { my ($d,$a,$s,$w) = @_;
2079 return qq!<th align="$html_align{$a}">$d</th>!;
2087 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2089 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2092 $prefix. join($separator, @result). $suffix;
2097 sub _condensed_description_generator {
2098 my ( $self, $format ) = ( shift, shift );
2100 my ( $f, $prefix, $suffix, $separator, $column ) =
2101 _condensed_generator_defaults($format);
2103 my $money_char = '$';
2104 if ($format eq 'latex') {
2105 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2107 $separator = " & \n";
2109 sub { my ($d,$a,$s,$w) = @_;
2110 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2112 $money_char = '\\dollar';
2113 }elsif ( $format eq 'html' ) {
2114 $prefix = '"><td align="center"></td>';
2118 sub { my ($d,$a,$s,$w) = @_;
2119 return qq!<td align="$html_align{$a}">$d</td>!;
2121 #$money_char = $conf->config('money_char') || '$';
2122 $money_char = ''; # this is madness
2130 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2132 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2134 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2135 map { $f->{$_}->[$i] } qw(align span width)
2139 $prefix. join( $separator, @result ). $suffix;
2144 sub _condensed_total_generator {
2145 my ( $self, $format ) = ( shift, shift );
2147 my ( $f, $prefix, $suffix, $separator, $column ) =
2148 _condensed_generator_defaults($format);
2151 if ($format eq 'latex') {
2154 $separator = " & \n";
2156 sub { my ($d,$a,$s,$w) = @_;
2157 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2159 }elsif ( $format eq 'html' ) {
2163 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2165 sub { my ($d,$a,$s,$w) = @_;
2166 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2175 # my $r = &{$f->{fields}->[$i]}(@args);
2176 # $r .= ' Total' unless $i;
2178 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2180 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2181 map { $f->{$_}->[$i] } qw(align span width)
2185 $prefix. join( $separator, @result ). $suffix;
2190 =item total_line_generator FORMAT
2192 Returns a coderef used for generation of invoice total line items for this
2193 usage_class. FORMAT is either html or latex
2197 # should not be used: will have issues with hash element names (description vs
2198 # total_item and amount vs total_amount -- another array of functions?
2200 sub _condensed_total_line_generator {
2201 my ( $self, $format ) = ( shift, shift );
2203 my ( $f, $prefix, $suffix, $separator, $column ) =
2204 _condensed_generator_defaults($format);
2207 if ($format eq 'latex') {
2210 $separator = " & \n";
2212 sub { my ($d,$a,$s,$w) = @_;
2213 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2215 }elsif ( $format eq 'html' ) {
2219 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2221 sub { my ($d,$a,$s,$w) = @_;
2222 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2231 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2233 &{$column}( &{$f->{fields}->[$i]}(@args),
2234 map { $f->{$_}->[$i] } qw(align span width)
2238 $prefix. join( $separator, @result ). $suffix;
2243 =item _items_pkg [ OPTIONS ]
2245 Return line item hashes for each package item on this invoice. Nearly
2248 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2250 The only OPTIONS accepted is 'section', which may point to a hashref
2251 with a key named 'condensed', which may have a true value. If it
2252 does, this method tries to merge identical items into items with
2253 'quantity' equal to the number of items (not the sum of their
2254 separate quantities, for some reason).
2260 grep { $_->pkgnum } $self->cust_bill_pkg;
2267 warn "$me _items_pkg searching for all package line items\n"
2270 my @cust_bill_pkg = $self->_items_nontax;
2272 warn "$me _items_pkg filtering line items\n"
2274 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2276 if ($options{section} && $options{section}->{condensed}) {
2278 warn "$me _items_pkg condensing section\n"
2282 local $Storable::canonical = 1;
2283 foreach ( @items ) {
2285 delete $item->{ref};
2286 delete $item->{ext_description};
2287 my $key = freeze($item);
2288 $itemshash{$key} ||= 0;
2289 $itemshash{$key} ++; # += $item->{quantity};
2291 @items = sort { $a->{description} cmp $b->{description} }
2292 map { my $i = thaw($_);
2293 $i->{quantity} = $itemshash{$_};
2295 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2301 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2308 return 0 unless $a->itemdesc cmp $b->itemdesc;
2309 return -1 if $b->itemdesc eq 'Tax';
2310 return 1 if $a->itemdesc eq 'Tax';
2311 return -1 if $b->itemdesc eq 'Other surcharges';
2312 return 1 if $a->itemdesc eq 'Other surcharges';
2313 $a->itemdesc cmp $b->itemdesc;
2318 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2319 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2321 if ( $self->conf->exists('always_show_tax') ) {
2322 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2323 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2325 { 'description' => $itemdesc,
2332 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2334 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2335 list of hashrefs describing the line items they generate on the invoice.
2337 OPTIONS may include:
2339 format: the invoice format.
2341 escape_function: the function used to escape strings.
2343 DEPRECATED? (expensive, mostly unused?)
2344 format_function: the function used to format CDRs.
2346 section: a hashref containing 'category' and/or 'locationnum'; if this
2347 is present, only returns line items that belong to that category and/or
2348 location (whichever is defined).
2350 multisection: a flag indicating that this is a multisection invoice,
2351 which does something complicated.
2353 Returns a list of hashrefs, each of which may contain:
2355 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2356 ext_description, which is an arrayref of detail lines to show below
2361 sub _items_cust_bill_pkg {
2363 my $conf = $self->conf;
2364 my $cust_bill_pkgs = shift;
2367 my $format = $opt{format} || '';
2368 my $escape_function = $opt{escape_function} || sub { shift };
2369 my $format_function = $opt{format_function} || '';
2370 my $no_usage = $opt{no_usage} || '';
2371 my $unsquelched = $opt{unsquelched} || ''; #unused
2372 my ($section, $locationnum, $category);
2373 if ( $opt{section} ) {
2374 $category = $opt{section}->{category};
2375 $locationnum = $opt{section}->{locationnum};
2377 my $summary_page = $opt{summary_page} || ''; #unused
2378 my $multisection = defined($category) || defined($locationnum);
2379 my $discount_show_always = 0;
2381 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2383 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2384 # and location labels
2387 my ($s, $r, $u) = ( undef, undef, undef );
2388 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2391 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2392 if ( $_ && !$cust_bill_pkg->hidden ) {
2393 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2394 $_->{amount} =~ s/^\-0\.00$/0.00/;
2395 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2397 if $_->{amount} != 0
2398 || $discount_show_always
2399 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2400 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2406 if ( $locationnum ) {
2407 # this is a location section; skip packages that aren't at this
2409 next if $cust_bill_pkg->pkgnum == 0;
2410 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2414 # Consider display records for this item to determine if it belongs
2415 # in this section. Note that if there are no display records, there
2416 # will be a default pseudo-record that includes all charge types
2417 # and has no section name.
2418 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2419 ? $cust_bill_pkg->cust_bill_pkg_display
2420 : ( $cust_bill_pkg );
2422 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2423 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2426 if ( defined($category) ) {
2427 # then this is a package category section; process all display records
2428 # that belong to this section.
2429 @cust_bill_pkg_display = grep { $_->section eq $category }
2430 @cust_bill_pkg_display;
2432 # otherwise, process all display records that aren't usage summaries
2433 # (I don't think there should be usage summaries if you aren't using
2434 # category sections, but this is the historical behavior)
2435 @cust_bill_pkg_display = grep { !$_->summary }
2436 @cust_bill_pkg_display;
2438 foreach my $display (@cust_bill_pkg_display) {
2440 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2441 $display->billpkgdisplaynum. "\n"
2444 my $type = $display->type;
2446 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2447 $desc = substr($desc, 0, $maxlength). '...'
2448 if $format eq 'latex' && length($desc) > $maxlength;
2450 my %details_opt = ( 'format' => $format,
2451 'escape_function' => $escape_function,
2452 'format_function' => $format_function,
2453 'no_usage' => $opt{'no_usage'},
2456 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2458 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2460 # quotation_pkgs are never fees, so don't worry about the case where
2461 # part_pkg is undefined
2463 if ( $cust_bill_pkg->setup != 0 ) {
2464 my $description = $desc;
2465 $description .= ' Setup'
2466 if $cust_bill_pkg->recur != 0
2467 || $discount_show_always
2468 || $cust_bill_pkg->recur_show_zero;
2470 'description' => $description,
2471 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2474 if ( $cust_bill_pkg->recur != 0 ) {
2476 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2477 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2481 } elsif ( $cust_bill_pkg->pkgnum > 0 ) { # and it's not a quotation_pkg
2483 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2486 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2487 my $part_pkg = $cust_pkg->part_pkg;
2489 # which pkgpart to show for display purposes?
2490 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2492 # start/end dates for invoice formats that do nonstandard
2494 my %item_dates = ();
2495 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2496 unless $part_pkg->option('disable_line_item_date_ranges',1);
2498 if ( (!$type || $type eq 'S')
2499 && ( $cust_bill_pkg->setup != 0
2500 || $cust_bill_pkg->setup_show_zero
2505 warn "$me _items_cust_bill_pkg adding setup\n"
2508 my $description = $desc;
2509 $description .= ' Setup'
2510 if $cust_bill_pkg->recur != 0
2511 || $discount_show_always
2512 || $cust_bill_pkg->recur_show_zero;
2514 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2516 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2517 # triggered by the recurring charge freq
2519 && $cust_bill_pkg->recur == 0
2520 && ! $cust_bill_pkg->recur_show_zero;
2524 unless ( $cust_pkg->part_pkg->hide_svc_detail
2525 || $cust_bill_pkg->hidden )
2528 my @svc_labels = map &{$escape_function}($_),
2529 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2530 push @d, @svc_labels
2531 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2532 $svc_label = $svc_labels[0];
2534 my $lnum = $cust_main ? $cust_main->ship_locationnum
2535 : $self->prospect_main->locationnum;
2536 # show the location label if it's not the customer's default
2537 # location, and we're not grouping items by location already
2538 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2539 my $loc = $cust_pkg->location_label;
2540 $loc = substr($loc, 0, $maxlength). '...'
2541 if $format eq 'latex' && length($loc) > $maxlength;
2542 push @d, &{$escape_function}($loc);
2545 } #unless hiding service details
2547 push @d, $cust_bill_pkg->details(%details_opt)
2548 if $cust_bill_pkg->recur == 0;
2550 if ( $cust_bill_pkg->hidden ) {
2551 $s->{amount} += $cust_bill_pkg->setup;
2552 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2553 push @{ $s->{ext_description} }, @d;
2557 description => $description,
2558 pkgpart => $pkgpart,
2559 pkgnum => $cust_bill_pkg->pkgnum,
2560 amount => $cust_bill_pkg->setup,
2561 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2562 unit_amount => $cust_bill_pkg->unitsetup,
2563 quantity => $cust_bill_pkg->quantity,
2564 ext_description => \@d,
2565 svc_label => ($svc_label || ''),
2571 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2573 $cust_bill_pkg->recur != 0
2574 || $cust_bill_pkg->setup == 0
2575 || $discount_show_always
2576 || $cust_bill_pkg->recur_show_zero
2581 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2584 my $is_summary = $display->summary;
2585 my $description = $desc;
2586 if ( $type eq 'U' and defined($r) ) {
2587 # don't just show the same description as the recur line
2588 $description = $self->mt('Usage charges');
2591 my $part_pkg = $cust_pkg->part_pkg;
2593 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2597 my @seconds = (); # for display of usage info
2600 #at least until cust_bill_pkg has "past" ranges in addition to
2601 #the "future" sdate/edate ones... see #3032
2602 my @dates = ( $self->_date );
2603 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2604 push @dates, $prev->sdate if $prev;
2605 push @dates, undef if !$prev;
2607 # show service labels, unless...
2608 # the package is set not to display them
2609 unless ( $part_pkg->hide_svc_detail
2610 # or this is a tax-like line item
2611 || $cust_bill_pkg->itemdesc
2612 # or this is a hidden (bundled) line item
2613 || $cust_bill_pkg->hidden
2614 # or this is a usage summary line
2615 || $is_summary && $type && $type eq 'U'
2616 # or this is a usage line and there's a recurring line
2617 # for the package in the same section (which will
2618 # have service labels already)
2619 || ($type eq 'U' and defined($r))
2623 warn "$me _items_cust_bill_pkg adding service details\n"
2626 my @svc_labels = map &{$escape_function}($_),
2627 $cust_pkg->h_labels_short(@dates, 'I');
2628 push @d, @svc_labels
2629 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2630 $svc_label = $svc_labels[0];
2632 warn "$me _items_cust_bill_pkg done adding service details\n"
2635 my $lnum = $cust_main ? $cust_main->ship_locationnum
2636 : $self->prospect_main->locationnum;
2637 # show the location label if it's not the customer's default
2638 # location, and we're not grouping items by location already
2639 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2640 my $loc = $cust_pkg->location_label;
2641 $loc = substr($loc, 0, $maxlength). '...'
2642 if $format eq 'latex' && length($loc) > $maxlength;
2643 push @d, &{$escape_function}($loc);
2646 # Display of seconds_since_sqlradacct:
2647 # On the invoice, when processing @detail_items, look for a field
2648 # named 'seconds'. This will contain total seconds for each
2649 # service, in the same order as @ext_description. For services
2650 # that don't support this it will show undef.
2651 if ( $conf->exists('svc_acct-usage_seconds')
2652 and ! $cust_bill_pkg->pkgpart_override ) {
2653 foreach my $cust_svc (
2654 $cust_pkg->h_cust_svc(@dates, 'I')
2657 # eval because not having any part_export_usage exports
2658 # is a fatal error, last_bill/_date because that's how
2659 # sqlradius_hour billing does it
2661 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2663 push @seconds, $sec;
2665 } #if svc_acct-usage_seconds
2667 } # if we are showing service labels
2669 unless ( $is_summary ) {
2670 warn "$me _items_cust_bill_pkg adding details\n"
2673 #instead of omitting details entirely in this case (unwanted side
2674 # effects), just omit CDRs
2675 $details_opt{'no_usage'} = 1
2676 if $type && $type eq 'R';
2678 push @d, $cust_bill_pkg->details(%details_opt);
2681 warn "$me _items_cust_bill_pkg calculating amount\n"
2686 $amount = $cust_bill_pkg->recur;
2687 } elsif ($type eq 'R') {
2688 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2689 } elsif ($type eq 'U') {
2690 $amount = $cust_bill_pkg->usage;
2693 if ( !$type || $type eq 'R' ) {
2695 warn "$me _items_cust_bill_pkg adding recur\n"
2699 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2702 if ( $cust_bill_pkg->hidden ) {
2703 $r->{amount} += $amount;
2704 $r->{unit_amount} += $unit_amount;
2705 push @{ $r->{ext_description} }, @d;
2708 description => $description,
2709 pkgpart => $pkgpart,
2710 pkgnum => $cust_bill_pkg->pkgnum,
2712 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2713 unit_amount => $unit_amount,
2714 quantity => $cust_bill_pkg->quantity,
2716 ext_description => \@d,
2717 svc_label => ($svc_label || ''),
2719 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2722 } else { # $type eq 'U'
2724 warn "$me _items_cust_bill_pkg adding usage\n"
2727 if ( $cust_bill_pkg->hidden and defined($u) ) {
2728 # if this is a hidden package and there's already a usage
2729 # line for the bundle, add this package's total amount and
2730 # usage details to it
2731 $u->{amount} += $amount;
2732 push @{ $u->{ext_description} }, @d;
2733 } elsif ( $amount ) {
2734 # create a new usage line
2736 description => $description,
2737 pkgpart => $pkgpart,
2738 pkgnum => $cust_bill_pkg->pkgnum,
2740 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2742 ext_description => \@d,
2744 } # else this has no usage, so don't create a usage section
2747 } # recurring or usage with recurring charge
2749 } else { # taxes and fees
2751 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2754 # items of this kind should normally not have sdate/edate.
2756 'description' => $desc,
2757 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
2758 + $cust_bill_pkg->recur)
2761 } # if quotation / package line item / other line item
2763 } # foreach $display
2765 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2766 && $conf->exists('discount-show-always'));
2770 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2772 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2773 if exists($_->{amount});
2774 $_->{amount} =~ s/^\-0\.00$/0.00/;
2775 $_->{unit_amount} = sprintf('%.2f', $_->{unit_amount})
2776 if exists($_->{unit_amount});
2779 if $_->{amount} != 0
2780 || $discount_show_always
2781 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2782 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2786 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2793 =item _items_discounts_avail
2795 Returns an array of line item hashrefs representing available term discounts
2796 for this invoice. This makes the same assumptions that apply to term
2797 discounts in general: that the package is billed monthly, at a flat rate,
2798 with no usage charges. A prorated first month will be handled, as will
2799 a setup fee if the discount is allowed to apply to setup fees.
2803 sub _items_discounts_avail {
2806 #maybe move this method from cust_bill when quotations support discount_plans
2807 return () unless $self->can('discount_plans');
2808 my %plans = $self->discount_plans;
2810 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2811 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2815 my $plan = $plans{$months};
2817 my $term_total = sprintf('%.2f', $plan->discounted_total);
2818 my $percent = sprintf('%.0f',
2819 100 * (1 - $term_total / $plan->base_total) );
2820 my $permonth = sprintf('%.2f', $term_total / $months);
2821 my $detail = $self->mt('discount on item'). ' '.
2822 join(', ', map { "#$_" } $plan->pkgnums)
2825 # discounts for non-integer months don't work anyway
2826 $months = sprintf("%d", $months);
2829 description => $self->mt('Save [_1]% by paying for [_2] months',
2831 amount => $self->mt('[_1] ([_2] per month)',
2832 $term_total, $money_char.$permonth),
2833 ext_description => ($detail || ''),
2836 sort { $b <=> $a } keys %plans;