1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use List::Util qw(sum);
11 use Text::Template 1.20;
17 use FS::Record qw( qsearch qsearchs );
18 use FS::Misc qw( generate_ps generate_pdf );
25 $me = '[FS::Template_Mixin]';
26 FS::UID->install_callback( sub {
27 my $conf = new FS::Conf; #global
28 $money_char = $conf->config('money_char') || '$';
29 $date_format = $conf->config('date_format') || '%x'; #/YY
30 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
31 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
36 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
38 If the "mode" pseudo-field is set on the object, the configuration handle
39 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
46 my $mode = $self->get('mode');
47 if ($self->{_conf} and !defined($mode)) {
48 return $self->{_conf};
51 my $cust_main = $self->cust_main;
52 my $locale = $cust_main ? $cust_main->locale : '';
55 if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
56 $mode = $mode->modenum;
57 } elsif ( $mode =~ /\D/ ) {
58 die "invalid invoice mode $mode";
60 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
62 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
63 # it doesn't have a locale, but system conf still might
64 $conf->set('locale' => $locale) if $conf;
67 # if $mode is unspecified, or if there is no invoice_conf matching this mode
68 # and locale, then use the system config only (but with the locale)
69 $conf ||= FS::Conf->new({ 'locale' => $locale });
71 return $self->{_conf} = $conf;
74 =item print_text OPTIONS
76 Returns an text invoice, as a list of lines.
78 Options can be passed as a hash.
80 I<time>, if specified, is used to control the printing of overdue messages. The
81 default is now. It isn't the date of the invoice; that's the `_date' field.
82 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
83 L<Time::Local> and L<Date::Parse> for conversion functions.
85 I<template>, if specified, is the name of a suffix for alternate invoices.
87 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
95 %params = %{ shift() };
100 $params{'format'} = 'template'; # for some reason
102 $self->print_generic( %params );
105 =item print_latex HASHREF
107 Internal method - returns a filename of a filled-in LaTeX template for this
108 invoice (Note: add ".tex" to get the actual filename), and a filename of
109 an associated logo (with the .eps extension included).
111 See print_ps and print_pdf for methods that return PostScript and PDF output.
113 Options can be passed as a hash.
115 I<time>, if specified, is used to control the printing of overdue messages. The
116 default is now. It isn't the date of the invoice; that's the `_date' field.
117 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
118 L<Time::Local> and L<Date::Parse> for conversion functions.
120 I<template>, if specified, is the name of a suffix for alternate invoices.
121 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
122 customize invoice templates for different purposes.
124 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
133 %params = %{ shift() };
138 $params{'format'} = 'latex';
139 my $conf = $self->conf;
141 # this needs to go away
142 my $template = $params{'template'};
143 # and this especially
144 $template ||= $self->_agent_template
145 if $self->can('_agent_template');
147 my $pkey = $self->primary_key;
148 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
150 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
151 my $lh = new File::Temp(
152 TEMPLATE => $tmp_template,
156 ) or die "can't open temp file: $!\n";
158 my $agentnum = $self->agentnum;
160 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
161 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
162 or die "can't write temp file: $!\n";
164 print $lh $conf->config_binary('logo.eps', $agentnum)
165 or die "can't write temp file: $!\n";
168 $params{'logo_file'} = $lh->filename;
170 if( $conf->exists('invoice-barcode')
171 && $self->can('invoice_barcode')
172 && $self->invnum ) { # don't try to barcode statements
173 my $png_file = $self->invoice_barcode($dir);
174 my $eps_file = $png_file;
175 $eps_file =~ s/\.png$/.eps/g;
176 $png_file =~ /(barcode.*png)/;
178 $eps_file =~ /(barcode.*eps)/;
181 my $curr_dir = cwd();
183 # after painfuly long experimentation, it was determined that sam2p won't
184 # accept : and other chars in the path, no matter how hard I tried to
185 # escape them, hence the chdir (and chdir back, just to be safe)
186 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
187 or die "sam2p failed: $!\n";
191 $params{'barcode_file'} = $eps_file;
194 my @filled_in = $self->print_generic( %params );
196 my $fh = new File::Temp( TEMPLATE => $tmp_template,
200 ) or die "can't open temp file: $!\n";
201 binmode($fh, ':utf8'); # language support
202 print $fh join('', @filled_in );
205 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
206 return ($1, $params{'logo_file'}, $params{'barcode_file'});
212 my $cust_main = $self->cust_main;
213 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
216 =item print_generic OPTION => VALUE ...
218 Internal method - returns a filled-in template for this invoice as a scalar.
220 See print_ps and print_pdf for methods that return PostScript and PDF output.
222 Non optional options include
223 format - latex, html, template
225 Optional options include
227 template - a value used as a suffix for a configuration template. Please
230 time - a value used to control the printing of overdue messages. The
231 default is now. It isn't the date of the invoice; that's the `_date' field.
232 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
233 L<Time::Local> and L<Date::Parse> for conversion functions.
237 unsquelch_cdr - overrides any per customer cdr squelching when true
239 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
241 locale - override customer's locale
245 #what's with all the sprintf('%10.2f')'s in here? will it cause any
246 # (alignment in text invoice?) problems to change them all to '%.2f' ?
247 # yes: fixed width/plain text printing will be borked
249 my( $self, %params ) = @_;
250 my $conf = $self->conf;
252 my $today = $params{today} ? $params{today} : time;
253 warn "$me print_generic called on $self with suffix $params{template}\n"
256 my $format = $params{format};
257 die "Unknown format: $format"
258 unless $format =~ /^(latex|html|template)$/;
260 my $cust_main = $self->cust_main || $self->prospect_main;
261 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
262 unless $cust_main->payname
263 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
265 my $locale = $params{'locale'} || $cust_main->locale;
267 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
268 'html' => [ '<%=', '%>' ],
269 'template' => [ '{', '}' ],
272 warn "$me print_generic creating template\n"
275 # set the notice name here, and nowhere else.
276 my $notice_name = $params{notice_name}
277 || $conf->config('notice_name')
278 || $self->notice_name;
281 my $template = $params{template} ? $params{template} : $self->_agent_template;
282 my $templatefile = $self->template_conf. $format;
283 $templatefile .= "_$template"
284 if length($template) && $conf->exists($templatefile."_$template");
287 my @invoice_template = map "$_\n", $conf->config($templatefile)
288 or die "cannot load config data $templatefile";
291 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
292 #change this to a die when the old code is removed
293 warn "old-style invoice template $templatefile; ".
294 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
296 @invoice_template = _translate_old_latex_format(@invoice_template);
299 warn "$me print_generic creating T:T object\n"
302 my $text_template = new Text::Template(
304 SOURCE => \@invoice_template,
305 DELIMITERS => $delimiters{$format},
308 warn "$me print_generic compiling T:T object\n"
311 $text_template->compile()
312 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
315 # additional substitution could possibly cause breakage in existing templates
318 'notes' => sub { map "$_", @_ },
319 'footer' => sub { map "$_", @_ },
320 'smallfooter' => sub { map "$_", @_ },
321 'returnaddress' => sub { map "$_", @_ },
322 'coupon' => sub { map "$_", @_ },
323 'summary' => sub { map "$_", @_ },
329 s/%%(.*)$/<!-- $1 -->/g;
330 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
331 s/\\begin\{enumerate\}/<ol>/g;
333 s/\\end\{enumerate\}/<\/ol>/g;
334 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
343 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
345 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
351 s/\\hyphenation\{[\w\s\-]+}//;
356 'coupon' => sub { "" },
357 'summary' => sub { "" },
364 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
365 s/\\begin\{enumerate\}//g;
367 s/\\end\{enumerate\}//g;
368 s/\\textbf\{(.*)\}/$1/g;
375 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
377 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
382 s/\\\\\*?\s*$/\n/; # dubious
383 s/\\hyphenation\{[\w\s\-]+}//;
387 'coupon' => sub { "" },
388 'summary' => sub { "" },
393 # hashes for differing output formats
394 my %nbsps = ( 'latex' => '~',
395 'html' => '', # '&nbps;' would be nice
396 'template' => '', # not used
398 my $nbsp = $nbsps{$format};
400 my %escape_functions = ( 'latex' => \&_latex_escape,
401 'html' => \&_html_escape_nbsp,#\&encode_entities,
402 'template' => sub { shift },
404 my $escape_function = $escape_functions{$format};
405 my $escape_function_nonbsp = ($format eq 'html')
406 ? \&_html_escape : $escape_function;
408 my %date_formats = ( 'latex' => $date_format_long,
409 'html' => $date_format_long,
412 $date_formats{'html'} =~ s/ / /g;
414 my $date_format = $date_formats{$format};
416 my %newline_tokens = ( 'latex' => '\\\\',
420 my $newline_token = $newline_tokens{$format};
422 warn "$me generating template variables\n"
425 # generate template variables
429 defined( $conf->config_orbase( "invoice_${format}returnaddress",
433 && length( $conf->config_orbase( "invoice_${format}returnaddress",
439 $returnaddress = join("\n",
440 $conf->config_orbase("invoice_${format}returnaddress", $template)
444 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
446 my $convert_map = $convert_maps{$format}{'returnaddress'};
449 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
454 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
456 my $convert_map = $convert_maps{$format}{'returnaddress'};
457 $returnaddress = join( "\n", &$convert_map(
458 map { s/( {2,})/'~' x length($1)/eg;
462 ( $conf->config('company_name', $cust_main->agentnum),
463 $conf->config('company_address', $cust_main->agentnum),
470 my $warning = "Couldn't find a return address; ".
471 "do you need to set the company_address configuration value?";
473 $returnaddress = $nbsp;
474 #$returnaddress = $warning;
478 warn "$me generating invoice data\n"
481 my $agentnum = $cust_main->agentnum;
486 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
487 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
488 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
489 'returnaddress' => $returnaddress,
490 'agent' => &$escape_function($cust_main->agent->agent),
492 #invoice/quotation info
493 'no_number' => $params{'no_number'},
494 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
495 'quotationnum' => $self->quotationnum,
496 'no_date' => $params{'no_date'},
497 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
498 'date' => ( $params{'no_date'}
500 : time2str($date_format, $self->_date)
502 'today' => time2str($date_format_long, $today),
503 'terms' => $self->terms,
504 'template' => $template, #params{'template'},
505 'notice_name' => $notice_name, # escape?
506 'current_charges' => sprintf("%.2f", $self->charged),
507 'duedate' => $self->due_date2str($rdate_format), #date_format?
510 'custnum' => $cust_main->display_custnum,
511 'prospectnum' => $cust_main->prospectnum,
512 'agent_custid' => &$escape_function($cust_main->agent_custid),
513 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
514 payname company address1 address2 city state zip fax
518 'ship_enable' => $conf->exists('invoice-ship_address'),
519 'unitprices' => $conf->exists('invoice-unitprice'),
520 'smallernotes' => $conf->exists('invoice-smallernotes'),
521 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
522 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
524 #layout info -- would be fancy to calc some of this and bury the template
526 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
527 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
528 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
529 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
530 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
531 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
532 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
533 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
534 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
535 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
537 # better hang on to conf_dir for a while (for old templates)
538 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
540 #these are only used when doing paged plaintext
547 my $lh = FS::L10N->get_handle( $locale );
548 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
549 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
550 # eval to avoid death for unimplemented languages
551 my $dh = eval { Date::Language->new($info{'name'}) } ||
552 Date::Language->new(); # fall back to English
553 # prototype here to silence warnings
554 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
555 # eventually use this date handle everywhere in here, too
557 my $min_sdate = 999999999999;
559 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
560 next unless $cust_bill_pkg->pkgnum > 0;
561 $min_sdate = $cust_bill_pkg->sdate
562 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
563 $max_edate = $cust_bill_pkg->edate
564 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
567 $invoice_data{'bill_period'} = '';
568 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
569 . " to " . time2str('%e %h', $max_edate)
570 if ($max_edate != 0 && $min_sdate != 999999999999);
572 $invoice_data{finance_section} = '';
573 if ( $conf->config('finance_pkgclass') ) {
575 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
576 $invoice_data{finance_section} = $pkg_class->categoryname;
578 $invoice_data{finance_amount} = '0.00';
579 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
581 my $countrydefault = $conf->config('countrydefault') || 'US';
582 foreach ( qw( address1 address2 city state zip country fax) ){
583 my $method = 'ship_'.$_;
584 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
586 foreach ( qw( contact company ) ) { #compatibility
587 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
589 $invoice_data{'ship_country'} = ''
590 if ( $invoice_data{'ship_country'} eq $countrydefault );
592 $invoice_data{'cid'} = $params{'cid'}
595 if ( $cust_main->country eq $countrydefault ) {
596 $invoice_data{'country'} = '';
598 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
602 $invoice_data{'address'} = \@address;
605 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
606 ? " (P.O. #". $cust_main->payinfo. ")"
610 push @address, $cust_main->company
611 if $cust_main->company;
612 push @address, $cust_main->address1;
613 push @address, $cust_main->address2
614 if $cust_main->address2;
616 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
617 push @address, $invoice_data{'country'}
618 if $invoice_data{'country'};
620 while (scalar(@address) < 5);
622 $invoice_data{'logo_file'} = $params{'logo_file'}
623 if $params{'logo_file'};
624 $invoice_data{'barcode_file'} = $params{'barcode_file'}
625 if $params{'barcode_file'};
626 $invoice_data{'barcode_img'} = $params{'barcode_img'}
627 if $params{'barcode_img'};
628 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
629 if $params{'barcode_cid'};
631 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
632 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
633 #my $balance_due = $self->owed + $pr_total - $cr_total;
634 my $balance_due = $self->owed + $pr_total;
636 #these are used on the summary page only
638 # the customer's current balance as shown on the invoice before this one
639 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
641 # the change in balance from that invoice to this one
642 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
644 # the sum of amount owed on all previous invoices
645 # ($pr_total is used elsewhere but not as $previous_balance)
646 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
648 # the sum of amount owed on all invoices
649 # (this is used in the summary & on the payment coupon)
650 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
652 # info from customer's last invoice before this one, for some
654 $invoice_data{'last_bill'} = {};
656 if ( $self->custnum && $self->invnum ) {
658 if ( $self->previous_bill ) {
659 my $last_bill = $self->previous_bill;
660 $invoice_data{'last_bill'} = {
661 '_date' => $last_bill->_date, #unformatted
663 my (@payments, @credits);
664 # for formats that itemize previous payments
665 foreach my $cust_pay ( qsearch('cust_pay', {
666 'custnum' => $self->custnum,
667 '_date' => { op => '>=',
668 value => $last_bill->_date }
671 next if $cust_pay->_date > $self->_date;
673 '_date' => $cust_pay->_date,
674 'date' => time2str($date_format, $cust_pay->_date),
675 'payinfo' => $cust_pay->payby_payinfo_pretty,
676 'amount' => sprintf('%.2f', $cust_pay->paid),
678 # not concerned about applications
680 foreach my $cust_credit ( qsearch('cust_credit', {
681 'custnum' => $self->custnum,
682 '_date' => { op => '>=',
683 value => $last_bill->_date }
686 next if $cust_credit->_date > $self->_date;
688 '_date' => $cust_credit->_date,
689 'date' => time2str($date_format, $cust_credit->_date),
690 'creditreason'=> $cust_credit->reason,
691 'amount' => sprintf('%.2f', $cust_credit->amount),
694 $invoice_data{'previous_payments'} = \@payments;
695 $invoice_data{'previous_credits'} = \@credits;
700 my $summarypage = '';
701 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
704 $invoice_data{'summarypage'} = $summarypage;
706 warn "$me substituting variables in notes, footer, smallfooter\n"
709 my $tc = $self->template_conf;
710 my @include = ( [ $tc, 'notes' ],
711 [ 'invoice_', 'footer' ],
712 [ 'invoice_', 'smallfooter', ],
714 push @include, [ $tc, 'coupon', ]
715 unless $params{'no_coupon'};
717 foreach my $i (@include) {
719 my($base, $include) = @$i;
721 my $inc_file = $conf->key_orbase("$base$format$include", $template);
724 if ( $conf->exists($inc_file, $agentnum)
725 && length( $conf->config($inc_file, $agentnum) ) ) {
727 @inc_src = $conf->config($inc_file, $agentnum);
731 $inc_file = $conf->key_orbase("${base}latex$include", $template);
733 my $convert_map = $convert_maps{$format}{$include};
735 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
736 s/--\@\]/$delimiters{$format}[1]/g;
739 &$convert_map( $conf->config($inc_file, $agentnum) );
743 my $inc_tt = new Text::Template (
745 SOURCE => [ map "$_\n", @inc_src ],
746 DELIMITERS => $delimiters{$format},
747 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
749 unless ( $inc_tt->compile() ) {
750 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
751 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
755 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
757 $invoice_data{$include} =~ s/\n+$//
758 if ($format eq 'latex');
761 # let invoices use either of these as needed
762 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
763 ? $cust_main->payinfo : '';
764 $invoice_data{'po_line'} =
765 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
766 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
769 my %money_chars = ( 'latex' => '',
770 'html' => $conf->config('money_char') || '$',
773 my $money_char = $money_chars{$format};
775 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
776 'html' => $conf->config('money_char') || '$',
779 my $other_money_char = $other_money_chars{$format};
780 $invoice_data{'dollar'} = $other_money_char;
782 my %minus_signs = ( 'latex' => '$-$',
784 'template' => '- ' );
785 my $minus = $minus_signs{$format};
787 my @detail_items = ();
788 my @total_items = ();
792 $invoice_data{'detail_items'} = \@detail_items;
793 $invoice_data{'total_items'} = \@total_items;
794 $invoice_data{'buf'} = \@buf;
795 $invoice_data{'sections'} = \@sections;
797 warn "$me generating sections\n"
801 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
802 'subtotal' => $taxtotal, # adjusted below
805 my $tax_weight = _pkg_category($tax_section->{description})
806 ? _pkg_category($tax_section->{description})->weight
808 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
809 $tax_section->{'sort_weight'} = $tax_weight;
812 my $adjust_section = {
813 'description' => $self->mt('Credits, Payments, and Adjustments'),
814 'adjust_section' => 1,
815 'subtotal' => 0, # adjusted below
817 my $adjust_weight = _pkg_category($adjust_section->{description})
818 ? _pkg_category($adjust_section->{description})->weight
820 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
821 $adjust_section->{'sort_weight'} = $adjust_weight;
823 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
824 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
825 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
826 $invoice_data{'multisection'} = $multisection;
828 my $extra_sections = [];
829 my $extra_lines = ();
831 # default section ('Charges')
832 my $default_section = { 'description' => '',
837 # Previous Charges section
838 # subtotal is the first return value from $self->previous
839 my $previous_section;
840 # if the invoice has major sections, or if we're summarizing previous
841 # charges with a single line, or if we've been specifically told to put them
842 # in a section, create a section for previous charges:
843 if ( $multisection or
844 $conf->exists('previous_balance-summary_only') or
845 $conf->exists('previous_balance-section') ) {
847 $previous_section = { 'description' => $self->mt('Previous Charges'),
848 'subtotal' => $other_money_char.
849 sprintf('%.2f', $pr_total),
850 'summarized' => '', #why? $summarypage ? 'Y' : '',
852 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
853 join(' / ', map { $cust_main->balance_date_range(@$_) }
854 $self->_prior_month30s
856 if $conf->exists('invoice_include_aging');
859 # otherwise put them in the main section
860 $previous_section = $default_section;
863 if ( $multisection ) {
864 ($extra_sections, $extra_lines) =
865 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
866 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
867 && $self->can('_items_extra_usage_sections');
869 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
871 push @detail_items, @$extra_lines if $extra_lines;
873 # the code is written so that both methods can be used together, but
874 # we haven't yet changed the template to take advantage of that, so for
875 # now, treat them as mutually exclusive.
876 my %section_method = ( by_category => 1 );
877 if ( $conf->exists($tc.'sections_by_location') ) {
878 %section_method = ( by_location => 1 );
881 $self->_items_sections( 'summary' => $summarypage,
882 'escape' => $escape_function_nonbsp,
883 'extra_sections' => $extra_sections,
887 push @sections, @$early;
888 $late_sections = $late;
890 if ( $conf->exists('svc_phone_sections')
891 && $self->can('_items_svc_phone_sections')
894 my ($phone_sections, $phone_lines) =
895 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
896 push @{$late_sections}, @$phone_sections;
897 push @detail_items, @$phone_lines;
899 if ( $conf->exists('voip-cust_accountcode_cdr')
900 && $cust_main->accountcode_cdr
901 && $self->can('_items_accountcode_cdr')
904 my ($accountcode_section, $accountcode_lines) =
905 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
906 if ( scalar(@$accountcode_lines) ) {
907 push @{$late_sections}, $accountcode_section;
908 push @detail_items, @$accountcode_lines;
911 } else {# not multisection
912 # make a default section
913 push @sections, $default_section;
914 # and calculate the finance charge total, since it won't get done otherwise.
915 # and the default section total
916 # XXX possibly finance_pkgclass should not be used in this manner?
919 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
920 if ( $invoice_data{finance_section} and
921 grep { $_->section eq $invoice_data{finance_section} }
922 $cust_bill_pkg->cust_bill_pkg_display ) {
923 # I think these are always setup fees, but just to be sure...
924 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
926 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
929 $invoice_data{finance_amount} =
930 sprintf('%.2f', sum( @finance_charges ) || 0);
931 $default_section->{subtotal} = $other_money_char.
932 sprintf('%.2f', sum( @charges ) || 0);
935 # previous invoice balances in the Previous Charges section if there
936 # is one, otherwise in the main detail section
937 # (except if summary_only is enabled, don't show them at all)
938 if ( $self->can('_items_previous') &&
939 $self->enable_previous &&
940 ! $conf->exists('previous_balance-summary_only') ) {
942 warn "$me adding previous balances\n"
945 foreach my $line_item ( $self->_items_previous ) {
948 ref => $line_item->{'pkgnum'},
949 pkgpart => $line_item->{'pkgpart'},
951 section => $previous_section, # which might be $default_section
952 description => &$escape_function($line_item->{'description'}),
953 ext_description => [ map { &$escape_function($_) }
954 @{ $line_item->{'ext_description'} || [] }
956 amount => ( $old_latex ? '' : $money_char).
957 $line_item->{'amount'},
958 product_code => $line_item->{'pkgpart'} || 'N/A',
961 push @detail_items, $detail;
962 push @buf, [ $detail->{'description'},
963 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
969 if ( @pr_cust_bill && $self->enable_previous ) {
970 push @buf, ['','-----------'];
971 push @buf, [ $self->mt('Total Previous Balance'),
972 $money_char. sprintf("%10.2f", $pr_total) ];
976 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
977 warn "$me adding DID summary\n"
980 my ($didsummary,$minutes) = $self->_did_summary;
981 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
983 { 'description' => $didsummary_desc,
984 'ext_description' => [ $didsummary, $minutes ],
988 foreach my $section (@sections, @$late_sections) {
990 # begin some normalization
991 $section->{'subtotal'} = $section->{'amount'}
993 && !exists($section->{subtotal})
994 && exists($section->{amount});
996 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
997 if ( $invoice_data{finance_section} &&
998 $section->{'description'} eq $invoice_data{finance_section} );
1000 $section->{'subtotal'} = $other_money_char.
1001 sprintf('%.2f', $section->{'subtotal'})
1004 # continue some normalization
1005 $section->{'amount'} = $section->{'subtotal'}
1009 if ( $section->{'description'} ) {
1010 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1015 warn "$me setting options\n"
1019 $options{'section'} = $section if $multisection;
1020 $options{'format'} = $format;
1021 $options{'escape_function'} = $escape_function;
1022 $options{'no_usage'} = 1 unless $unsquelched;
1023 $options{'unsquelched'} = $unsquelched;
1024 $options{'summary_page'} = $summarypage;
1025 $options{'skip_usage'} =
1026 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1028 warn "$me searching for line items\n"
1031 foreach my $line_item ( $self->_items_pkg(%options) ) {
1033 warn "$me adding line item $line_item\n"
1037 ext_description => [],
1039 $detail->{'ref'} = $line_item->{'pkgnum'};
1040 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1041 $detail->{'quantity'} = $line_item->{'quantity'};
1042 $detail->{'section'} = $section;
1043 $detail->{'description'} = &$escape_function($line_item->{'description'});
1044 if ( exists $line_item->{'ext_description'} ) {
1045 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1047 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1048 $line_item->{'amount'};
1049 if ( exists $line_item->{'unit_amount'} ) {
1050 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1051 $line_item->{'unit_amount'};
1053 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1055 $detail->{'sdate'} = $line_item->{'sdate'};
1056 $detail->{'edate'} = $line_item->{'edate'};
1057 $detail->{'seconds'} = $line_item->{'seconds'};
1058 $detail->{'svc_label'} = $line_item->{'svc_label'};
1060 push @detail_items, $detail;
1061 push @buf, ( [ $detail->{'description'},
1062 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1064 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1068 if ( $section->{'description'} ) {
1069 push @buf, ( ['','-----------'],
1070 [ $section->{'description'}. ' sub-total',
1071 $section->{'subtotal'} # already formatted this
1080 $invoice_data{current_less_finance} =
1081 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1083 # if there's anything in the Previous Charges section, prepend it to the list
1084 if ( $pr_total and $previous_section ne $default_section ) {
1085 unshift @sections, $previous_section;
1088 warn "$me adding taxes\n"
1091 my @items_tax = $self->_items_tax;
1092 foreach my $tax ( @items_tax ) {
1094 $taxtotal += $tax->{'amount'};
1096 my $description = &$escape_function( $tax->{'description'} );
1097 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1099 if ( $multisection ) {
1101 my $money = $old_latex ? '' : $money_char;
1102 push @detail_items, {
1103 ext_description => [],
1106 description => $description,
1107 amount => $money. $amount,
1109 section => $tax_section,
1114 push @total_items, {
1115 'total_item' => $description,
1116 'total_amount' => $other_money_char. $amount,
1121 push @buf,[ $description,
1122 $money_char. $amount,
1129 $total->{'total_item'} = $self->mt('Sub-total');
1130 $total->{'total_amount'} =
1131 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1133 if ( $multisection ) {
1134 $tax_section->{'subtotal'} = $other_money_char.
1135 sprintf('%.2f', $taxtotal);
1136 $tax_section->{'pretotal'} = 'New charges sub-total '.
1137 $total->{'total_amount'};
1138 push @sections, $tax_section if $taxtotal;
1140 unshift @total_items, $total;
1143 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1145 push @buf,['','-----------'];
1146 push @buf,[$self->mt(
1147 (!$self->enable_previous)
1149 : 'Total New Charges'
1151 $money_char. sprintf("%10.2f",$self->charged) ];
1159 my %embolden_functions = (
1160 'latex' => sub { return '\textbf{'. shift(). '}' },
1161 'html' => sub { return '<b>'. shift(). '</b>' },
1162 'template' => sub { shift },
1164 my $embolden_function = $embolden_functions{$format};
1166 if ( $self->can('_items_total') ) { # quotations
1168 $self->_items_total(\@total_items);
1170 foreach ( @total_items ) {
1171 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1172 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1173 $_->{'total_amount'}
1177 } else { #normal invoice case
1179 # calculate total, possibly including total owed on previous
1183 $item = $conf->config('previous_balance-exclude_from_total')
1184 || 'Total New Charges'
1185 if $conf->exists('previous_balance-exclude_from_total');
1186 my $amount = $self->charged;
1187 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1188 $amount += $pr_total;
1191 $total->{'total_item'} = &$embolden_function($self->mt($item));
1192 $total->{'total_amount'} =
1193 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1194 if ( $multisection ) {
1195 if ( $adjust_section->{'sort_weight'} ) {
1196 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1197 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1199 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1200 $other_money_char. sprintf('%.2f', $self->charged );
1203 push @total_items, $total;
1205 push @buf,['','-----------'];
1208 sprintf( '%10.2f', $amount )
1212 # if we're showing previous invoices, also show previous
1213 # credits and payments
1214 if ( $self->enable_previous
1215 and $self->can('_items_credits')
1216 and $self->can('_items_payments') )
1218 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1221 my $credittotal = 0;
1222 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1225 $total->{'total_item'} = &$escape_function($credit->{'description'});
1226 $credittotal += $credit->{'amount'};
1227 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1228 $adjusttotal += $credit->{'amount'};
1229 if ( $multisection ) {
1230 my $money = $old_latex ? '' : $money_char;
1231 push @detail_items, {
1232 ext_description => [],
1235 description => &$escape_function($credit->{'description'}),
1236 amount => $money. $credit->{'amount'},
1238 section => $adjust_section,
1241 push @total_items, $total;
1245 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1248 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1249 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1253 my $paymenttotal = 0;
1254 foreach my $payment ( $self->_items_payments ) {
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 ? time2str(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($date_format);
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 = time2str($rdate_format, $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 time2str($date_format, $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"
2461 if ( $cust_bill_pkg->setup != 0 ) {
2462 my $description = $desc;
2463 $description .= ' Setup'
2464 if $cust_bill_pkg->recur != 0
2465 || $discount_show_always
2466 || $cust_bill_pkg->recur_show_zero;
2468 'description' => $description,
2469 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2472 if ( $cust_bill_pkg->recur != 0 ) {
2474 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2475 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2479 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2481 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2484 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2486 # which pkgpart to show for display purposes?
2487 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2489 # start/end dates for invoice formats that do nonstandard
2491 my %item_dates = ();
2492 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2493 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2495 if ( (!$type || $type eq 'S')
2496 && ( $cust_bill_pkg->setup != 0
2497 || $cust_bill_pkg->setup_show_zero
2502 warn "$me _items_cust_bill_pkg adding setup\n"
2505 my $description = $desc;
2506 $description .= ' Setup'
2507 if $cust_bill_pkg->recur != 0
2508 || $discount_show_always
2509 || $cust_bill_pkg->recur_show_zero;
2513 unless ( $cust_pkg->part_pkg->hide_svc_detail
2514 || $cust_bill_pkg->hidden )
2517 my @svc_labels = map &{$escape_function}($_),
2518 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2519 push @d, @svc_labels
2520 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2521 $svc_label = $svc_labels[0];
2523 my $lnum = $cust_main ? $cust_main->ship_locationnum
2524 : $self->prospect_main->locationnum;
2525 # show the location label if it's not the customer's default
2526 # location, and we're not grouping items by location already
2527 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2528 my $loc = $cust_pkg->location_label;
2529 $loc = substr($loc, 0, $maxlength). '...'
2530 if $format eq 'latex' && length($loc) > $maxlength;
2531 push @d, &{$escape_function}($loc);
2534 } #unless hiding service details
2536 push @d, $cust_bill_pkg->details(%details_opt)
2537 if $cust_bill_pkg->recur == 0;
2539 if ( $cust_bill_pkg->hidden ) {
2540 $s->{amount} += $cust_bill_pkg->setup;
2541 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2542 push @{ $s->{ext_description} }, @d;
2546 description => $description,
2547 pkgpart => $pkgpart,
2548 pkgnum => $cust_bill_pkg->pkgnum,
2549 amount => $cust_bill_pkg->setup,
2550 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2551 unit_amount => $cust_bill_pkg->unitsetup,
2552 quantity => $cust_bill_pkg->quantity,
2553 ext_description => \@d,
2554 svc_label => ($svc_label || ''),
2560 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2562 $cust_bill_pkg->recur != 0
2563 || $cust_bill_pkg->setup == 0
2564 || $discount_show_always
2565 || $cust_bill_pkg->recur_show_zero
2570 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2573 my $is_summary = $display->summary;
2574 my $description = $desc;
2575 if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2576 $description = $self->mt('Usage charges');
2579 my $part_pkg = $cust_pkg->part_pkg;
2581 #pry be a bit more efficient to look some of this conf stuff up
2584 $conf->exists('disable_line_item_date_ranges')
2585 || $part_pkg->option('disable_line_item_date_ranges',1)
2586 || ! $cust_bill_pkg->sdate
2587 || ! $cust_bill_pkg->edate
2590 my $date_style = '';
2591 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2594 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2595 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2598 if ( defined($date_style) && $date_style eq 'month_of' ) {
2599 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2600 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2601 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2604 $desc .= ' ' unless $desc =~ /\s$/;
2605 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2607 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2608 " - ". time2str($date_format, $cust_bill_pkg->edate);
2610 $description .= " ($time_period)";
2614 my @seconds = (); # for display of usage info
2617 #at least until cust_bill_pkg has "past" ranges in addition to
2618 #the "future" sdate/edate ones... see #3032
2619 my @dates = ( $self->_date );
2620 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2621 push @dates, $prev->sdate if $prev;
2622 push @dates, undef if !$prev;
2624 unless ( $part_pkg->hide_svc_detail
2625 || $cust_bill_pkg->itemdesc
2626 || $cust_bill_pkg->hidden
2627 || $is_summary && $type && $type eq 'U'
2631 warn "$me _items_cust_bill_pkg adding service details\n"
2634 my @svc_labels = map &{$escape_function}($_),
2635 $cust_pkg->h_labels_short(@dates, 'I');
2636 push @d, @svc_labels
2637 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2638 $svc_label = $svc_labels[0];
2640 warn "$me _items_cust_bill_pkg done adding service details\n"
2643 my $lnum = $cust_main ? $cust_main->ship_locationnum
2644 : $self->prospect_main->locationnum;
2645 # show the location label if it's not the customer's default
2646 # location, and we're not grouping items by location already
2647 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2648 my $loc = $cust_pkg->location_label;
2649 $loc = substr($loc, 0, $maxlength). '...'
2650 if $format eq 'latex' && length($loc) > $maxlength;
2651 push @d, &{$escape_function}($loc);
2654 # Display of seconds_since_sqlradacct:
2655 # On the invoice, when processing @detail_items, look for a field
2656 # named 'seconds'. This will contain total seconds for each
2657 # service, in the same order as @ext_description. For services
2658 # that don't support this it will show undef.
2659 if ( $conf->exists('svc_acct-usage_seconds')
2660 and ! $cust_bill_pkg->pkgpart_override ) {
2661 foreach my $cust_svc (
2662 $cust_pkg->h_cust_svc(@dates, 'I')
2665 # eval because not having any part_export_usage exports
2666 # is a fatal error, last_bill/_date because that's how
2667 # sqlradius_hour billing does it
2669 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2671 push @seconds, $sec;
2673 } #if svc_acct-usage_seconds
2677 unless ( $is_summary ) {
2678 warn "$me _items_cust_bill_pkg adding details\n"
2681 #instead of omitting details entirely in this case (unwanted side
2682 # effects), just omit CDRs
2683 $details_opt{'no_usage'} = 1
2684 if $type && $type eq 'R';
2686 push @d, $cust_bill_pkg->details(%details_opt);
2689 warn "$me _items_cust_bill_pkg calculating amount\n"
2694 $amount = $cust_bill_pkg->recur;
2695 } elsif ($type eq 'R') {
2696 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2697 } elsif ($type eq 'U') {
2698 $amount = $cust_bill_pkg->usage;
2702 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2705 if ( !$type || $type eq 'R' ) {
2707 warn "$me _items_cust_bill_pkg adding recur\n"
2710 if ( $cust_bill_pkg->hidden ) {
2711 $r->{amount} += $amount;
2712 $r->{unit_amount} += $unit_amount;
2713 push @{ $r->{ext_description} }, @d;
2716 description => $description,
2717 pkgpart => $pkgpart,
2718 pkgnum => $cust_bill_pkg->pkgnum,
2720 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2721 unit_amount => $unit_amount,
2722 quantity => $cust_bill_pkg->quantity,
2724 ext_description => \@d,
2725 svc_label => ($svc_label || ''),
2727 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2730 } else { # $type eq 'U'
2732 warn "$me _items_cust_bill_pkg adding usage\n"
2735 if ( $cust_bill_pkg->hidden and defined($u) ) {
2736 # if this is a hidden package and there's already a usage
2737 # line for the bundle, add this package's total amount and
2738 # usage details to it
2739 $u->{amount} += $amount;
2740 $u->{unit_amount} += $unit_amount,
2741 push @{ $u->{ext_description} }, @d;
2742 } elsif ( $amount ) {
2743 # create a new usage line
2745 description => $description,
2746 pkgpart => $pkgpart,
2747 pkgnum => $cust_bill_pkg->pkgnum,
2749 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2750 unit_amount => $unit_amount,
2751 quantity => $cust_bill_pkg->quantity,
2753 ext_description => \@d,
2755 } # else this has no usage, so don't create a usage section
2758 } # recurring or usage with recurring charge
2760 } else { #pkgnum tax or one-shot line item (??)
2762 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2765 if ( $cust_bill_pkg->setup != 0 ) {
2767 'description' => $desc,
2768 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2771 if ( $cust_bill_pkg->recur != 0 ) {
2773 'description' => "$desc (".
2774 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2775 time2str($date_format, $cust_bill_pkg->edate). ')',
2776 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2784 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2785 && $conf->exists('discount-show-always'));
2789 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2791 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2792 $_->{amount} =~ s/^\-0\.00$/0.00/;
2793 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2795 if $_->{amount} != 0
2796 || $discount_show_always
2797 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2798 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2802 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2809 =item _items_discounts_avail
2811 Returns an array of line item hashrefs representing available term discounts
2812 for this invoice. This makes the same assumptions that apply to term
2813 discounts in general: that the package is billed monthly, at a flat rate,
2814 with no usage charges. A prorated first month will be handled, as will
2815 a setup fee if the discount is allowed to apply to setup fees.
2819 sub _items_discounts_avail {
2822 #maybe move this method from cust_bill when quotations support discount_plans
2823 return () unless $self->can('discount_plans');
2824 my %plans = $self->discount_plans;
2826 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2827 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2831 my $plan = $plans{$months};
2833 my $term_total = sprintf('%.2f', $plan->discounted_total);
2834 my $percent = sprintf('%.0f',
2835 100 * (1 - $term_total / $plan->base_total) );
2836 my $permonth = sprintf('%.2f', $term_total / $months);
2837 my $detail = $self->mt('discount on item'). ' '.
2838 join(', ', map { "#$_" } $plan->pkgnums)
2841 # discounts for non-integer months don't work anyway
2842 $months = sprintf("%d", $months);
2845 description => $self->mt('Save [_1]% by paying for [_2] months',
2847 amount => $self->mt('[_1] ([_2] per month)',
2848 $term_total, $money_char.$permonth),
2849 ext_description => ($detail || ''),
2852 sort { $b <=> $a } keys %plans;