1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
9 use vars qw( $invoice_lines @buf ); #yuck
10 use List::Util qw(sum);
13 use Text::Template 1.20;
19 use FS::Record qw( qsearch qsearchs );
21 use FS::Misc qw( generate_ps generate_pdf );
28 $me = '[FS::Template_Mixin]';
29 FS::UID->install_callback( sub {
30 my $conf = new FS::Conf; #global
31 $money_char = $conf->config('money_char') || '$';
32 $date_format = $conf->config('date_format') || '%x'; #/YY
37 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
39 If the "mode" pseudo-field is set on the object, the configuration handle
40 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
47 my $mode = $self->get('mode');
48 if ($self->{_conf} and !defined($mode)) {
49 return $self->{_conf};
52 my $cust_main = $self->cust_main;
53 my $locale = $cust_main ? $cust_main->locale : '';
56 if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
57 $mode = $mode->modenum;
58 } elsif ( $mode =~ /\D/ ) {
59 die "invalid invoice mode $mode";
61 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
63 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
64 # it doesn't have a locale, but system conf still might
65 $conf->set('locale' => $locale) if $conf;
68 # if $mode is unspecified, or if there is no invoice_conf matching this mode
69 # and locale, then use the system config only (but with the locale)
70 $conf ||= FS::Conf->new({ 'locale' => $locale });
72 return $self->{_conf} = $conf;
75 =item print_text OPTIONS
77 Returns an text invoice, as a list of lines.
79 Options can be passed as a hash.
81 I<time>, if specified, is used to control the printing of overdue messages. The
82 default is now. It isn't the date of the invoice; that's the `_date' field.
83 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
84 L<Time::Local> and L<Date::Parse> for conversion functions.
86 I<template>, if specified, is the name of a suffix for alternate invoices.
88 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
96 %params = %{ shift() };
101 $params{'format'} = 'template'; # for some reason
103 $self->print_generic( %params );
106 =item print_latex HASHREF
108 Internal method - returns a filename of a filled-in LaTeX template for this
109 invoice (Note: add ".tex" to get the actual filename), and a filename of
110 an associated logo (with the .eps extension included).
112 See print_ps and print_pdf for methods that return PostScript and PDF output.
114 Options can be passed as a hash.
116 I<time>, if specified, is used to control the printing of overdue messages. The
117 default is now. It isn't the date of the invoice; that's the `_date' field.
118 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
119 L<Time::Local> and L<Date::Parse> for conversion functions.
121 I<template>, if specified, is the name of a suffix for alternate invoices.
122 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
123 customize invoice templates for different purposes.
125 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
134 %params = %{ shift() };
139 $params{'format'} = 'latex';
140 my $conf = $self->conf;
142 # this needs to go away
143 my $template = $params{'template'};
144 # and this especially
145 $template ||= $self->_agent_template
146 if $self->can('_agent_template');
148 my $pkey = $self->primary_key;
149 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
151 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
152 my $lh = new File::Temp(
153 TEMPLATE => $tmp_template,
157 ) or die "can't open temp file: $!\n";
159 my $agentnum = $self->agentnum;
161 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
162 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
163 or die "can't write temp file: $!\n";
165 print $lh $conf->config_binary('logo.eps', $agentnum)
166 or die "can't write temp file: $!\n";
169 $params{'logo_file'} = $lh->filename;
171 if( $conf->exists('invoice-barcode')
172 && $self->can('invoice_barcode')
173 && $self->invnum ) { # don't try to barcode statements
174 my $png_file = $self->invoice_barcode($dir);
175 my $eps_file = $png_file;
176 $eps_file =~ s/\.png$/.eps/g;
177 $png_file =~ /(barcode.*png)/;
179 $eps_file =~ /(barcode.*eps)/;
182 my $curr_dir = cwd();
184 # after painfuly long experimentation, it was determined that sam2p won't
185 # accept : and other chars in the path, no matter how hard I tried to
186 # escape them, hence the chdir (and chdir back, just to be safe)
187 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
188 or die "sam2p failed: $!\n";
192 $params{'barcode_file'} = $eps_file;
195 my @filled_in = $self->print_generic( %params );
197 my $fh = new File::Temp( TEMPLATE => $tmp_template,
201 ) or die "can't open temp file: $!\n";
202 binmode($fh, ':utf8'); # language support
203 print $fh join('', @filled_in );
206 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
207 return ($1, $params{'logo_file'}, $params{'barcode_file'});
213 my $cust_main = $self->cust_main;
214 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
217 =item print_generic OPTION => VALUE ...
219 Internal method - returns a filled-in template for this invoice as a scalar.
221 See print_ps and print_pdf for methods that return PostScript and PDF output.
223 Non optional options include
224 format - latex, html, template
226 Optional options include
228 template - a value used as a suffix for a configuration template. Please
231 time - a value used to control the printing of overdue messages. The
232 default is now. It isn't the date of the invoice; that's the `_date' field.
233 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
234 L<Time::Local> and L<Date::Parse> for conversion functions.
238 unsquelch_cdr - overrides any per customer cdr squelching when true
240 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
242 locale - override customer's locale
246 #what's with all the sprintf('%10.2f')'s in here? will it cause any
247 # (alignment in text invoice?) problems to change them all to '%.2f' ?
248 # yes: fixed width/plain text printing will be borked
250 my( $self, %params ) = @_;
251 my $conf = $self->conf;
253 my $today = $params{today} ? $params{today} : time;
254 warn "$me print_generic called on $self with suffix $params{template}\n"
257 my $format = $params{format};
258 die "Unknown format: $format"
259 unless $format =~ /^(latex|html|template)$/;
261 my $cust_main = $self->cust_main || $self->prospect_main;
262 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
263 unless $cust_main->payname
264 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
266 my $locale = $params{'locale'} || $cust_main->locale;
268 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
269 'html' => [ '<%=', '%>' ],
270 'template' => [ '{', '}' ],
273 warn "$me print_generic creating template\n"
276 # set the notice name here, and nowhere else.
277 my $notice_name = $params{notice_name}
278 || $conf->config('notice_name')
279 || $self->notice_name;
282 my $template = $params{template} ? $params{template} : $self->_agent_template;
283 my $templatefile = $self->template_conf. $format;
284 $templatefile .= "_$template"
285 if length($template) && $conf->exists($templatefile."_$template");
288 my @invoice_template = map "$_\n", $conf->config($templatefile)
289 or die "cannot load config data $templatefile";
292 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
293 #change this to a die when the old code is removed
294 warn "old-style invoice template $templatefile; ".
295 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
297 @invoice_template = _translate_old_latex_format(@invoice_template);
300 warn "$me print_generic creating T:T object\n"
303 my $text_template = new Text::Template(
305 SOURCE => \@invoice_template,
306 DELIMITERS => $delimiters{$format},
309 warn "$me print_generic compiling T:T object\n"
312 $text_template->compile()
313 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
316 # additional substitution could possibly cause breakage in existing templates
319 'notes' => sub { map "$_", @_ },
320 'footer' => sub { map "$_", @_ },
321 'smallfooter' => sub { map "$_", @_ },
322 'returnaddress' => sub { map "$_", @_ },
323 'coupon' => sub { map "$_", @_ },
324 'summary' => sub { map "$_", @_ },
330 s/%%(.*)$/<!-- $1 -->/g;
331 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
332 s/\\begin\{enumerate\}/<ol>/g;
334 s/\\end\{enumerate\}/<\/ol>/g;
335 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
344 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
346 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
352 s/\\hyphenation\{[\w\s\-]+}//;
357 'coupon' => sub { "" },
358 'summary' => sub { "" },
365 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
366 s/\\begin\{enumerate\}//g;
368 s/\\end\{enumerate\}//g;
369 s/\\textbf\{(.*)\}/$1/g;
376 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
378 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
383 s/\\\\\*?\s*$/\n/; # dubious
384 s/\\hyphenation\{[\w\s\-]+}//;
388 'coupon' => sub { "" },
389 'summary' => sub { "" },
394 # hashes for differing output formats
395 my %nbsps = ( 'latex' => '~',
396 'html' => '', # '&nbps;' would be nice
397 'template' => '', # not used
399 my $nbsp = $nbsps{$format};
401 my %escape_functions = ( 'latex' => \&_latex_escape,
402 'html' => \&_html_escape_nbsp,#\&encode_entities,
403 'template' => sub { shift },
405 my $escape_function = $escape_functions{$format};
406 my $escape_function_nonbsp = ($format eq 'html')
407 ? \&_html_escape : $escape_function;
409 my %newline_tokens = ( 'latex' => '\\\\',
413 my $newline_token = $newline_tokens{$format};
415 warn "$me generating template variables\n"
418 # generate template variables
422 defined( $conf->config_orbase( "invoice_${format}returnaddress",
426 && length( $conf->config_orbase( "invoice_${format}returnaddress",
432 $returnaddress = join("\n",
433 $conf->config_orbase("invoice_${format}returnaddress", $template)
437 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
439 my $convert_map = $convert_maps{$format}{'returnaddress'};
442 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
447 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
449 my $convert_map = $convert_maps{$format}{'returnaddress'};
450 $returnaddress = join( "\n", &$convert_map(
451 map { s/( {2,})/'~' x length($1)/eg;
455 ( $conf->config('company_name', $cust_main->agentnum),
456 $conf->config('company_address', $cust_main->agentnum),
463 my $warning = "Couldn't find a return address; ".
464 "do you need to set the company_address configuration value?";
466 $returnaddress = $nbsp;
467 #$returnaddress = $warning;
471 warn "$me generating invoice data\n"
474 my $agentnum = $cust_main->agentnum;
479 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
480 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
481 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
482 'returnaddress' => $returnaddress,
483 'agent' => &$escape_function($cust_main->agent->agent),
485 #invoice/quotation info
486 'no_number' => $params{'no_number'},
487 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
488 'quotationnum' => $self->quotationnum,
489 'no_date' => $params{'no_date'},
490 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
491 # workaround for inconsistent behavior in the early plain text
492 # templates; see RT#28271
493 'date' => ( $params{'no_date'}
495 : ($format eq 'template'
497 : $self->time2str_local('long', $self->_date, $format)
500 'today' => $self->time2str_local('long', $today, $format),
501 'terms' => $self->terms,
502 'template' => $template, #params{'template'},
503 'notice_name' => $notice_name, # escape?
504 'current_charges' => sprintf("%.2f", $self->charged),
505 'duedate' => $self->due_date2str('rdate'), #date_format?
508 'custnum' => $cust_main->display_custnum,
509 'prospectnum' => $cust_main->prospectnum,
510 'agent_custid' => &$escape_function($cust_main->agent_custid),
511 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
512 payname company address1 address2 city state zip fax
516 'ship_enable' => $conf->exists('invoice-ship_address'),
517 'unitprices' => $conf->exists('invoice-unitprice'),
518 'smallernotes' => $conf->exists('invoice-smallernotes'),
519 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
520 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
522 #layout info -- would be fancy to calc some of this and bury the template
524 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
525 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
526 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
527 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
528 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
529 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
530 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
531 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
532 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
533 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
535 # better hang on to conf_dir for a while (for old templates)
536 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
538 #these are only used when doing paged plaintext
545 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
546 # prototype here to silence warnings
547 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
549 my $min_sdate = 999999999999;
551 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
552 next unless $cust_bill_pkg->pkgnum > 0;
553 $min_sdate = $cust_bill_pkg->sdate
554 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
555 $max_edate = $cust_bill_pkg->edate
556 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
559 $invoice_data{'bill_period'} = '';
560 $invoice_data{'bill_period'} =
561 $self->time2str_local('%e %h', $min_sdate, $format)
563 $self->time2str_local('%e %h', $max_edate, $format)
564 if ($max_edate != 0 && $min_sdate != 999999999999);
566 $invoice_data{finance_section} = '';
567 if ( $conf->config('finance_pkgclass') ) {
569 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
570 $invoice_data{finance_section} = $pkg_class->categoryname;
572 $invoice_data{finance_amount} = '0.00';
573 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
575 my $countrydefault = $conf->config('countrydefault') || 'US';
576 foreach ( qw( address1 address2 city state zip country fax) ){
577 my $method = 'ship_'.$_;
578 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
580 if ( length($cust_main->ship_company) ) {
581 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
583 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
585 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
586 $invoice_data{'ship_country'} = ''
587 if ( $invoice_data{'ship_country'} eq $countrydefault );
589 $invoice_data{'cid'} = $params{'cid'}
592 if ( $cust_main->country eq $countrydefault ) {
593 $invoice_data{'country'} = '';
595 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
599 $invoice_data{'address'} = \@address;
602 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
603 ? " (P.O. #". $cust_main->payinfo. ")"
607 push @address, $cust_main->company
608 if $cust_main->company;
609 push @address, $cust_main->address1;
610 push @address, $cust_main->address2
611 if $cust_main->address2;
613 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
614 push @address, $invoice_data{'country'}
615 if $invoice_data{'country'};
617 while (scalar(@address) < 5);
619 $invoice_data{'logo_file'} = $params{'logo_file'}
620 if $params{'logo_file'};
621 $invoice_data{'barcode_file'} = $params{'barcode_file'}
622 if $params{'barcode_file'};
623 $invoice_data{'barcode_img'} = $params{'barcode_img'}
624 if $params{'barcode_img'};
625 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
626 if $params{'barcode_cid'};
628 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
629 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
630 #my $balance_due = $self->owed + $pr_total - $cr_total;
631 my $balance_due = $self->owed + $pr_total;
633 #these are used on the summary page only
635 # the customer's current balance as shown on the invoice before this one
636 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
638 # the change in balance from that invoice to this one
639 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
641 # the sum of amount owed on all previous invoices
642 # ($pr_total is used elsewhere but not as $previous_balance)
643 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
645 # the sum of amount owed on all invoices
646 # (this is used in the summary & on the payment coupon)
647 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
649 # info from customer's last invoice before this one, for some
651 $invoice_data{'last_bill'} = {};
653 if ( $self->custnum && $self->invnum ) {
655 if ( $self->previous_bill ) {
656 my $last_bill = $self->previous_bill;
657 $invoice_data{'last_bill'} = {
658 '_date' => $last_bill->_date, #unformatted
660 my (@payments, @credits);
661 # for formats that itemize previous payments
662 foreach my $cust_pay ( qsearch('cust_pay', {
663 'custnum' => $self->custnum,
664 '_date' => { op => '>=',
665 value => $last_bill->_date }
668 next if $cust_pay->_date > $self->_date;
670 '_date' => $cust_pay->_date,
671 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
672 'payinfo' => $cust_pay->payby_payinfo_pretty,
673 'amount' => sprintf('%.2f', $cust_pay->paid),
675 # not concerned about applications
677 foreach my $cust_credit ( qsearch('cust_credit', {
678 'custnum' => $self->custnum,
679 '_date' => { op => '>=',
680 value => $last_bill->_date }
683 next if $cust_credit->_date > $self->_date;
685 '_date' => $cust_credit->_date,
686 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
687 'creditreason'=> $cust_credit->reason,
688 'amount' => sprintf('%.2f', $cust_credit->amount),
691 $invoice_data{'previous_payments'} = \@payments;
692 $invoice_data{'previous_credits'} = \@credits;
697 my $summarypage = '';
698 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
701 $invoice_data{'summarypage'} = $summarypage;
703 warn "$me substituting variables in notes, footer, smallfooter\n"
706 my $tc = $self->template_conf;
707 my @include = ( [ $tc, 'notes' ],
708 [ 'invoice_', 'footer' ],
709 [ 'invoice_', 'smallfooter', ],
711 push @include, [ $tc, 'coupon', ]
712 unless $params{'no_coupon'};
714 foreach my $i (@include) {
716 my($base, $include) = @$i;
718 my $inc_file = $conf->key_orbase("$base$format$include", $template);
721 if ( $conf->exists($inc_file, $agentnum)
722 && length( $conf->config($inc_file, $agentnum) ) ) {
724 @inc_src = $conf->config($inc_file, $agentnum);
728 $inc_file = $conf->key_orbase("${base}latex$include", $template);
730 my $convert_map = $convert_maps{$format}{$include};
732 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
733 s/--\@\]/$delimiters{$format}[1]/g;
736 &$convert_map( $conf->config($inc_file, $agentnum) );
740 my $inc_tt = new Text::Template (
742 SOURCE => [ map "$_\n", @inc_src ],
743 DELIMITERS => $delimiters{$format},
744 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
746 unless ( $inc_tt->compile() ) {
747 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
748 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
752 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
754 $invoice_data{$include} =~ s/\n+$//
755 if ($format eq 'latex');
758 # let invoices use either of these as needed
759 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
760 ? $cust_main->payinfo : '';
761 $invoice_data{'po_line'} =
762 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
763 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
766 my %money_chars = ( 'latex' => '',
767 'html' => $conf->config('money_char') || '$',
770 my $money_char = $money_chars{$format};
772 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
773 'html' => $conf->config('money_char') || '$',
776 my $other_money_char = $other_money_chars{$format};
777 $invoice_data{'dollar'} = $other_money_char;
779 my %minus_signs = ( 'latex' => '$-$',
781 'template' => '- ' );
782 my $minus = $minus_signs{$format};
784 my @detail_items = ();
785 my @total_items = ();
789 $invoice_data{'detail_items'} = \@detail_items;
790 $invoice_data{'total_items'} = \@total_items;
791 $invoice_data{'buf'} = \@buf;
792 $invoice_data{'sections'} = \@sections;
794 warn "$me generating sections\n"
798 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
799 'subtotal' => $taxtotal, # adjusted below
802 my $tax_weight = _pkg_category($tax_section->{description})
803 ? _pkg_category($tax_section->{description})->weight
805 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
806 $tax_section->{'sort_weight'} = $tax_weight;
809 my $adjust_section = {
810 'description' => $self->mt('Credits, Payments, and Adjustments'),
811 'adjust_section' => 1,
812 'subtotal' => 0, # adjusted below
814 my $adjust_weight = _pkg_category($adjust_section->{description})
815 ? _pkg_category($adjust_section->{description})->weight
817 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
818 $adjust_section->{'sort_weight'} = $adjust_weight;
820 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
821 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
822 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
823 $invoice_data{'multisection'} = $multisection;
825 my $extra_sections = [];
826 my $extra_lines = ();
828 # default section ('Charges')
829 my $default_section = { 'description' => '',
834 # Previous Charges section
835 # subtotal is the first return value from $self->previous
836 my $previous_section;
837 # if the invoice has major sections, or if we're summarizing previous
838 # charges with a single line, or if we've been specifically told to put them
839 # in a section, create a section for previous charges:
840 if ( $multisection or
841 $conf->exists('previous_balance-summary_only') or
842 $conf->exists('previous_balance-section') ) {
844 $previous_section = { 'description' => $self->mt('Previous Charges'),
845 'subtotal' => $other_money_char.
846 sprintf('%.2f', $pr_total),
847 'summarized' => '', #why? $summarypage ? 'Y' : '',
849 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
850 join(' / ', map { $cust_main->balance_date_range(@$_) }
851 $self->_prior_month30s
853 if $conf->exists('invoice_include_aging');
856 # otherwise put them in the main section
857 $previous_section = $default_section;
860 if ( $multisection ) {
861 ($extra_sections, $extra_lines) =
862 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
863 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
864 && $self->can('_items_extra_usage_sections');
866 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
868 push @detail_items, @$extra_lines if $extra_lines;
870 # the code is written so that both methods can be used together, but
871 # we haven't yet changed the template to take advantage of that, so for
872 # now, treat them as mutually exclusive.
873 my %section_method = ( by_category => 1 );
874 if ( $conf->exists($tc.'sections_by_location') ) {
875 %section_method = ( by_location => 1 );
878 $self->_items_sections( 'summary' => $summarypage,
879 'escape' => $escape_function_nonbsp,
880 'extra_sections' => $extra_sections,
884 push @sections, @$early;
885 $late_sections = $late;
887 if ( $conf->exists('svc_phone_sections')
888 && $self->can('_items_svc_phone_sections')
891 my ($phone_sections, $phone_lines) =
892 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
893 push @{$late_sections}, @$phone_sections;
894 push @detail_items, @$phone_lines;
896 if ( $conf->exists('voip-cust_accountcode_cdr')
897 && $cust_main->accountcode_cdr
898 && $self->can('_items_accountcode_cdr')
901 my ($accountcode_section, $accountcode_lines) =
902 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
903 if ( scalar(@$accountcode_lines) ) {
904 push @{$late_sections}, $accountcode_section;
905 push @detail_items, @$accountcode_lines;
908 } else {# not multisection
909 # make a default section
910 push @sections, $default_section;
911 # and calculate the finance charge total, since it won't get done otherwise.
912 # and the default section total
913 # XXX possibly finance_pkgclass should not be used in this manner?
916 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
917 if ( $invoice_data{finance_section} and
918 grep { $_->section eq $invoice_data{finance_section} }
919 $cust_bill_pkg->cust_bill_pkg_display ) {
920 # I think these are always setup fees, but just to be sure...
921 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
923 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
926 $invoice_data{finance_amount} =
927 sprintf('%.2f', sum( @finance_charges ) || 0);
928 $default_section->{subtotal} = $other_money_char.
929 sprintf('%.2f', sum( @charges ) || 0);
932 # previous invoice balances in the Previous Charges section if there
933 # is one, otherwise in the main detail section
934 # (except if summary_only is enabled, don't show them at all)
935 if ( $self->can('_items_previous') &&
936 $self->enable_previous &&
937 ! $conf->exists('previous_balance-summary_only') ) {
939 warn "$me adding previous balances\n"
942 foreach my $line_item ( $self->_items_previous ) {
945 ref => $line_item->{'pkgnum'},
946 pkgpart => $line_item->{'pkgpart'},
947 #quantity => 1, # not really correct
948 section => $previous_section, # which might be $default_section
949 description => &$escape_function($line_item->{'description'}),
950 ext_description => [ map { &$escape_function($_) }
951 @{ $line_item->{'ext_description'} || [] }
953 amount => ( $old_latex ? '' : $money_char).
954 $line_item->{'amount'},
955 product_code => $line_item->{'pkgpart'} || 'N/A',
958 push @detail_items, $detail;
959 push @buf, [ $detail->{'description'},
960 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
966 if ( @pr_cust_bill && $self->enable_previous ) {
967 push @buf, ['','-----------'];
968 push @buf, [ $self->mt('Total Previous Balance'),
969 $money_char. sprintf("%10.2f", $pr_total) ];
973 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
974 warn "$me adding DID summary\n"
977 my ($didsummary,$minutes) = $self->_did_summary;
978 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
980 { 'description' => $didsummary_desc,
981 'ext_description' => [ $didsummary, $minutes ],
985 foreach my $section (@sections, @$late_sections) {
987 # begin some normalization
988 $section->{'subtotal'} = $section->{'amount'}
990 && !exists($section->{subtotal})
991 && exists($section->{amount});
993 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
994 if ( $invoice_data{finance_section} &&
995 $section->{'description'} eq $invoice_data{finance_section} );
997 $section->{'subtotal'} = $other_money_char.
998 sprintf('%.2f', $section->{'subtotal'})
1001 # continue some normalization
1002 $section->{'amount'} = $section->{'subtotal'}
1006 if ( $section->{'description'} ) {
1007 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1012 warn "$me setting options\n"
1016 $options{'section'} = $section if $multisection;
1017 $options{'format'} = $format;
1018 $options{'escape_function'} = $escape_function;
1019 $options{'no_usage'} = 1 unless $unsquelched;
1020 $options{'unsquelched'} = $unsquelched;
1021 $options{'summary_page'} = $summarypage;
1022 $options{'skip_usage'} =
1023 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1025 warn "$me searching for line items\n"
1028 foreach my $line_item ( $self->_items_pkg(%options),
1029 $self->_items_fee(%options) ) {
1031 warn "$me adding line item $line_item\n"
1036 # ext_description => [],
1038 #$detail->{'ref'} = $line_item->{'pkgnum'};
1039 #$detail->{'pkgpart'} = $line_item->{'pkgpart'};
1040 #$detail->{'quantity'} = $line_item->{'quantity'};
1041 #$detail->{'section'} = $section;
1042 #$detail->{'description'} = &$escape_function($line_item->{'description'});
1043 #if ( exists $line_item->{'ext_description'} ) {
1044 # @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1046 #$detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1047 # $line_item->{'amount'};
1048 #if ( exists $line_item->{'unit_amount'} ) {
1049 # $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1050 # $line_item->{'unit_amount'};
1052 #$detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1054 #$detail->{'sdate'} = $line_item->{'sdate'};
1055 #$detail->{'edate'} = $line_item->{'edate'};
1056 #$detail->{'seconds'} = $line_item->{'seconds'};
1057 #$detail->{'svc_label'} = $line_item->{'svc_label'};
1058 #$detail->{'usage_item'} = $line_item->{'usage_item'};
1059 $line_item->{'ref'} = $line_item->{'pkgnum'};
1060 $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1061 $line_item->{'section'} = $section;
1062 $line_item->{'description'} = &$escape_function($line_item->{'description'});
1063 if (!$old_latex) { # dubious; templates should provide this
1064 $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1065 $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1067 $line_item->{'ext_description'} ||= [];
1069 push @detail_items, $line_item;
1070 push @buf, ( [ $line_item->{'description'},
1071 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1073 map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1077 if ( $section->{'description'} ) {
1078 push @buf, ( ['','-----------'],
1079 [ $section->{'description'}. ' sub-total',
1080 $section->{'subtotal'} # already formatted this
1089 $invoice_data{current_less_finance} =
1090 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1092 # if there's anything in the Previous Charges section, prepend it to the list
1093 if ( $pr_total and $previous_section ne $default_section ) {
1094 unshift @sections, $previous_section;
1097 warn "$me adding taxes\n"
1100 my @items_tax = $self->_items_tax;
1101 foreach my $tax ( @items_tax ) {
1103 $taxtotal += $tax->{'amount'};
1105 my $description = &$escape_function( $tax->{'description'} );
1106 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1108 if ( $multisection ) {
1110 my $money = $old_latex ? '' : $money_char;
1111 push @detail_items, {
1112 ext_description => [],
1115 description => $description,
1116 amount => $money. $amount,
1118 section => $tax_section,
1123 push @total_items, {
1124 'total_item' => $description,
1125 'total_amount' => $other_money_char. $amount,
1130 push @buf,[ $description,
1131 $money_char. $amount,
1138 $total->{'total_item'} = $self->mt('Sub-total');
1139 $total->{'total_amount'} =
1140 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1142 if ( $multisection ) {
1143 $tax_section->{'subtotal'} = $other_money_char.
1144 sprintf('%.2f', $taxtotal);
1145 $tax_section->{'pretotal'} = 'New charges sub-total '.
1146 $total->{'total_amount'};
1147 push @sections, $tax_section if $taxtotal;
1149 unshift @total_items, $total;
1152 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1154 push @buf,['','-----------'];
1155 push @buf,[$self->mt(
1156 (!$self->enable_previous)
1158 : 'Total New Charges'
1160 $money_char. sprintf("%10.2f",$self->charged) ];
1168 my %embolden_functions = (
1169 'latex' => sub { return '\textbf{'. shift(). '}' },
1170 'html' => sub { return '<b>'. shift(). '</b>' },
1171 'template' => sub { shift },
1173 my $embolden_function = $embolden_functions{$format};
1175 if ( $self->can('_items_total') ) { # quotations
1177 $self->_items_total(\@total_items);
1179 foreach ( @total_items ) {
1180 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1181 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1182 $_->{'total_amount'}
1186 } else { #normal invoice case
1188 # calculate total, possibly including total owed on previous
1192 $item = $conf->config('previous_balance-exclude_from_total')
1193 || 'Total New Charges'
1194 if $conf->exists('previous_balance-exclude_from_total');
1195 my $amount = $self->charged;
1196 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1197 $amount += $pr_total;
1200 $total->{'total_item'} = &$embolden_function($self->mt($item));
1201 $total->{'total_amount'} =
1202 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1203 if ( $multisection ) {
1204 if ( $adjust_section->{'sort_weight'} ) {
1205 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1206 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1208 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1209 $other_money_char. sprintf('%.2f', $self->charged );
1212 push @total_items, $total;
1214 push @buf,['','-----------'];
1217 sprintf( '%10.2f', $amount )
1221 # if we're showing previous invoices, also show previous
1222 # credits and payments
1223 if ( $self->enable_previous
1224 and $self->can('_items_credits')
1225 and $self->can('_items_payments') )
1227 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1230 my $credittotal = 0;
1231 foreach my $credit (
1232 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1236 $total->{'total_item'} = &$escape_function($credit->{'description'});
1237 $credittotal += $credit->{'amount'};
1238 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1239 $adjusttotal += $credit->{'amount'};
1240 if ( $multisection ) {
1241 my $money = $old_latex ? '' : $money_char;
1242 push @detail_items, {
1243 ext_description => [],
1246 description => &$escape_function($credit->{'description'}),
1247 amount => $money. $credit->{'amount'},
1249 section => $adjust_section,
1252 push @total_items, $total;
1256 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1259 foreach my $credit (
1260 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1262 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1266 my $paymenttotal = 0;
1267 foreach my $payment (
1268 $self->_items_payments( 'template' => $template )
1271 $total->{'total_item'} = &$escape_function($payment->{'description'});
1272 $paymenttotal += $payment->{'amount'};
1273 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1274 $adjusttotal += $payment->{'amount'};
1275 if ( $multisection ) {
1276 my $money = $old_latex ? '' : $money_char;
1277 push @detail_items, {
1278 ext_description => [],
1281 description => &$escape_function($payment->{'description'}),
1282 amount => $money. $payment->{'amount'},
1284 section => $adjust_section,
1287 push @total_items, $total;
1289 push @buf, [ $payment->{'description'},
1290 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1293 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1295 if ( $multisection ) {
1296 $adjust_section->{'subtotal'} = $other_money_char.
1297 sprintf('%.2f', $adjusttotal);
1298 push @sections, $adjust_section
1299 unless $adjust_section->{sort_weight};
1302 # create Balance Due message
1305 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1306 $total->{'total_amount'} =
1307 &$embolden_function(
1308 $other_money_char. sprintf('%.2f', #why? $summarypage
1309 # ? $self->charged +
1310 # $self->billing_balance
1312 $self->owed + $pr_total
1315 if ( $multisection && !$adjust_section->{sort_weight} ) {
1316 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1317 $total->{'total_amount'};
1319 push @total_items, $total;
1321 push @buf,['','-----------'];
1322 push @buf,[$self->balance_due_msg, $money_char.
1323 sprintf("%10.2f", $balance_due ) ];
1326 if ( $conf->exists('previous_balance-show_credit')
1327 and $cust_main->balance < 0 ) {
1328 my $credit_total = {
1329 'total_item' => &$embolden_function($self->credit_balance_msg),
1330 'total_amount' => &$embolden_function(
1331 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1334 if ( $multisection ) {
1335 $adjust_section->{'posttotal'} .= $newline_token .
1336 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1339 push @total_items, $credit_total;
1341 push @buf,['','-----------'];
1342 push @buf,[$self->credit_balance_msg, $money_char.
1343 sprintf("%10.2f", -$cust_main->balance ) ];
1347 } #end of default total adding ! can('_items_total')
1349 if ( $multisection ) {
1350 if ( $conf->exists('svc_phone_sections')
1351 && $self->can('_items_svc_phone_sections')
1355 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1356 $total->{'total_amount'} =
1357 &$embolden_function(
1358 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1360 my $last_section = pop @sections;
1361 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1362 $total->{'total_amount'};
1363 push @sections, $last_section;
1365 push @sections, @$late_sections
1369 # make a discounts-available section, even without multisection
1370 if ( $conf->exists('discount-show_available')
1371 and my @discounts_avail = $self->_items_discounts_avail ) {
1372 my $discount_section = {
1373 'description' => $self->mt('Discounts Available'),
1378 push @sections, $discount_section;
1379 push @detail_items, map { +{
1380 'ref' => '', #should this be something else?
1381 'section' => $discount_section,
1382 'description' => &$escape_function( $_->{description} ),
1383 'amount' => $money_char . &$escape_function( $_->{amount} ),
1384 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1385 } } @discounts_avail;
1388 my @summary_subtotals;
1389 # the templates say "$_->{tax_section} || !$_->{summarized}"
1390 # except 'summarized' is only true when tax_section is true, so this
1391 # is always true, so what's the deal?
1392 foreach my $s (@sections) {
1393 # not to include in the "summary of new charges" block:
1394 # finance charges, adjustments, previous charges,
1395 # and itemized phone usage sections
1396 if ( $s eq $adjust_section or
1397 ($s eq $previous_section and $s ne $default_section) or
1398 ($invoice_data{'finance_section'} and
1399 $invoice_data{'finance_section'} eq $s->{description}) or
1400 $s->{'description'} =~ /^\d+ $/ ) {
1403 push @summary_subtotals, $s;
1405 $invoice_data{summary_subtotals} = \@summary_subtotals;
1408 if ( $conf->exists('usage_class_summary')
1409 and $self->can('_items_usage_class_summary') ) {
1410 my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
1411 if ( @usage_subtotals ) {
1412 unshift @sections, $usage_subtotals[0]->{section};
1413 unshift @detail_items, @usage_subtotals;
1417 # invoice history "section" (not really a section)
1418 # not to be included in any subtotals, completely independent of
1420 if ( $conf->exists('previous_invoice_history') ) {
1423 foreach my $cust_bill ( $cust_main->cust_bill ) {
1424 # XXX hardcoded format, and currently only 'charged'; add other fields
1425 # if they become necessary
1426 my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1427 $history{$date} ||= 0;
1428 $history{$date} += $cust_bill->charged;
1429 # just so we have a numeric sort key
1430 $monthorder{$date} ||= $cust_bill->_date;
1432 my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1434 my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1435 $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1438 # service locations: another option for template customization
1440 foreach my $item (@detail_items) {
1441 if ( $item->{locationnum} ) {
1442 $location_info{ $item->{locationnum} } ||= {
1443 FS::cust_location->by_key( $item->{locationnum} )->location_hash
1447 $invoice_data{location_info} = \%location_info;
1449 # debugging hook: call this with 'diag' => 1 to just get a hash of
1450 # the invoice variables
1451 return \%invoice_data if ( $params{'diag'} );
1453 # All sections and items are built; now fill in templates.
1454 my @includelist = ();
1455 push @includelist, 'summary' if $summarypage;
1456 foreach my $include ( @includelist ) {
1458 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1461 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1463 @inc_src = $conf->config($inc_file, $agentnum);
1467 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1469 my $convert_map = $convert_maps{$format}{$include};
1471 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1472 s/--\@\]/$delimiters{$format}[1]/g;
1475 &$convert_map( $conf->config($inc_file, $agentnum) );
1479 my $inc_tt = new Text::Template (
1481 SOURCE => [ map "$_\n", @inc_src ],
1482 DELIMITERS => $delimiters{$format},
1483 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1485 unless ( $inc_tt->compile() ) {
1486 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1487 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1491 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1493 $invoice_data{$include} =~ s/\n+$//
1494 if ($format eq 'latex');
1499 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1500 /invoice_lines\((\d*)\)/;
1501 $invoice_lines += $1 || scalar(@buf);
1504 die "no invoice_lines() functions in template?"
1505 if ( $format eq 'template' && !$wasfunc );
1507 if ($format eq 'template') {
1509 if ( $invoice_lines ) {
1510 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1511 $invoice_data{'total_pages'}++
1512 if scalar(@buf) % $invoice_lines;
1515 #setup subroutine for the template
1516 $invoice_data{invoice_lines} = sub {
1517 my $lines = shift || scalar(@buf);
1529 push @collect, split("\n",
1530 $text_template->fill_in( HASH => \%invoice_data )
1532 $invoice_data{'page'}++;
1534 map "$_\n", @collect;
1536 } else { # this is where we actually create the invoice
1538 warn "filling in template for invoice ". $self->invnum. "\n"
1540 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1543 $text_template->fill_in(HASH => \%invoice_data);
1547 sub notice_name { '('.shift->table.')'; }
1549 sub template_conf { 'invoice_'; }
1551 # helper routine for generating date ranges
1552 sub _prior_month30s {
1555 [ 1, 2592000 ], # 0-30 days ago
1556 [ 2592000, 5184000 ], # 30-60 days ago
1557 [ 5184000, 7776000 ], # 60-90 days ago
1558 [ 7776000, 0 ], # 90+ days ago
1561 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1562 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1567 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1569 Returns an postscript invoice, as a scalar.
1571 Options can be passed as a hashref (recommended) or as a list of time, template
1572 and then any key/value pairs for any other options.
1574 I<time> an optional value used to control the printing of overdue messages. The
1575 default is now. It isn't the date of the invoice; that's the `_date' field.
1576 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1577 L<Time::Local> and L<Date::Parse> for conversion functions.
1579 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1586 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1587 my $ps = generate_ps($file);
1589 unlink($barcodefile) if $barcodefile;
1594 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1596 Returns an PDF invoice, as a scalar.
1598 Options can be passed as a hashref (recommended) or as a list of time, template
1599 and then any key/value pairs for any other options.
1601 I<time> an optional value used to control the printing of overdue messages. The
1602 default is now. It isn't the date of the invoice; that's the `_date' field.
1603 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1604 L<Time::Local> and L<Date::Parse> for conversion functions.
1606 I<template>, if specified, is the name of a suffix for alternate invoices.
1608 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1615 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1616 my $pdf = generate_pdf($file);
1618 unlink($barcodefile) if $barcodefile;
1623 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1625 Returns an HTML invoice, as a scalar.
1627 I<time> an optional value used to control the printing of overdue messages. The
1628 default is now. It isn't the date of the invoice; that's the `_date' field.
1629 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1630 L<Time::Local> and L<Date::Parse> for conversion functions.
1632 I<template>, if specified, is the name of a suffix for alternate invoices.
1634 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1636 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1637 when emailing the invoice as part of a multipart/related MIME email.
1645 %params = %{ shift() };
1649 $params{'format'} = 'html';
1651 $self->print_generic( %params );
1654 # quick subroutine for print_latex
1656 # There are ten characters that LaTeX treats as special characters, which
1657 # means that they do not simply typeset themselves:
1658 # # $ % & ~ _ ^ \ { }
1660 # TeX ignores blanks following an escaped character; if you want a blank (as
1661 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1665 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1666 $value =~ s/([<>])/\$$1\$/g;
1672 encode_entities($value);
1676 sub _html_escape_nbsp {
1677 my $value = _html_escape(shift);
1678 $value =~ s/ +/ /g;
1682 #utility methods for print_*
1684 sub _translate_old_latex_format {
1685 warn "_translate_old_latex_format called\n"
1692 if ( $line =~ /^%%Detail\s*$/ ) {
1694 push @template, q![@--!,
1695 q! foreach my $_tr_line (@detail_items) {!,
1696 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1697 q! $_tr_line->{'description'} .= !,
1698 q! "\\tabularnewline\n~~".!,
1699 q! join( "\\tabularnewline\n~~",!,
1700 q! @{$_tr_line->{'ext_description'}}!,
1704 while ( ( my $line_item_line = shift )
1705 !~ /^%%EndDetail\s*$/ ) {
1706 $line_item_line =~ s/'/\\'/g; # nice LTS
1707 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1708 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1709 push @template, " \$OUT .= '$line_item_line';";
1712 push @template, '}',
1715 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1717 push @template, '[@--',
1718 ' foreach my $_tr_line (@total_items) {';
1720 while ( ( my $total_item_line = shift )
1721 !~ /^%%EndTotalDetails\s*$/ ) {
1722 $total_item_line =~ s/'/\\'/g; # nice LTS
1723 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1724 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1725 push @template, " \$OUT .= '$total_item_line';";
1728 push @template, '}',
1732 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1733 push @template, $line;
1739 warn "$_\n" foreach @template;
1747 my $conf = $self->conf;
1749 #check for an invoice-specific override
1750 return $self->invoice_terms if $self->invoice_terms;
1752 #check for a customer- specific override
1753 my $cust_main = $self->cust_main;
1754 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1756 #use configured default
1757 $conf->config('invoice_default_terms') || '';
1763 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1764 $duedate = $self->_date() + ( $1 * 86400 );
1771 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1774 sub balance_due_msg {
1776 my $msg = $self->mt('Balance Due');
1777 return $msg unless $self->terms;
1778 if ( $self->due_date ) {
1779 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1780 $self->due_date2str('short');
1781 } elsif ( $self->terms ) {
1782 $msg .= ' - '. $self->terms;
1787 sub balance_due_date {
1789 my $conf = $self->conf;
1791 if ( $conf->exists('invoice_default_terms')
1792 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1793 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1798 sub credit_balance_msg {
1800 $self->mt('Credit Balance Remaining')
1805 Returns a string with the date, for example: "3/20/2008", localized for the
1806 customer. Use _date_pretty_unlocalized for non-end-customer display use.
1812 $self->time2str_local('short', $self->_date);
1815 =item _date_pretty_unlocalized
1817 Returns a string with the date, for example: "3/20/2008", in the format
1818 configured for the back-office. Use _date_pretty for end-customer display use.
1822 sub _date_pretty_unlocalized {
1824 time2str($date_format, $self->_date);
1827 =item _items_sections OPTIONS
1829 Generate section information for all items appearing on this invoice.
1830 This will only be called for multi-section invoices.
1832 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1833 related display records (L<FS::cust_bill_pkg_display>) and organize
1834 them into two groups ("early" and "late" according to whether they come
1835 before or after the total), then into sections. A subtotal is calculated
1838 Section descriptions are returned in sort weight order. Each consists
1839 of a hash containing:
1841 description: the package category name, escaped
1842 subtotal: the total charges in that section
1843 tax_section: a flag indicating that the section contains only tax charges
1844 summarized: same as tax_section, for some reason
1845 sort_weight: the package category's sort weight
1847 If 'condense' is set on the display record, it also contains everything
1848 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1849 coderefs to generate parts of the invoice. This is not advised.
1851 The method returns two arrayrefs, one of "early" sections and one of "late"
1854 OPTIONS may include:
1856 by_location: a flag to divide the invoice into sections by location.
1857 Each section hash will have a 'location' element containing a hashref of
1858 the location fields (see L<FS::cust_location>). The section description
1859 will be the location label, but the template can use any of the location
1860 fields to create a suitable label.
1862 by_category: a flag to divide the invoice into sections using display
1863 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1864 behavior. Each section hash will have a 'category' element containing
1865 the section name from the display record (which probably equals the
1866 category name of the package, but may not in some cases).
1868 summary: a flag indicating that this is a summary-format invoice.
1869 Turning this on has the following effects:
1870 - Ignores display items with the 'summary' flag.
1871 - Places all sections in the "early" group even if they have post_total.
1872 - Creates sections for all non-disabled package categories, even if they
1873 have no charges on this invoice, as well as a section with no name.
1875 escape: an escape function to use for section titles.
1877 extra_sections: an arrayref of additional sections to return after the
1878 sorted list. If there are any of these, section subtotals exclude
1881 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1882 passed through to C<_condense_section()>.
1886 use vars qw(%pkg_category_cache);
1887 sub _items_sections {
1891 my $escape = $opt{escape};
1892 my @extra_sections = @{ $opt{extra_sections} || [] };
1894 # $subtotal{$locationnum}{$categoryname} = amount.
1895 # if we're not using by_location, $locationnum is undef.
1896 # if we're not using by_category, you guessed it, $categoryname is undef.
1897 # if we're not using either one, we shouldn't be here in the first place...
1899 my %late_subtotal = ();
1902 # About tax items + multisection invoices:
1903 # If either invoice_*summary option is enabled, AND there is a
1904 # package category with the name of the tax, then there will be
1905 # a display record assigning the tax item to that category.
1907 # However, the taxes are always placed in the "Taxes, Surcharges,
1908 # and Fees" section regardless of that. The only effect of the
1909 # display record is to create a subtotal for the summary page.
1912 my $pkg_hash = $self->cust_pkg_hash;
1914 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1917 my $usage = $cust_bill_pkg->usage;
1920 if ( $opt{by_location} ) {
1921 if ( $cust_bill_pkg->pkgnum ) {
1922 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1927 $locationnum = undef;
1930 # as in _items_cust_pkg, if a line item has no display records,
1931 # cust_bill_pkg_display() returns a default record for it
1933 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1934 next if ( $display->summary && $opt{summary} );
1936 my $section = $display->section;
1937 my $type = $display->type;
1938 # Set $section = undef if we're sectioning by location and this
1939 # line item _has_ a location (i.e. isn't a fee).
1940 $section = undef if $locationnum;
1942 # set this flag if the section is not tax-only
1943 $not_tax{$locationnum}{$section} = 1
1944 if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
1946 # there's actually a very important piece of logic buried in here:
1947 # incrementing $late_subtotal{$section} CREATES
1948 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1949 # to define the list of late sections, and likewise keys(%subtotal).
1950 # When _items_cust_bill_pkg is called to generate line items for
1951 # real, it will be called with 'section' => $section for each
1953 if ( $display->post_total && !$opt{summary} ) {
1954 if (! $type || $type eq 'S') {
1955 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1956 if $cust_bill_pkg->setup != 0
1957 || $cust_bill_pkg->setup_show_zero;
1961 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1962 if $cust_bill_pkg->recur != 0
1963 || $cust_bill_pkg->recur_show_zero;
1966 if ($type && $type eq 'R') {
1967 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1968 if $cust_bill_pkg->recur != 0
1969 || $cust_bill_pkg->recur_show_zero;
1972 if ($type && $type eq 'U') {
1973 $late_subtotal{$locationnum}{$section} += $usage
1974 unless scalar(@extra_sections);
1977 } else { # it's a pre-total (normal) section
1979 # skip tax items unless they're explicitly included in a section
1980 next if $cust_bill_pkg->pkgnum == 0 and
1981 ! $cust_bill_pkg->feepart and
1984 if (! $type || $type eq 'S') {
1985 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1986 if $cust_bill_pkg->setup != 0
1987 || $cust_bill_pkg->setup_show_zero;
1991 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1992 if $cust_bill_pkg->recur != 0
1993 || $cust_bill_pkg->recur_show_zero;
1996 if ($type && $type eq 'R') {
1997 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1998 if $cust_bill_pkg->recur != 0
1999 || $cust_bill_pkg->recur_show_zero;
2002 if ($type && $type eq 'U') {
2003 $subtotal{$locationnum}{$section} += $usage
2004 unless scalar(@extra_sections);
2013 %pkg_category_cache = ();
2015 # summary invoices need subtotals for all non-disabled package categories,
2016 # even if they're zero
2017 # but currently assume that there are no location sections, or at least
2018 # that the summary page doesn't care about them
2019 if ( $opt{summary} ) {
2020 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2021 $subtotal{''}{$category->categoryname} ||= 0;
2023 $subtotal{''}{''} ||= 0;
2027 foreach my $post_total (0,1) {
2029 my $s = $post_total ? \%late_subtotal : \%subtotal;
2030 foreach my $locationnum (keys %$s) {
2031 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2033 'subtotal' => $s->{$locationnum}{$sectionname},
2034 'post_total' => $post_total,
2037 if ( $locationnum ) {
2038 $section->{'locationnum'} = $locationnum;
2039 my $location = FS::cust_location->by_key($locationnum);
2040 $section->{'description'} = &{ $escape }($location->location_label);
2041 # Better ideas? This will roughly group them by proximity,
2042 # which alpha sorting on any of the address fields won't.
2043 # Sorting by locationnum is meaningless.
2044 # We have to sort on _something_ or the order may change
2045 # randomly from one invoice to the next, which will confuse
2047 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2049 $section->{'location'} = {
2050 map { $_ => &{ $escape }($location->get($_)) }
2054 $section->{'category'} = $sectionname;
2055 $section->{'description'} = &{ $escape }($sectionname);
2056 if ( _pkg_category($_) ) {
2057 $section->{'sort_weight'} = _pkg_category($_)->weight;
2058 if ( _pkg_category($_)->condense ) {
2059 $section = { %$section, $self->_condense_section($opt{format}) };
2063 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2064 # then it's a tax-only section
2065 $section->{'summarized'} = 'Y';
2066 $section->{'tax_section'} = 'Y';
2068 push @these, $section;
2069 } # foreach $sectionname
2070 } #foreach $locationnum
2071 push @these, @extra_sections if $post_total == 0;
2072 # need an alpha sort for location sections, because postal codes can
2074 $sections[ $post_total ] = [ sort {
2075 $opt{'by_location'} ?
2076 ($a->{sort_weight} cmp $b->{sort_weight}) :
2077 ($a->{sort_weight} <=> $b->{sort_weight})
2079 } #foreach $post_total
2081 return @sections; # early, late
2084 #helper subs for above
2088 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2092 my $categoryname = shift;
2093 $pkg_category_cache{$categoryname} ||=
2094 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2097 my %condensed_format = (
2098 'label' => [ qw( Description Qty Amount ) ],
2100 sub { shift->{description} },
2101 sub { shift->{quantity} },
2102 sub { my($href, %opt) = @_;
2103 ($opt{dollar} || ''). $href->{amount};
2106 'align' => [ qw( l r r ) ],
2107 'span' => [ qw( 5 1 1 ) ], # unitprices?
2108 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2111 sub _condense_section {
2112 my ( $self, $format ) = ( shift, shift );
2114 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2115 qw( description_generator
2118 total_line_generator
2123 sub _condensed_generator_defaults {
2124 my ( $self, $format ) = ( shift, shift );
2125 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2134 sub _condensed_header_generator {
2135 my ( $self, $format ) = ( shift, shift );
2137 my ( $f, $prefix, $suffix, $separator, $column ) =
2138 _condensed_generator_defaults($format);
2140 if ($format eq 'latex') {
2141 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2142 $suffix = "\\\\\n\\hline";
2145 sub { my ($d,$a,$s,$w) = @_;
2146 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2148 } elsif ( $format eq 'html' ) {
2149 $prefix = '<th></th>';
2153 sub { my ($d,$a,$s,$w) = @_;
2154 return qq!<th align="$html_align{$a}">$d</th>!;
2162 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2164 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2167 $prefix. join($separator, @result). $suffix;
2172 sub _condensed_description_generator {
2173 my ( $self, $format ) = ( shift, shift );
2175 my ( $f, $prefix, $suffix, $separator, $column ) =
2176 _condensed_generator_defaults($format);
2178 my $money_char = '$';
2179 if ($format eq 'latex') {
2180 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2182 $separator = " & \n";
2184 sub { my ($d,$a,$s,$w) = @_;
2185 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2187 $money_char = '\\dollar';
2188 }elsif ( $format eq 'html' ) {
2189 $prefix = '"><td align="center"></td>';
2193 sub { my ($d,$a,$s,$w) = @_;
2194 return qq!<td align="$html_align{$a}">$d</td>!;
2196 #$money_char = $conf->config('money_char') || '$';
2197 $money_char = ''; # this is madness
2205 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2207 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2209 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2210 map { $f->{$_}->[$i] } qw(align span width)
2214 $prefix. join( $separator, @result ). $suffix;
2219 sub _condensed_total_generator {
2220 my ( $self, $format ) = ( shift, shift );
2222 my ( $f, $prefix, $suffix, $separator, $column ) =
2223 _condensed_generator_defaults($format);
2226 if ($format eq 'latex') {
2229 $separator = " & \n";
2231 sub { my ($d,$a,$s,$w) = @_;
2232 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2234 }elsif ( $format eq 'html' ) {
2238 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2240 sub { my ($d,$a,$s,$w) = @_;
2241 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2250 # my $r = &{$f->{fields}->[$i]}(@args);
2251 # $r .= ' Total' unless $i;
2253 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2255 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2256 map { $f->{$_}->[$i] } qw(align span width)
2260 $prefix. join( $separator, @result ). $suffix;
2265 =item total_line_generator FORMAT
2267 Returns a coderef used for generation of invoice total line items for this
2268 usage_class. FORMAT is either html or latex
2272 # should not be used: will have issues with hash element names (description vs
2273 # total_item and amount vs total_amount -- another array of functions?
2275 sub _condensed_total_line_generator {
2276 my ( $self, $format ) = ( shift, shift );
2278 my ( $f, $prefix, $suffix, $separator, $column ) =
2279 _condensed_generator_defaults($format);
2282 if ($format eq 'latex') {
2285 $separator = " & \n";
2287 sub { my ($d,$a,$s,$w) = @_;
2288 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2290 }elsif ( $format eq 'html' ) {
2294 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2296 sub { my ($d,$a,$s,$w) = @_;
2297 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2306 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2308 &{$column}( &{$f->{fields}->[$i]}(@args),
2309 map { $f->{$_}->[$i] } qw(align span width)
2313 $prefix. join( $separator, @result ). $suffix;
2318 =item _items_pkg [ OPTIONS ]
2320 Return line item hashes for each package item on this invoice. Nearly
2323 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2325 The only OPTIONS accepted is 'section', which may point to a hashref
2326 with a key named 'condensed', which may have a true value. If it
2327 does, this method tries to merge identical items into items with
2328 'quantity' equal to the number of items (not the sum of their
2329 separate quantities, for some reason).
2335 # The order of these is important. Bundled line items will be merged into
2336 # the most recent non-hidden item, so it needs to be the one with:
2338 # - the same start date
2339 # - no pkgpart_override
2341 # So: sort by pkgnum,
2343 # then sort the base line item before any overrides
2344 # then sort hidden before non-hidden add-ons
2345 # then sort by override pkgpart (for consistency)
2346 sort { $a->pkgnum <=> $b->pkgnum or
2347 $a->sdate <=> $b->sdate or
2348 ($a->pkgpart_override ? 0 : -1) or
2349 ($b->pkgpart_override ? 0 : 1) or
2350 $b->hidden cmp $a->hidden or
2351 $a->pkgpart_override <=> $b->pkgpart_override
2353 # and of course exclude taxes and fees
2354 grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2360 my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2362 foreach my $cust_bill_pkg (@cust_bill_pkg) {
2363 # cache this, so we don't look it up again in every section
2364 my $part_fee = $cust_bill_pkg->get('part_fee')
2365 || $cust_bill_pkg->part_fee;
2366 $cust_bill_pkg->set('part_fee', $part_fee);
2368 #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2369 warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2372 if ( exists($options{section}) and exists($options{section}{category}) )
2374 my $categoryname = $options{section}{category};
2375 # then filter for items that have that section
2376 if ( $part_fee->categoryname ne $categoryname ) {
2377 warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2380 } # otherwise include them all in the main section
2381 # XXX what to do when sectioning by location?
2384 my %base_invnums; # invnum => invoice date
2385 foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2386 if ($_->base_invnum) {
2387 my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2388 my $base_date = $self->time2str_local('short', $base_bill->_date)
2390 $base_invnums{$_->base_invnum} = $base_date || '';
2393 foreach (sort keys(%base_invnums)) {
2394 next if $_ == $self->invnum;
2396 $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2399 { feepart => $cust_bill_pkg->feepart,
2400 amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2401 description => $part_fee->itemdesc_locale($self->cust_main->locale),
2402 ext_description => \@ext_desc
2413 warn "$me _items_pkg searching for all package line items\n"
2416 my @cust_bill_pkg = $self->_items_nontax;
2418 warn "$me _items_pkg filtering line items\n"
2420 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2422 if ($options{section} && $options{section}->{condensed}) {
2424 warn "$me _items_pkg condensing section\n"
2428 local $Storable::canonical = 1;
2429 foreach ( @items ) {
2431 delete $item->{ref};
2432 delete $item->{ext_description};
2433 my $key = freeze($item);
2434 $itemshash{$key} ||= 0;
2435 $itemshash{$key} ++; # += $item->{quantity};
2437 @items = sort { $a->{description} cmp $b->{description} }
2438 map { my $i = thaw($_);
2439 $i->{quantity} = $itemshash{$_};
2441 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2447 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2454 return 0 unless $a->itemdesc cmp $b->itemdesc;
2455 return -1 if $b->itemdesc eq 'Tax';
2456 return 1 if $a->itemdesc eq 'Tax';
2457 return -1 if $b->itemdesc eq 'Other surcharges';
2458 return 1 if $a->itemdesc eq 'Other surcharges';
2459 $a->itemdesc cmp $b->itemdesc;
2464 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
2465 $self->cust_bill_pkg;
2466 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2468 if ( $self->conf->exists('always_show_tax') ) {
2469 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2470 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2472 { 'description' => $itemdesc,
2479 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2481 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2482 list of hashrefs describing the line items they generate on the invoice.
2484 OPTIONS may include:
2486 format: the invoice format.
2488 escape_function: the function used to escape strings.
2490 DEPRECATED? (expensive, mostly unused?)
2491 format_function: the function used to format CDRs.
2493 section: a hashref containing 'category' and/or 'locationnum'; if this
2494 is present, only returns line items that belong to that category and/or
2495 location (whichever is defined).
2497 multisection: a flag indicating that this is a multisection invoice,
2498 which does something complicated.
2500 Returns a list of hashrefs, each of which may contain:
2502 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2503 ext_description, which is an arrayref of detail lines to show below
2508 sub _items_cust_bill_pkg {
2510 my $conf = $self->conf;
2511 my $cust_bill_pkgs = shift;
2514 my $format = $opt{format} || '';
2515 my $escape_function = $opt{escape_function} || sub { shift };
2516 my $format_function = $opt{format_function} || '';
2517 my $no_usage = $opt{no_usage} || '';
2518 my $unsquelched = $opt{unsquelched} || ''; #unused
2519 my ($section, $locationnum, $category);
2520 if ( $opt{section} ) {
2521 $category = $opt{section}->{category};
2522 $locationnum = $opt{section}->{locationnum};
2524 my $summary_page = $opt{summary_page} || ''; #unused
2525 my $multisection = defined($category) || defined($locationnum);
2526 my $discount_show_always = 0;
2528 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2530 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2531 # and location labels
2534 my ($s, $r, $u) = ( undef, undef, undef );
2535 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2538 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2539 if ( $_ && !$cust_bill_pkg->hidden ) {
2540 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2541 $_->{amount} =~ s/^\-0\.00$/0.00/;
2542 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2544 if $_->{amount} != 0
2545 || $discount_show_always
2546 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2547 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2553 if ( $locationnum ) {
2554 # this is a location section; skip packages that aren't at this
2556 next if $cust_bill_pkg->pkgnum == 0; # skips fees...
2557 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2561 # Consider display records for this item to determine if it belongs
2562 # in this section. Note that if there are no display records, there
2563 # will be a default pseudo-record that includes all charge types
2564 # and has no section name.
2565 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2566 ? $cust_bill_pkg->cust_bill_pkg_display
2567 : ( $cust_bill_pkg );
2569 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2570 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2573 if ( defined($category) ) {
2574 # then this is a package category section; process all display records
2575 # that belong to this section.
2576 @cust_bill_pkg_display = grep { $_->section eq $category }
2577 @cust_bill_pkg_display;
2579 # otherwise, process all display records that aren't usage summaries
2580 # (I don't think there should be usage summaries if you aren't using
2581 # category sections, but this is the historical behavior)
2582 @cust_bill_pkg_display = grep { !$_->summary }
2583 @cust_bill_pkg_display;
2586 my $classname = ''; # package class name, will fill in later
2588 foreach my $display (@cust_bill_pkg_display) {
2590 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2591 $display->billpkgdisplaynum. "\n"
2594 my $type = $display->type;
2596 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2597 $desc = substr($desc, 0, $maxlength). '...'
2598 if $format eq 'latex' && length($desc) > $maxlength;
2600 my %details_opt = ( 'format' => $format,
2601 'escape_function' => $escape_function,
2602 'format_function' => $format_function,
2603 'no_usage' => $opt{'no_usage'},
2606 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2608 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2610 # quotation_pkgs are never fees, so don't worry about the case where
2611 # part_pkg is undefined
2613 if ( $cust_bill_pkg->setup != 0 ) {
2614 my $description = $desc;
2615 $description .= ' Setup'
2616 if $cust_bill_pkg->recur != 0
2617 || $discount_show_always
2618 || $cust_bill_pkg->recur_show_zero;
2620 'description' => $description,
2621 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2624 if ( $cust_bill_pkg->recur != 0 ) {
2626 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2627 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2631 } elsif ( $cust_bill_pkg->pkgnum > 0 ) { # and it's not a quotation_pkg
2633 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2636 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2637 my $part_pkg = $cust_pkg->part_pkg;
2639 # which pkgpart to show for display purposes?
2640 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2642 # start/end dates for invoice formats that do nonstandard
2644 my %item_dates = ();
2645 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2646 unless $part_pkg->option('disable_line_item_date_ranges',1);
2648 # not normally used, but pass this to the template anyway
2649 $classname = $part_pkg->classname;
2651 if ( (!$type || $type eq 'S')
2652 && ( $cust_bill_pkg->setup != 0
2653 || $cust_bill_pkg->setup_show_zero
2658 warn "$me _items_cust_bill_pkg adding setup\n"
2661 my $description = $desc;
2662 $description .= ' Setup'
2663 if $cust_bill_pkg->recur != 0
2664 || $discount_show_always
2665 || $cust_bill_pkg->recur_show_zero;
2667 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2669 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2670 # triggered by the recurring charge freq
2672 && $cust_bill_pkg->recur == 0
2673 && ! $cust_bill_pkg->recur_show_zero;
2678 # always pass the svc_label through to the template, even if
2679 # not displaying it as an ext_description
2680 my @svc_labels = map &{$escape_function}($_),
2681 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2683 $svc_label = $svc_labels[0];
2685 unless ( $cust_pkg->part_pkg->hide_svc_detail
2686 || $cust_bill_pkg->hidden )
2689 push @d, @svc_labels
2690 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2691 my $lnum = $cust_main ? $cust_main->ship_locationnum
2692 : $self->prospect_main->locationnum;
2693 # show the location label if it's not the customer's default
2694 # location, and we're not grouping items by location already
2695 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2696 my $loc = $cust_pkg->location_label;
2697 $loc = substr($loc, 0, $maxlength). '...'
2698 if $format eq 'latex' && length($loc) > $maxlength;
2699 push @d, &{$escape_function}($loc);
2702 } #unless hiding service details
2704 push @d, $cust_bill_pkg->details(%details_opt)
2705 if $cust_bill_pkg->recur == 0;
2707 if ( $cust_bill_pkg->hidden ) {
2708 $s->{amount} += $cust_bill_pkg->setup;
2709 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2710 push @{ $s->{ext_description} }, @d;
2714 description => $description,
2715 pkgpart => $pkgpart,
2716 pkgnum => $cust_bill_pkg->pkgnum,
2717 amount => $cust_bill_pkg->setup,
2718 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2719 unit_amount => $cust_bill_pkg->unitsetup,
2720 quantity => $cust_bill_pkg->quantity,
2721 ext_description => \@d,
2722 svc_label => ($svc_label || ''),
2723 locationnum => $cust_pkg->locationnum, # sure, why not?
2729 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2731 $cust_bill_pkg->recur != 0
2732 || $cust_bill_pkg->setup == 0
2733 || $discount_show_always
2734 || $cust_bill_pkg->recur_show_zero
2739 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2742 my $is_summary = $display->summary;
2743 my $description = $desc;
2744 if ( $type eq 'U' and defined($r) ) {
2745 # don't just show the same description as the recur line
2746 $description = $self->mt('Usage charges');
2749 my $part_pkg = $cust_pkg->part_pkg;
2751 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2755 my @seconds = (); # for display of usage info
2758 #at least until cust_bill_pkg has "past" ranges in addition to
2759 #the "future" sdate/edate ones... see #3032
2760 my @dates = ( $self->_date );
2761 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2762 push @dates, $prev->sdate if $prev;
2763 push @dates, undef if !$prev;
2765 my @svc_labels = map &{$escape_function}($_),
2766 $cust_pkg->h_labels_short(@dates, 'I');
2767 $svc_label = $svc_labels[0];
2769 # show service labels, unless...
2770 # the package is set not to display them
2771 unless ( $part_pkg->hide_svc_detail
2772 # or this is a tax-like line item
2773 || $cust_bill_pkg->itemdesc
2774 # or this is a hidden (bundled) line item
2775 || $cust_bill_pkg->hidden
2776 # or this is a usage summary line
2777 || $is_summary && $type && $type eq 'U'
2778 # or this is a usage line and there's a recurring line
2779 # for the package in the same section (which will
2780 # have service labels already)
2781 || ($type eq 'U' and defined($r))
2785 warn "$me _items_cust_bill_pkg adding service details\n"
2788 push @d, @svc_labels
2789 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2790 warn "$me _items_cust_bill_pkg done adding service details\n"
2793 my $lnum = $cust_main ? $cust_main->ship_locationnum
2794 : $self->prospect_main->locationnum;
2795 # show the location label if it's not the customer's default
2796 # location, and we're not grouping items by location already
2797 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2798 my $loc = $cust_pkg->location_label;
2799 $loc = substr($loc, 0, $maxlength). '...'
2800 if $format eq 'latex' && length($loc) > $maxlength;
2801 push @d, &{$escape_function}($loc);
2804 # Display of seconds_since_sqlradacct:
2805 # On the invoice, when processing @detail_items, look for a field
2806 # named 'seconds'. This will contain total seconds for each
2807 # service, in the same order as @ext_description. For services
2808 # that don't support this it will show undef.
2809 if ( $conf->exists('svc_acct-usage_seconds')
2810 and ! $cust_bill_pkg->pkgpart_override ) {
2811 foreach my $cust_svc (
2812 $cust_pkg->h_cust_svc(@dates, 'I')
2815 # eval because not having any part_export_usage exports
2816 # is a fatal error, last_bill/_date because that's how
2817 # sqlradius_hour billing does it
2819 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2821 push @seconds, $sec;
2823 } #if svc_acct-usage_seconds
2825 } # if we are showing service labels
2827 unless ( $is_summary ) {
2828 warn "$me _items_cust_bill_pkg adding details\n"
2831 #instead of omitting details entirely in this case (unwanted side
2832 # effects), just omit CDRs
2833 $details_opt{'no_usage'} = 1
2834 if $type && $type eq 'R';
2836 push @d, $cust_bill_pkg->details(%details_opt);
2839 warn "$me _items_cust_bill_pkg calculating amount\n"
2844 $amount = $cust_bill_pkg->recur;
2845 } elsif ($type eq 'R') {
2846 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2847 } elsif ($type eq 'U') {
2848 $amount = $cust_bill_pkg->usage;
2851 if ( !$type || $type eq 'R' ) {
2853 warn "$me _items_cust_bill_pkg adding recur\n"
2857 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2860 if ( $cust_bill_pkg->hidden ) {
2861 $r->{amount} += $amount;
2862 $r->{unit_amount} += $unit_amount;
2863 push @{ $r->{ext_description} }, @d;
2866 description => $description,
2867 pkgpart => $pkgpart,
2868 pkgnum => $cust_bill_pkg->pkgnum,
2870 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2871 unit_amount => $unit_amount,
2872 quantity => $cust_bill_pkg->quantity,
2874 ext_description => \@d,
2875 svc_label => ($svc_label || ''),
2876 locationnum => $cust_pkg->locationnum,
2878 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2881 } else { # $type eq 'U'
2883 warn "$me _items_cust_bill_pkg adding usage\n"
2886 if ( $cust_bill_pkg->hidden and defined($u) ) {
2887 # if this is a hidden package and there's already a usage
2888 # line for the bundle, add this package's total amount and
2889 # usage details to it
2890 $u->{amount} += $amount;
2891 push @{ $u->{ext_description} }, @d;
2892 } elsif ( $amount ) {
2893 # create a new usage line
2895 description => $description,
2896 pkgpart => $pkgpart,
2897 pkgnum => $cust_bill_pkg->pkgnum,
2900 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2902 ext_description => \@d,
2903 locationnum => $cust_pkg->locationnum,
2905 } # else this has no usage, so don't create a usage section
2908 } # recurring or usage with recurring charge
2910 } else { # taxes and fees
2912 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2915 # items of this kind should normally not have sdate/edate.
2917 'description' => $desc,
2918 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
2919 + $cust_bill_pkg->recur)
2922 } # if quotation / package line item / other line item
2924 } # foreach $display
2926 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2927 && $conf->exists('discount-show-always'));
2931 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2933 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2934 if exists($_->{amount});
2935 $_->{amount} =~ s/^\-0\.00$/0.00/;
2936 $_->{unit_amount} = sprintf('%.2f', $_->{unit_amount})
2937 if exists($_->{unit_amount});
2940 if $_->{amount} != 0
2941 || $discount_show_always
2942 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2943 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2947 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2954 =item _items_discounts_avail
2956 Returns an array of line item hashrefs representing available term discounts
2957 for this invoice. This makes the same assumptions that apply to term
2958 discounts in general: that the package is billed monthly, at a flat rate,
2959 with no usage charges. A prorated first month will be handled, as will
2960 a setup fee if the discount is allowed to apply to setup fees.
2964 sub _items_discounts_avail {
2967 #maybe move this method from cust_bill when quotations support discount_plans
2968 return () unless $self->can('discount_plans');
2969 my %plans = $self->discount_plans;
2971 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2972 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2976 my $plan = $plans{$months};
2978 my $term_total = sprintf('%.2f', $plan->discounted_total);
2979 my $percent = sprintf('%.0f',
2980 100 * (1 - $term_total / $plan->base_total) );
2981 my $permonth = sprintf('%.2f', $term_total / $months);
2982 my $detail = $self->mt('discount on item'). ' '.
2983 join(', ', map { "#$_" } $plan->pkgnums)
2986 # discounts for non-integer months don't work anyway
2987 $months = sprintf("%d", $months);
2990 description => $self->mt('Save [_1]% by paying for [_2] months',
2992 amount => $self->mt('[_1] ([_2] per month)',
2993 $term_total, $money_char.$permonth),
2994 ext_description => ($detail || ''),
2997 sort { $b <=> $a } keys %plans;