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 );
19 use FS::Misc qw( generate_ps generate_pdf );
26 $me = '[FS::Template_Mixin]';
27 FS::UID->install_callback( sub {
28 my $conf = new FS::Conf; #global
29 $money_char = $conf->config('money_char') || '$';
30 $date_format = $conf->config('date_format') || '%x'; #/YY
31 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
32 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
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 %date_formats = ( 'latex' => $date_format_long,
410 'html' => $date_format_long,
413 $date_formats{'html'} =~ s/ / /g;
415 my $date_format = $date_formats{$format};
417 my %newline_tokens = ( 'latex' => '\\\\',
421 my $newline_token = $newline_tokens{$format};
423 warn "$me generating template variables\n"
426 # generate template variables
430 defined( $conf->config_orbase( "invoice_${format}returnaddress",
434 && length( $conf->config_orbase( "invoice_${format}returnaddress",
440 $returnaddress = join("\n",
441 $conf->config_orbase("invoice_${format}returnaddress", $template)
445 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
447 my $convert_map = $convert_maps{$format}{'returnaddress'};
450 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
455 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
457 my $convert_map = $convert_maps{$format}{'returnaddress'};
458 $returnaddress = join( "\n", &$convert_map(
459 map { s/( {2,})/'~' x length($1)/eg;
463 ( $conf->config('company_name', $cust_main->agentnum),
464 $conf->config('company_address', $cust_main->agentnum),
471 my $warning = "Couldn't find a return address; ".
472 "do you need to set the company_address configuration value?";
474 $returnaddress = $nbsp;
475 #$returnaddress = $warning;
479 warn "$me generating invoice data\n"
482 my $agentnum = $cust_main->agentnum;
487 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
488 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
489 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
490 'returnaddress' => $returnaddress,
491 'agent' => &$escape_function($cust_main->agent->agent),
493 #invoice/quotation info
494 'no_number' => $params{'no_number'},
495 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
496 'quotationnum' => $self->quotationnum,
497 'no_date' => $params{'no_date'},
498 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
499 'date' => ( $params{'no_date'}
501 : $self->time2str_local($date_format, $self->_date)
503 'today' => $self->time2str_local($date_format_long, $today),
504 'terms' => $self->terms,
505 'template' => $template, #params{'template'},
506 'notice_name' => $notice_name, # escape?
507 'current_charges' => sprintf("%.2f", $self->charged),
508 'duedate' => $self->due_date2str($rdate_format), #date_format?
511 'custnum' => $cust_main->display_custnum,
512 'prospectnum' => $cust_main->prospectnum,
513 'agent_custid' => &$escape_function($cust_main->agent_custid),
514 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
515 payname company address1 address2 city state zip fax
519 'ship_enable' => $conf->exists('invoice-ship_address'),
520 'unitprices' => $conf->exists('invoice-unitprice'),
521 'smallernotes' => $conf->exists('invoice-smallernotes'),
522 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
523 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
525 #layout info -- would be fancy to calc some of this and bury the template
527 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
528 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
529 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
530 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
531 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
532 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
533 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
534 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
535 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
536 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
538 # better hang on to conf_dir for a while (for old templates)
539 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
541 #these are only used when doing paged plaintext
548 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
549 # prototype here to silence warnings
550 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_) };
552 my $min_sdate = 999999999999;
554 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
555 next unless $cust_bill_pkg->pkgnum > 0;
556 $min_sdate = $cust_bill_pkg->sdate
557 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
558 $max_edate = $cust_bill_pkg->edate
559 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
562 $invoice_data{'bill_period'} = '';
563 $invoice_data{'bill_period'} = $self->time2str_local('%e %h', $min_sdate)
565 $self->time2str_local('%e %h', $max_edate)
566 if ($max_edate != 0 && $min_sdate != 999999999999);
568 $invoice_data{finance_section} = '';
569 if ( $conf->config('finance_pkgclass') ) {
571 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
572 $invoice_data{finance_section} = $pkg_class->categoryname;
574 $invoice_data{finance_amount} = '0.00';
575 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
577 my $countrydefault = $conf->config('countrydefault') || 'US';
578 foreach ( qw( address1 address2 city state zip country fax) ){
579 my $method = 'ship_'.$_;
580 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
582 if ( length($cust_main->ship_company) ) {
583 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
585 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
587 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
588 $invoice_data{'ship_country'} = ''
589 if ( $invoice_data{'ship_country'} eq $countrydefault );
591 $invoice_data{'cid'} = $params{'cid'}
594 if ( $cust_main->country eq $countrydefault ) {
595 $invoice_data{'country'} = '';
597 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
601 $invoice_data{'address'} = \@address;
604 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
605 ? " (P.O. #". $cust_main->payinfo. ")"
609 push @address, $cust_main->company
610 if $cust_main->company;
611 push @address, $cust_main->address1;
612 push @address, $cust_main->address2
613 if $cust_main->address2;
615 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
616 push @address, $invoice_data{'country'}
617 if $invoice_data{'country'};
619 while (scalar(@address) < 5);
621 $invoice_data{'logo_file'} = $params{'logo_file'}
622 if $params{'logo_file'};
623 $invoice_data{'barcode_file'} = $params{'barcode_file'}
624 if $params{'barcode_file'};
625 $invoice_data{'barcode_img'} = $params{'barcode_img'}
626 if $params{'barcode_img'};
627 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
628 if $params{'barcode_cid'};
630 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
631 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
632 #my $balance_due = $self->owed + $pr_total - $cr_total;
633 my $balance_due = $self->owed + $pr_total;
635 #these are used on the summary page only
637 # the customer's current balance as shown on the invoice before this one
638 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
640 # the change in balance from that invoice to this one
641 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
643 # the sum of amount owed on all previous invoices
644 # ($pr_total is used elsewhere but not as $previous_balance)
645 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
647 # the sum of amount owed on all invoices
648 # (this is used in the summary & on the payment coupon)
649 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
651 # info from customer's last invoice before this one, for some
653 $invoice_data{'last_bill'} = {};
655 if ( $self->custnum && $self->invnum ) {
657 if ( $self->previous_bill ) {
658 my $last_bill = $self->previous_bill;
659 $invoice_data{'last_bill'} = {
660 '_date' => $last_bill->_date, #unformatted
662 my (@payments, @credits);
663 # for formats that itemize previous payments
664 foreach my $cust_pay ( qsearch('cust_pay', {
665 'custnum' => $self->custnum,
666 '_date' => { op => '>=',
667 value => $last_bill->_date }
670 next if $cust_pay->_date > $self->_date;
672 '_date' => $cust_pay->_date,
673 'date' => time2str($date_format, $cust_pay->_date),
674 'payinfo' => $cust_pay->payby_payinfo_pretty,
675 'amount' => sprintf('%.2f', $cust_pay->paid),
677 # not concerned about applications
679 foreach my $cust_credit ( qsearch('cust_credit', {
680 'custnum' => $self->custnum,
681 '_date' => { op => '>=',
682 value => $last_bill->_date }
685 next if $cust_credit->_date > $self->_date;
687 '_date' => $cust_credit->_date,
688 'date' => time2str($date_format, $cust_credit->_date),
689 'creditreason'=> $cust_credit->reason,
690 'amount' => sprintf('%.2f', $cust_credit->amount),
693 $invoice_data{'previous_payments'} = \@payments;
694 $invoice_data{'previous_credits'} = \@credits;
699 my $summarypage = '';
700 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
703 $invoice_data{'summarypage'} = $summarypage;
705 warn "$me substituting variables in notes, footer, smallfooter\n"
708 my $tc = $self->template_conf;
709 my @include = ( [ $tc, 'notes' ],
710 [ 'invoice_', 'footer' ],
711 [ 'invoice_', 'smallfooter', ],
713 push @include, [ $tc, 'coupon', ]
714 unless $params{'no_coupon'};
716 foreach my $i (@include) {
718 my($base, $include) = @$i;
720 my $inc_file = $conf->key_orbase("$base$format$include", $template);
723 if ( $conf->exists($inc_file, $agentnum)
724 && length( $conf->config($inc_file, $agentnum) ) ) {
726 @inc_src = $conf->config($inc_file, $agentnum);
730 $inc_file = $conf->key_orbase("${base}latex$include", $template);
732 my $convert_map = $convert_maps{$format}{$include};
734 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
735 s/--\@\]/$delimiters{$format}[1]/g;
738 &$convert_map( $conf->config($inc_file, $agentnum) );
742 my $inc_tt = new Text::Template (
744 SOURCE => [ map "$_\n", @inc_src ],
745 DELIMITERS => $delimiters{$format},
746 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
748 unless ( $inc_tt->compile() ) {
749 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
750 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
754 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
756 $invoice_data{$include} =~ s/\n+$//
757 if ($format eq 'latex');
760 # let invoices use either of these as needed
761 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
762 ? $cust_main->payinfo : '';
763 $invoice_data{'po_line'} =
764 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
765 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
768 my %money_chars = ( 'latex' => '',
769 'html' => $conf->config('money_char') || '$',
772 my $money_char = $money_chars{$format};
774 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
775 'html' => $conf->config('money_char') || '$',
778 my $other_money_char = $other_money_chars{$format};
779 $invoice_data{'dollar'} = $other_money_char;
781 my %minus_signs = ( 'latex' => '$-$',
783 'template' => '- ' );
784 my $minus = $minus_signs{$format};
786 my @detail_items = ();
787 my @total_items = ();
791 $invoice_data{'detail_items'} = \@detail_items;
792 $invoice_data{'total_items'} = \@total_items;
793 $invoice_data{'buf'} = \@buf;
794 $invoice_data{'sections'} = \@sections;
796 warn "$me generating sections\n"
800 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
801 'subtotal' => $taxtotal, # adjusted below
804 my $tax_weight = _pkg_category($tax_section->{description})
805 ? _pkg_category($tax_section->{description})->weight
807 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
808 $tax_section->{'sort_weight'} = $tax_weight;
811 my $adjust_section = {
812 'description' => $self->mt('Credits, Payments, and Adjustments'),
813 'adjust_section' => 1,
814 'subtotal' => 0, # adjusted below
816 my $adjust_weight = _pkg_category($adjust_section->{description})
817 ? _pkg_category($adjust_section->{description})->weight
819 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
820 $adjust_section->{'sort_weight'} = $adjust_weight;
822 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
823 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
824 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
825 $invoice_data{'multisection'} = $multisection;
827 my $extra_sections = [];
828 my $extra_lines = ();
830 # default section ('Charges')
831 my $default_section = { 'description' => '',
836 # Previous Charges section
837 # subtotal is the first return value from $self->previous
838 my $previous_section;
839 # if the invoice has major sections, or if we're summarizing previous
840 # charges with a single line, or if we've been specifically told to put them
841 # in a section, create a section for previous charges:
842 if ( $multisection or
843 $conf->exists('previous_balance-summary_only') or
844 $conf->exists('previous_balance-section') ) {
846 $previous_section = { 'description' => $self->mt('Previous Charges'),
847 'subtotal' => $other_money_char.
848 sprintf('%.2f', $pr_total),
849 'summarized' => '', #why? $summarypage ? 'Y' : '',
851 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
852 join(' / ', map { $cust_main->balance_date_range(@$_) }
853 $self->_prior_month30s
855 if $conf->exists('invoice_include_aging');
858 # otherwise put them in the main section
859 $previous_section = $default_section;
862 if ( $multisection ) {
863 ($extra_sections, $extra_lines) =
864 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
865 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
866 && $self->can('_items_extra_usage_sections');
868 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
870 push @detail_items, @$extra_lines if $extra_lines;
872 # the code is written so that both methods can be used together, but
873 # we haven't yet changed the template to take advantage of that, so for
874 # now, treat them as mutually exclusive.
875 my %section_method = ( by_category => 1 );
876 if ( $conf->exists($tc.'sections_by_location') ) {
877 %section_method = ( by_location => 1 );
880 $self->_items_sections( 'summary' => $summarypage,
881 'escape' => $escape_function_nonbsp,
882 'extra_sections' => $extra_sections,
886 push @sections, @$early;
887 $late_sections = $late;
889 if ( $conf->exists('svc_phone_sections')
890 && $self->can('_items_svc_phone_sections')
893 my ($phone_sections, $phone_lines) =
894 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
895 push @{$late_sections}, @$phone_sections;
896 push @detail_items, @$phone_lines;
898 if ( $conf->exists('voip-cust_accountcode_cdr')
899 && $cust_main->accountcode_cdr
900 && $self->can('_items_accountcode_cdr')
903 my ($accountcode_section, $accountcode_lines) =
904 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
905 if ( scalar(@$accountcode_lines) ) {
906 push @{$late_sections}, $accountcode_section;
907 push @detail_items, @$accountcode_lines;
910 } else {# not multisection
911 # make a default section
912 push @sections, $default_section;
913 # and calculate the finance charge total, since it won't get done otherwise.
914 # and the default section total
915 # XXX possibly finance_pkgclass should not be used in this manner?
918 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
919 if ( $invoice_data{finance_section} and
920 grep { $_->section eq $invoice_data{finance_section} }
921 $cust_bill_pkg->cust_bill_pkg_display ) {
922 # I think these are always setup fees, but just to be sure...
923 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
925 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
928 $invoice_data{finance_amount} =
929 sprintf('%.2f', sum( @finance_charges ) || 0);
930 $default_section->{subtotal} = $other_money_char.
931 sprintf('%.2f', sum( @charges ) || 0);
934 # previous invoice balances in the Previous Charges section if there
935 # is one, otherwise in the main detail section
936 # (except if summary_only is enabled, don't show them at all)
937 if ( $self->can('_items_previous') &&
938 $self->enable_previous &&
939 ! $conf->exists('previous_balance-summary_only') ) {
941 warn "$me adding previous balances\n"
944 foreach my $line_item ( $self->_items_previous ) {
947 ref => $line_item->{'pkgnum'},
948 pkgpart => $line_item->{'pkgpart'},
950 section => $previous_section, # which might be $default_section
951 description => &$escape_function($line_item->{'description'}),
952 ext_description => [ map { &$escape_function($_) }
953 @{ $line_item->{'ext_description'} || [] }
955 amount => ( $old_latex ? '' : $money_char).
956 $line_item->{'amount'},
957 product_code => $line_item->{'pkgpart'} || 'N/A',
960 push @detail_items, $detail;
961 push @buf, [ $detail->{'description'},
962 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
968 if ( @pr_cust_bill && $self->enable_previous ) {
969 push @buf, ['','-----------'];
970 push @buf, [ $self->mt('Total Previous Balance'),
971 $money_char. sprintf("%10.2f", $pr_total) ];
975 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
976 warn "$me adding DID summary\n"
979 my ($didsummary,$minutes) = $self->_did_summary;
980 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
982 { 'description' => $didsummary_desc,
983 'ext_description' => [ $didsummary, $minutes ],
987 foreach my $section (@sections, @$late_sections) {
989 # begin some normalization
990 $section->{'subtotal'} = $section->{'amount'}
992 && !exists($section->{subtotal})
993 && exists($section->{amount});
995 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
996 if ( $invoice_data{finance_section} &&
997 $section->{'description'} eq $invoice_data{finance_section} );
999 $section->{'subtotal'} = $other_money_char.
1000 sprintf('%.2f', $section->{'subtotal'})
1003 # continue some normalization
1004 $section->{'amount'} = $section->{'subtotal'}
1008 if ( $section->{'description'} ) {
1009 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1014 warn "$me setting options\n"
1018 $options{'section'} = $section if $multisection;
1019 $options{'format'} = $format;
1020 $options{'escape_function'} = $escape_function;
1021 $options{'no_usage'} = 1 unless $unsquelched;
1022 $options{'unsquelched'} = $unsquelched;
1023 $options{'summary_page'} = $summarypage;
1024 $options{'skip_usage'} =
1025 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1027 warn "$me searching for line items\n"
1030 foreach my $line_item ( $self->_items_pkg(%options) ) {
1032 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'};
1059 push @detail_items, $detail;
1060 push @buf, ( [ $detail->{'description'},
1061 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1063 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1067 if ( $section->{'description'} ) {
1068 push @buf, ( ['','-----------'],
1069 [ $section->{'description'}. ' sub-total',
1070 $section->{'subtotal'} # already formatted this
1079 $invoice_data{current_less_finance} =
1080 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1082 # if there's anything in the Previous Charges section, prepend it to the list
1083 if ( $pr_total and $previous_section ne $default_section ) {
1084 unshift @sections, $previous_section;
1087 warn "$me adding taxes\n"
1090 my @items_tax = $self->_items_tax;
1091 foreach my $tax ( @items_tax ) {
1093 $taxtotal += $tax->{'amount'};
1095 my $description = &$escape_function( $tax->{'description'} );
1096 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1098 if ( $multisection ) {
1100 my $money = $old_latex ? '' : $money_char;
1101 push @detail_items, {
1102 ext_description => [],
1105 description => $description,
1106 amount => $money. $amount,
1108 section => $tax_section,
1113 push @total_items, {
1114 'total_item' => $description,
1115 'total_amount' => $other_money_char. $amount,
1120 push @buf,[ $description,
1121 $money_char. $amount,
1128 $total->{'total_item'} = $self->mt('Sub-total');
1129 $total->{'total_amount'} =
1130 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1132 if ( $multisection ) {
1133 $tax_section->{'subtotal'} = $other_money_char.
1134 sprintf('%.2f', $taxtotal);
1135 $tax_section->{'pretotal'} = 'New charges sub-total '.
1136 $total->{'total_amount'};
1137 push @sections, $tax_section if $taxtotal;
1139 unshift @total_items, $total;
1142 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1144 push @buf,['','-----------'];
1145 push @buf,[$self->mt(
1146 (!$self->enable_previous)
1148 : 'Total New Charges'
1150 $money_char. sprintf("%10.2f",$self->charged) ];
1158 my %embolden_functions = (
1159 'latex' => sub { return '\textbf{'. shift(). '}' },
1160 'html' => sub { return '<b>'. shift(). '</b>' },
1161 'template' => sub { shift },
1163 my $embolden_function = $embolden_functions{$format};
1165 if ( $self->can('_items_total') ) { # quotations
1167 $self->_items_total(\@total_items);
1169 foreach ( @total_items ) {
1170 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1171 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1172 $_->{'total_amount'}
1176 } else { #normal invoice case
1178 # calculate total, possibly including total owed on previous
1182 $item = $conf->config('previous_balance-exclude_from_total')
1183 || 'Total New Charges'
1184 if $conf->exists('previous_balance-exclude_from_total');
1185 my $amount = $self->charged;
1186 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1187 $amount += $pr_total;
1190 $total->{'total_item'} = &$embolden_function($self->mt($item));
1191 $total->{'total_amount'} =
1192 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1193 if ( $multisection ) {
1194 if ( $adjust_section->{'sort_weight'} ) {
1195 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1196 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1198 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1199 $other_money_char. sprintf('%.2f', $self->charged );
1202 push @total_items, $total;
1204 push @buf,['','-----------'];
1207 sprintf( '%10.2f', $amount )
1211 # if we're showing previous invoices, also show previous
1212 # credits and payments
1213 if ( $self->enable_previous
1214 and $self->can('_items_credits')
1215 and $self->can('_items_payments') )
1217 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1220 my $credittotal = 0;
1221 foreach my $credit (
1222 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1226 $total->{'total_item'} = &$escape_function($credit->{'description'});
1227 $credittotal += $credit->{'amount'};
1228 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1229 $adjusttotal += $credit->{'amount'};
1230 if ( $multisection ) {
1231 my $money = $old_latex ? '' : $money_char;
1232 push @detail_items, {
1233 ext_description => [],
1236 description => &$escape_function($credit->{'description'}),
1237 amount => $money. $credit->{'amount'},
1239 section => $adjust_section,
1242 push @total_items, $total;
1246 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1249 foreach my $credit (
1250 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1252 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1256 my $paymenttotal = 0;
1257 foreach my $payment (
1258 $self->_items_payments( 'template' => $template )
1261 $total->{'total_item'} = &$escape_function($payment->{'description'});
1262 $paymenttotal += $payment->{'amount'};
1263 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1264 $adjusttotal += $payment->{'amount'};
1265 if ( $multisection ) {
1266 my $money = $old_latex ? '' : $money_char;
1267 push @detail_items, {
1268 ext_description => [],
1271 description => &$escape_function($payment->{'description'}),
1272 amount => $money. $payment->{'amount'},
1274 section => $adjust_section,
1277 push @total_items, $total;
1279 push @buf, [ $payment->{'description'},
1280 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1283 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1285 if ( $multisection ) {
1286 $adjust_section->{'subtotal'} = $other_money_char.
1287 sprintf('%.2f', $adjusttotal);
1288 push @sections, $adjust_section
1289 unless $adjust_section->{sort_weight};
1292 # create Balance Due message
1295 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1296 $total->{'total_amount'} =
1297 &$embolden_function(
1298 $other_money_char. sprintf('%.2f', #why? $summarypage
1299 # ? $self->charged +
1300 # $self->billing_balance
1302 $self->owed + $pr_total
1305 if ( $multisection && !$adjust_section->{sort_weight} ) {
1306 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1307 $total->{'total_amount'};
1309 push @total_items, $total;
1311 push @buf,['','-----------'];
1312 push @buf,[$self->balance_due_msg, $money_char.
1313 sprintf("%10.2f", $balance_due ) ];
1316 if ( $conf->exists('previous_balance-show_credit')
1317 and $cust_main->balance < 0 ) {
1318 my $credit_total = {
1319 'total_item' => &$embolden_function($self->credit_balance_msg),
1320 'total_amount' => &$embolden_function(
1321 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1324 if ( $multisection ) {
1325 $adjust_section->{'posttotal'} .= $newline_token .
1326 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1329 push @total_items, $credit_total;
1331 push @buf,['','-----------'];
1332 push @buf,[$self->credit_balance_msg, $money_char.
1333 sprintf("%10.2f", -$cust_main->balance ) ];
1337 } #end of default total adding ! can('_items_total')
1339 if ( $multisection ) {
1340 if ( $conf->exists('svc_phone_sections')
1341 && $self->can('_items_svc_phone_sections')
1345 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1346 $total->{'total_amount'} =
1347 &$embolden_function(
1348 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1350 my $last_section = pop @sections;
1351 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1352 $total->{'total_amount'};
1353 push @sections, $last_section;
1355 push @sections, @$late_sections
1359 # make a discounts-available section, even without multisection
1360 if ( $conf->exists('discount-show_available')
1361 and my @discounts_avail = $self->_items_discounts_avail ) {
1362 my $discount_section = {
1363 'description' => $self->mt('Discounts Available'),
1368 push @sections, $discount_section;
1369 push @detail_items, map { +{
1370 'ref' => '', #should this be something else?
1371 'section' => $discount_section,
1372 'description' => &$escape_function( $_->{description} ),
1373 'amount' => $money_char . &$escape_function( $_->{amount} ),
1374 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1375 } } @discounts_avail;
1378 my @summary_subtotals;
1379 # the templates say "$_->{tax_section} || !$_->{summarized}"
1380 # except 'summarized' is only true when tax_section is true, so this
1381 # is always true, so what's the deal?
1382 foreach my $s (@sections) {
1383 # not to include in the "summary of new charges" block:
1384 # finance charges, adjustments, previous charges,
1385 # and itemized phone usage sections
1386 if ( $s eq $adjust_section or
1387 ($s eq $previous_section and $s ne $default_section) or
1388 ($invoice_data{'finance_section'} and
1389 $invoice_data{'finance_section'} eq $s->{description}) or
1390 $s->{'description'} =~ /^\d+ $/ ) {
1393 push @summary_subtotals, $s;
1395 $invoice_data{summary_subtotals} = \@summary_subtotals;
1397 # debugging hook: call this with 'diag' => 1 to just get a hash of
1398 # the invoice variables
1399 return \%invoice_data if ( $params{'diag'} );
1401 # All sections and items are built; now fill in templates.
1402 my @includelist = ();
1403 push @includelist, 'summary' if $summarypage;
1404 foreach my $include ( @includelist ) {
1406 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1409 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1411 @inc_src = $conf->config($inc_file, $agentnum);
1415 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1417 my $convert_map = $convert_maps{$format}{$include};
1419 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1420 s/--\@\]/$delimiters{$format}[1]/g;
1423 &$convert_map( $conf->config($inc_file, $agentnum) );
1427 my $inc_tt = new Text::Template (
1429 SOURCE => [ map "$_\n", @inc_src ],
1430 DELIMITERS => $delimiters{$format},
1431 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1433 unless ( $inc_tt->compile() ) {
1434 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1435 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1439 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1441 $invoice_data{$include} =~ s/\n+$//
1442 if ($format eq 'latex');
1447 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1448 /invoice_lines\((\d*)\)/;
1449 $invoice_lines += $1 || scalar(@buf);
1452 die "no invoice_lines() functions in template?"
1453 if ( $format eq 'template' && !$wasfunc );
1455 if ($format eq 'template') {
1457 if ( $invoice_lines ) {
1458 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1459 $invoice_data{'total_pages'}++
1460 if scalar(@buf) % $invoice_lines;
1463 #setup subroutine for the template
1464 $invoice_data{invoice_lines} = sub {
1465 my $lines = shift || scalar(@buf);
1477 push @collect, split("\n",
1478 $text_template->fill_in( HASH => \%invoice_data )
1480 $invoice_data{'page'}++;
1482 map "$_\n", @collect;
1484 } else { # this is where we actually create the invoice
1486 warn "filling in template for invoice ". $self->invnum. "\n"
1488 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1491 $text_template->fill_in(HASH => \%invoice_data);
1495 sub notice_name { '('.shift->table.')'; }
1497 sub template_conf { 'invoice_'; }
1499 # helper routine for generating date ranges
1500 sub _prior_month30s {
1503 [ 1, 2592000 ], # 0-30 days ago
1504 [ 2592000, 5184000 ], # 30-60 days ago
1505 [ 5184000, 7776000 ], # 60-90 days ago
1506 [ 7776000, 0 ], # 90+ days ago
1509 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1510 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1515 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1517 Returns an postscript invoice, as a scalar.
1519 Options can be passed as a hashref (recommended) or as a list of time, template
1520 and then any key/value pairs for any other options.
1522 I<time> an optional value used to control the printing of overdue messages. The
1523 default is now. It isn't the date of the invoice; that's the `_date' field.
1524 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1525 L<Time::Local> and L<Date::Parse> for conversion functions.
1527 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1534 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1535 my $ps = generate_ps($file);
1537 unlink($barcodefile) if $barcodefile;
1542 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1544 Returns an PDF invoice, as a scalar.
1546 Options can be passed as a hashref (recommended) or as a list of time, template
1547 and then any key/value pairs for any other options.
1549 I<time> an optional value used to control the printing of overdue messages. The
1550 default is now. It isn't the date of the invoice; that's the `_date' field.
1551 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1552 L<Time::Local> and L<Date::Parse> for conversion functions.
1554 I<template>, if specified, is the name of a suffix for alternate invoices.
1556 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1563 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1564 my $pdf = generate_pdf($file);
1566 unlink($barcodefile) if $barcodefile;
1571 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1573 Returns an HTML invoice, as a scalar.
1575 I<time> an optional value used to control the printing of overdue messages. The
1576 default is now. It isn't the date of the invoice; that's the `_date' field.
1577 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1578 L<Time::Local> and L<Date::Parse> for conversion functions.
1580 I<template>, if specified, is the name of a suffix for alternate invoices.
1582 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1584 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1585 when emailing the invoice as part of a multipart/related MIME email.
1593 %params = %{ shift() };
1597 $params{'format'} = 'html';
1599 $self->print_generic( %params );
1602 # quick subroutine for print_latex
1604 # There are ten characters that LaTeX treats as special characters, which
1605 # means that they do not simply typeset themselves:
1606 # # $ % & ~ _ ^ \ { }
1608 # TeX ignores blanks following an escaped character; if you want a blank (as
1609 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1613 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1614 $value =~ s/([<>])/\$$1\$/g;
1620 encode_entities($value);
1624 sub _html_escape_nbsp {
1625 my $value = _html_escape(shift);
1626 $value =~ s/ +/ /g;
1630 #utility methods for print_*
1632 sub _translate_old_latex_format {
1633 warn "_translate_old_latex_format called\n"
1640 if ( $line =~ /^%%Detail\s*$/ ) {
1642 push @template, q![@--!,
1643 q! foreach my $_tr_line (@detail_items) {!,
1644 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1645 q! $_tr_line->{'description'} .= !,
1646 q! "\\tabularnewline\n~~".!,
1647 q! join( "\\tabularnewline\n~~",!,
1648 q! @{$_tr_line->{'ext_description'}}!,
1652 while ( ( my $line_item_line = shift )
1653 !~ /^%%EndDetail\s*$/ ) {
1654 $line_item_line =~ s/'/\\'/g; # nice LTS
1655 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1656 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1657 push @template, " \$OUT .= '$line_item_line';";
1660 push @template, '}',
1663 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1665 push @template, '[@--',
1666 ' foreach my $_tr_line (@total_items) {';
1668 while ( ( my $total_item_line = shift )
1669 !~ /^%%EndTotalDetails\s*$/ ) {
1670 $total_item_line =~ s/'/\\'/g; # nice LTS
1671 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1672 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1673 push @template, " \$OUT .= '$total_item_line';";
1676 push @template, '}',
1680 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1681 push @template, $line;
1687 warn "$_\n" foreach @template;
1695 my $conf = $self->conf;
1697 #check for an invoice-specific override
1698 return $self->invoice_terms if $self->invoice_terms;
1700 #check for a customer- specific override
1701 my $cust_main = $self->cust_main;
1702 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1704 #use configured default
1705 $conf->config('invoice_default_terms') || '';
1711 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1712 $duedate = $self->_date() + ( $1 * 86400 );
1719 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1722 sub balance_due_msg {
1724 my $msg = $self->mt('Balance Due');
1725 return $msg unless $self->terms;
1726 if ( $self->due_date ) {
1727 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1728 $self->due_date2str($date_format);
1729 } elsif ( $self->terms ) {
1730 $msg .= ' - '. $self->terms;
1735 sub balance_due_date {
1737 my $conf = $self->conf;
1739 if ( $conf->exists('invoice_default_terms')
1740 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1741 $duedate = $self->time2str_local($rdate_format, $self->_date + ($1*86400) );
1746 sub credit_balance_msg {
1748 $self->mt('Credit Balance Remaining')
1753 Returns a string with the date, for example: "3/20/2008"
1759 $self->time2str_local($date_format, $self->_date);
1762 =item _items_sections OPTIONS
1764 Generate section information for all items appearing on this invoice.
1765 This will only be called for multi-section invoices.
1767 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1768 related display records (L<FS::cust_bill_pkg_display>) and organize
1769 them into two groups ("early" and "late" according to whether they come
1770 before or after the total), then into sections. A subtotal is calculated
1773 Section descriptions are returned in sort weight order. Each consists
1774 of a hash containing:
1776 description: the package category name, escaped
1777 subtotal: the total charges in that section
1778 tax_section: a flag indicating that the section contains only tax charges
1779 summarized: same as tax_section, for some reason
1780 sort_weight: the package category's sort weight
1782 If 'condense' is set on the display record, it also contains everything
1783 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1784 coderefs to generate parts of the invoice. This is not advised.
1786 The method returns two arrayrefs, one of "early" sections and one of "late"
1789 OPTIONS may include:
1791 by_location: a flag to divide the invoice into sections by location.
1792 Each section hash will have a 'location' element containing a hashref of
1793 the location fields (see L<FS::cust_location>). The section description
1794 will be the location label, but the template can use any of the location
1795 fields to create a suitable label.
1797 by_category: a flag to divide the invoice into sections using display
1798 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1799 behavior. Each section hash will have a 'category' element containing
1800 the section name from the display record (which probably equals the
1801 category name of the package, but may not in some cases).
1803 summary: a flag indicating that this is a summary-format invoice.
1804 Turning this on has the following effects:
1805 - Ignores display items with the 'summary' flag.
1806 - Places all sections in the "early" group even if they have post_total.
1807 - Creates sections for all non-disabled package categories, even if they
1808 have no charges on this invoice, as well as a section with no name.
1810 escape: an escape function to use for section titles.
1812 extra_sections: an arrayref of additional sections to return after the
1813 sorted list. If there are any of these, section subtotals exclude
1816 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1817 passed through to C<_condense_section()>.
1821 use vars qw(%pkg_category_cache);
1822 sub _items_sections {
1826 my $escape = $opt{escape};
1827 my @extra_sections = @{ $opt{extra_sections} || [] };
1829 # $subtotal{$locationnum}{$categoryname} = amount.
1830 # if we're not using by_location, $locationnum is undef.
1831 # if we're not using by_category, you guessed it, $categoryname is undef.
1832 # if we're not using either one, we shouldn't be here in the first place...
1834 my %late_subtotal = ();
1837 # About tax items + multisection invoices:
1838 # If either invoice_*summary option is enabled, AND there is a
1839 # package category with the name of the tax, then there will be
1840 # a display record assigning the tax item to that category.
1842 # However, the taxes are always placed in the "Taxes, Surcharges,
1843 # and Fees" section regardless of that. The only effect of the
1844 # display record is to create a subtotal for the summary page.
1847 my $pkg_hash = $self->cust_pkg_hash;
1849 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1852 my $usage = $cust_bill_pkg->usage;
1855 if ( $opt{by_location} ) {
1856 if ( $cust_bill_pkg->pkgnum ) {
1857 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1862 $locationnum = undef;
1865 # as in _items_cust_pkg, if a line item has no display records,
1866 # cust_bill_pkg_display() returns a default record for it
1868 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1869 next if ( $display->summary && $opt{summary} );
1871 my $section = $display->section;
1872 my $type = $display->type;
1873 $section = undef unless $opt{by_category};
1875 $not_tax{$locationnum}{$section} = 1
1876 unless $cust_bill_pkg->pkgnum == 0;
1878 # there's actually a very important piece of logic buried in here:
1879 # incrementing $late_subtotal{$section} CREATES
1880 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1881 # to define the list of late sections, and likewise keys(%subtotal).
1882 # When _items_cust_bill_pkg is called to generate line items for
1883 # real, it will be called with 'section' => $section for each
1885 if ( $display->post_total && !$opt{summary} ) {
1886 if (! $type || $type eq 'S') {
1887 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1888 if $cust_bill_pkg->setup != 0
1889 || $cust_bill_pkg->setup_show_zero;
1893 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1894 if $cust_bill_pkg->recur != 0
1895 || $cust_bill_pkg->recur_show_zero;
1898 if ($type && $type eq 'R') {
1899 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1900 if $cust_bill_pkg->recur != 0
1901 || $cust_bill_pkg->recur_show_zero;
1904 if ($type && $type eq 'U') {
1905 $late_subtotal{$locationnum}{$section} += $usage
1906 unless scalar(@extra_sections);
1909 } else { # it's a pre-total (normal) section
1911 # skip tax items unless they're explicitly included in a section
1912 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1914 if (! $type || $type eq 'S') {
1915 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1916 if $cust_bill_pkg->setup != 0
1917 || $cust_bill_pkg->setup_show_zero;
1921 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1922 if $cust_bill_pkg->recur != 0
1923 || $cust_bill_pkg->recur_show_zero;
1926 if ($type && $type eq 'R') {
1927 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1928 if $cust_bill_pkg->recur != 0
1929 || $cust_bill_pkg->recur_show_zero;
1932 if ($type && $type eq 'U') {
1933 $subtotal{$locationnum}{$section} += $usage
1934 unless scalar(@extra_sections);
1943 %pkg_category_cache = ();
1945 # summary invoices need subtotals for all non-disabled package categories,
1946 # even if they're zero
1947 # but currently assume that there are no location sections, or at least
1948 # that the summary page doesn't care about them
1949 if ( $opt{summary} ) {
1950 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1951 $subtotal{''}{$category->categoryname} ||= 0;
1953 $subtotal{''}{''} ||= 0;
1957 foreach my $post_total (0,1) {
1959 my $s = $post_total ? \%late_subtotal : \%subtotal;
1960 foreach my $locationnum (keys %$s) {
1961 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1963 'subtotal' => $s->{$locationnum}{$sectionname},
1964 'post_total' => $post_total,
1967 if ( $locationnum ) {
1968 $section->{'locationnum'} = $locationnum;
1969 my $location = FS::cust_location->by_key($locationnum);
1970 $section->{'description'} = &{ $escape }($location->location_label);
1971 # Better ideas? This will roughly group them by proximity,
1972 # which alpha sorting on any of the address fields won't.
1973 # Sorting by locationnum is meaningless.
1974 # We have to sort on _something_ or the order may change
1975 # randomly from one invoice to the next, which will confuse
1977 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1979 $section->{'location'} = {
1980 map { $_ => &{ $escape }($location->get($_)) }
1984 $section->{'category'} = $sectionname;
1985 $section->{'description'} = &{ $escape }($sectionname);
1986 if ( _pkg_category($_) ) {
1987 $section->{'sort_weight'} = _pkg_category($_)->weight;
1988 if ( _pkg_category($_)->condense ) {
1989 $section = { %$section, $self->_condense_section($opt{format}) };
1993 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1994 # then it's a tax-only section
1995 $section->{'summarized'} = 'Y';
1996 $section->{'tax_section'} = 'Y';
1998 push @these, $section;
1999 } # foreach $sectionname
2000 } #foreach $locationnum
2001 push @these, @extra_sections if $post_total == 0;
2002 # need an alpha sort for location sections, because postal codes can
2004 $sections[ $post_total ] = [ sort {
2005 $opt{'by_location'} ?
2006 ($a->{sort_weight} cmp $b->{sort_weight}) :
2007 ($a->{sort_weight} <=> $b->{sort_weight})
2009 } #foreach $post_total
2011 return @sections; # early, late
2014 #helper subs for above
2018 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2022 my $categoryname = shift;
2023 $pkg_category_cache{$categoryname} ||=
2024 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2027 my %condensed_format = (
2028 'label' => [ qw( Description Qty Amount ) ],
2030 sub { shift->{description} },
2031 sub { shift->{quantity} },
2032 sub { my($href, %opt) = @_;
2033 ($opt{dollar} || ''). $href->{amount};
2036 'align' => [ qw( l r r ) ],
2037 'span' => [ qw( 5 1 1 ) ], # unitprices?
2038 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2041 sub _condense_section {
2042 my ( $self, $format ) = ( shift, shift );
2044 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2045 qw( description_generator
2048 total_line_generator
2053 sub _condensed_generator_defaults {
2054 my ( $self, $format ) = ( shift, shift );
2055 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2064 sub _condensed_header_generator {
2065 my ( $self, $format ) = ( shift, shift );
2067 my ( $f, $prefix, $suffix, $separator, $column ) =
2068 _condensed_generator_defaults($format);
2070 if ($format eq 'latex') {
2071 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2072 $suffix = "\\\\\n\\hline";
2075 sub { my ($d,$a,$s,$w) = @_;
2076 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2078 } elsif ( $format eq 'html' ) {
2079 $prefix = '<th></th>';
2083 sub { my ($d,$a,$s,$w) = @_;
2084 return qq!<th align="$html_align{$a}">$d</th>!;
2092 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2094 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2097 $prefix. join($separator, @result). $suffix;
2102 sub _condensed_description_generator {
2103 my ( $self, $format ) = ( shift, shift );
2105 my ( $f, $prefix, $suffix, $separator, $column ) =
2106 _condensed_generator_defaults($format);
2108 my $money_char = '$';
2109 if ($format eq 'latex') {
2110 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2112 $separator = " & \n";
2114 sub { my ($d,$a,$s,$w) = @_;
2115 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2117 $money_char = '\\dollar';
2118 }elsif ( $format eq 'html' ) {
2119 $prefix = '"><td align="center"></td>';
2123 sub { my ($d,$a,$s,$w) = @_;
2124 return qq!<td align="$html_align{$a}">$d</td>!;
2126 #$money_char = $conf->config('money_char') || '$';
2127 $money_char = ''; # this is madness
2135 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2137 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2139 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2140 map { $f->{$_}->[$i] } qw(align span width)
2144 $prefix. join( $separator, @result ). $suffix;
2149 sub _condensed_total_generator {
2150 my ( $self, $format ) = ( shift, shift );
2152 my ( $f, $prefix, $suffix, $separator, $column ) =
2153 _condensed_generator_defaults($format);
2156 if ($format eq 'latex') {
2159 $separator = " & \n";
2161 sub { my ($d,$a,$s,$w) = @_;
2162 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2164 }elsif ( $format eq 'html' ) {
2168 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2170 sub { my ($d,$a,$s,$w) = @_;
2171 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2180 # my $r = &{$f->{fields}->[$i]}(@args);
2181 # $r .= ' Total' unless $i;
2183 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2185 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2186 map { $f->{$_}->[$i] } qw(align span width)
2190 $prefix. join( $separator, @result ). $suffix;
2195 =item total_line_generator FORMAT
2197 Returns a coderef used for generation of invoice total line items for this
2198 usage_class. FORMAT is either html or latex
2202 # should not be used: will have issues with hash element names (description vs
2203 # total_item and amount vs total_amount -- another array of functions?
2205 sub _condensed_total_line_generator {
2206 my ( $self, $format ) = ( shift, shift );
2208 my ( $f, $prefix, $suffix, $separator, $column ) =
2209 _condensed_generator_defaults($format);
2212 if ($format eq 'latex') {
2215 $separator = " & \n";
2217 sub { my ($d,$a,$s,$w) = @_;
2218 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2220 }elsif ( $format eq 'html' ) {
2224 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2226 sub { my ($d,$a,$s,$w) = @_;
2227 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2236 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2238 &{$column}( &{$f->{fields}->[$i]}(@args),
2239 map { $f->{$_}->[$i] } qw(align span width)
2243 $prefix. join( $separator, @result ). $suffix;
2248 =item _items_pkg [ OPTIONS ]
2250 Return line item hashes for each package item on this invoice. Nearly
2253 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2255 The only OPTIONS accepted is 'section', which may point to a hashref
2256 with a key named 'condensed', which may have a true value. If it
2257 does, this method tries to merge identical items into items with
2258 'quantity' equal to the number of items (not the sum of their
2259 separate quantities, for some reason).
2265 grep { $_->pkgnum } $self->cust_bill_pkg;
2272 warn "$me _items_pkg searching for all package line items\n"
2275 my @cust_bill_pkg = $self->_items_nontax;
2277 warn "$me _items_pkg filtering line items\n"
2279 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2281 if ($options{section} && $options{section}->{condensed}) {
2283 warn "$me _items_pkg condensing section\n"
2287 local $Storable::canonical = 1;
2288 foreach ( @items ) {
2290 delete $item->{ref};
2291 delete $item->{ext_description};
2292 my $key = freeze($item);
2293 $itemshash{$key} ||= 0;
2294 $itemshash{$key} ++; # += $item->{quantity};
2296 @items = sort { $a->{description} cmp $b->{description} }
2297 map { my $i = thaw($_);
2298 $i->{quantity} = $itemshash{$_};
2300 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2306 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2313 return 0 unless $a->itemdesc cmp $b->itemdesc;
2314 return -1 if $b->itemdesc eq 'Tax';
2315 return 1 if $a->itemdesc eq 'Tax';
2316 return -1 if $b->itemdesc eq 'Other surcharges';
2317 return 1 if $a->itemdesc eq 'Other surcharges';
2318 $a->itemdesc cmp $b->itemdesc;
2323 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2324 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2326 if ( $self->conf->exists('always_show_tax') ) {
2327 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2328 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2330 { 'description' => $itemdesc,
2337 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2339 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2340 list of hashrefs describing the line items they generate on the invoice.
2342 OPTIONS may include:
2344 format: the invoice format.
2346 escape_function: the function used to escape strings.
2348 DEPRECATED? (expensive, mostly unused?)
2349 format_function: the function used to format CDRs.
2351 section: a hashref containing 'category' and/or 'locationnum'; if this
2352 is present, only returns line items that belong to that category and/or
2353 location (whichever is defined).
2355 multisection: a flag indicating that this is a multisection invoice,
2356 which does something complicated.
2358 Returns a list of hashrefs, each of which may contain:
2360 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2361 ext_description, which is an arrayref of detail lines to show below
2366 sub _items_cust_bill_pkg {
2368 my $conf = $self->conf;
2369 my $cust_bill_pkgs = shift;
2372 my $format = $opt{format} || '';
2373 my $escape_function = $opt{escape_function} || sub { shift };
2374 my $format_function = $opt{format_function} || '';
2375 my $no_usage = $opt{no_usage} || '';
2376 my $unsquelched = $opt{unsquelched} || ''; #unused
2377 my ($section, $locationnum, $category);
2378 if ( $opt{section} ) {
2379 $category = $opt{section}->{category};
2380 $locationnum = $opt{section}->{locationnum};
2382 my $summary_page = $opt{summary_page} || ''; #unused
2383 my $multisection = defined($category) || defined($locationnum);
2384 my $discount_show_always = 0;
2386 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2388 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2389 # and location labels
2392 my ($s, $r, $u) = ( undef, undef, undef );
2393 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2396 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2397 if ( $_ && !$cust_bill_pkg->hidden ) {
2398 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2399 $_->{amount} =~ s/^\-0\.00$/0.00/;
2400 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2402 if $_->{amount} != 0
2403 || $discount_show_always
2404 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2405 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2411 if ( $locationnum ) {
2412 # this is a location section; skip packages that aren't at this
2414 next if $cust_bill_pkg->pkgnum == 0;
2415 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2419 # Consider display records for this item to determine if it belongs
2420 # in this section. Note that if there are no display records, there
2421 # will be a default pseudo-record that includes all charge types
2422 # and has no section name.
2423 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2424 ? $cust_bill_pkg->cust_bill_pkg_display
2425 : ( $cust_bill_pkg );
2427 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2428 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2431 if ( defined($category) ) {
2432 # then this is a package category section; process all display records
2433 # that belong to this section.
2434 @cust_bill_pkg_display = grep { $_->section eq $category }
2435 @cust_bill_pkg_display;
2437 # otherwise, process all display records that aren't usage summaries
2438 # (I don't think there should be usage summaries if you aren't using
2439 # category sections, but this is the historical behavior)
2440 @cust_bill_pkg_display = grep { !$_->summary }
2441 @cust_bill_pkg_display;
2443 foreach my $display (@cust_bill_pkg_display) {
2445 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2446 $display->billpkgdisplaynum. "\n"
2449 my $type = $display->type;
2451 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2452 $desc = substr($desc, 0, $maxlength). '...'
2453 if $format eq 'latex' && length($desc) > $maxlength;
2455 my %details_opt = ( 'format' => $format,
2456 'escape_function' => $escape_function,
2457 'format_function' => $format_function,
2458 'no_usage' => $opt{'no_usage'},
2461 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2463 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2466 if ( $cust_bill_pkg->setup != 0 ) {
2467 my $description = $desc;
2468 $description .= ' Setup'
2469 if $cust_bill_pkg->recur != 0
2470 || $discount_show_always
2471 || $cust_bill_pkg->recur_show_zero;
2473 'description' => $description,
2474 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2477 if ( $cust_bill_pkg->recur != 0 ) {
2479 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2480 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2484 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2486 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2489 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2490 my $part_pkg = $cust_pkg->part_pkg;
2492 # which pkgpart to show for display purposes?
2493 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2495 # start/end dates for invoice formats that do nonstandard
2497 my %item_dates = ();
2498 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2499 unless $part_pkg->option('disable_line_item_date_ranges',1);
2501 if ( (!$type || $type eq 'S')
2502 && ( $cust_bill_pkg->setup != 0
2503 || $cust_bill_pkg->setup_show_zero
2508 warn "$me _items_cust_bill_pkg adding setup\n"
2511 my $description = $desc;
2512 $description .= ' Setup'
2513 if $cust_bill_pkg->recur != 0
2514 || $discount_show_always
2515 || $cust_bill_pkg->recur_show_zero;
2517 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2519 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2520 # triggered by the recurring charge freq
2522 && $cust_bill_pkg->recur == 0
2523 && ! $cust_bill_pkg->recur_show_zero;
2527 unless ( $cust_pkg->part_pkg->hide_svc_detail
2528 || $cust_bill_pkg->hidden )
2531 my @svc_labels = map &{$escape_function}($_),
2532 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2533 push @d, @svc_labels
2534 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2535 $svc_label = $svc_labels[0];
2537 my $lnum = $cust_main ? $cust_main->ship_locationnum
2538 : $self->prospect_main->locationnum;
2539 # show the location label if it's not the customer's default
2540 # location, and we're not grouping items by location already
2541 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2542 my $loc = $cust_pkg->location_label;
2543 $loc = substr($loc, 0, $maxlength). '...'
2544 if $format eq 'latex' && length($loc) > $maxlength;
2545 push @d, &{$escape_function}($loc);
2548 } #unless hiding service details
2550 push @d, $cust_bill_pkg->details(%details_opt)
2551 if $cust_bill_pkg->recur == 0;
2553 if ( $cust_bill_pkg->hidden ) {
2554 $s->{amount} += $cust_bill_pkg->setup;
2555 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2556 push @{ $s->{ext_description} }, @d;
2560 description => $description,
2561 pkgpart => $pkgpart,
2562 pkgnum => $cust_bill_pkg->pkgnum,
2563 amount => $cust_bill_pkg->setup,
2564 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2565 unit_amount => $cust_bill_pkg->unitsetup,
2566 quantity => $cust_bill_pkg->quantity,
2567 ext_description => \@d,
2568 svc_label => ($svc_label || ''),
2574 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2576 $cust_bill_pkg->recur != 0
2577 || $cust_bill_pkg->setup == 0
2578 || $discount_show_always
2579 || $cust_bill_pkg->recur_show_zero
2584 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2587 my $is_summary = $display->summary;
2588 my $description = $desc;
2589 if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2590 $description = $self->mt('Usage charges');
2593 my $part_pkg = $cust_pkg->part_pkg;
2595 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2599 my @seconds = (); # for display of usage info
2602 #at least until cust_bill_pkg has "past" ranges in addition to
2603 #the "future" sdate/edate ones... see #3032
2604 my @dates = ( $self->_date );
2605 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2606 push @dates, $prev->sdate if $prev;
2607 push @dates, undef if !$prev;
2609 unless ( $part_pkg->hide_svc_detail
2610 || $cust_bill_pkg->itemdesc
2611 || $cust_bill_pkg->hidden
2612 || $is_summary && $type && $type eq 'U'
2616 warn "$me _items_cust_bill_pkg adding service details\n"
2619 my @svc_labels = map &{$escape_function}($_),
2620 $cust_pkg->h_labels_short(@dates, 'I');
2621 push @d, @svc_labels
2622 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2623 $svc_label = $svc_labels[0];
2625 warn "$me _items_cust_bill_pkg done adding service details\n"
2628 my $lnum = $cust_main ? $cust_main->ship_locationnum
2629 : $self->prospect_main->locationnum;
2630 # show the location label if it's not the customer's default
2631 # location, and we're not grouping items by location already
2632 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2633 my $loc = $cust_pkg->location_label;
2634 $loc = substr($loc, 0, $maxlength). '...'
2635 if $format eq 'latex' && length($loc) > $maxlength;
2636 push @d, &{$escape_function}($loc);
2639 # Display of seconds_since_sqlradacct:
2640 # On the invoice, when processing @detail_items, look for a field
2641 # named 'seconds'. This will contain total seconds for each
2642 # service, in the same order as @ext_description. For services
2643 # that don't support this it will show undef.
2644 if ( $conf->exists('svc_acct-usage_seconds')
2645 and ! $cust_bill_pkg->pkgpart_override ) {
2646 foreach my $cust_svc (
2647 $cust_pkg->h_cust_svc(@dates, 'I')
2650 # eval because not having any part_export_usage exports
2651 # is a fatal error, last_bill/_date because that's how
2652 # sqlradius_hour billing does it
2654 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2656 push @seconds, $sec;
2658 } #if svc_acct-usage_seconds
2662 unless ( $is_summary ) {
2663 warn "$me _items_cust_bill_pkg adding details\n"
2666 #instead of omitting details entirely in this case (unwanted side
2667 # effects), just omit CDRs
2668 $details_opt{'no_usage'} = 1
2669 if $type && $type eq 'R';
2671 push @d, $cust_bill_pkg->details(%details_opt);
2674 warn "$me _items_cust_bill_pkg calculating amount\n"
2679 $amount = $cust_bill_pkg->recur;
2680 } elsif ($type eq 'R') {
2681 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2682 } elsif ($type eq 'U') {
2683 $amount = $cust_bill_pkg->usage;
2687 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2690 if ( !$type || $type eq 'R' ) {
2692 warn "$me _items_cust_bill_pkg adding recur\n"
2695 if ( $cust_bill_pkg->hidden ) {
2696 $r->{amount} += $amount;
2697 $r->{unit_amount} += $unit_amount;
2698 push @{ $r->{ext_description} }, @d;
2701 description => $description,
2702 pkgpart => $pkgpart,
2703 pkgnum => $cust_bill_pkg->pkgnum,
2705 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2706 unit_amount => $unit_amount,
2707 quantity => $cust_bill_pkg->quantity,
2709 ext_description => \@d,
2710 svc_label => ($svc_label || ''),
2712 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2715 } else { # $type eq 'U'
2717 warn "$me _items_cust_bill_pkg adding usage\n"
2720 if ( $cust_bill_pkg->hidden and defined($u) ) {
2721 # if this is a hidden package and there's already a usage
2722 # line for the bundle, add this package's total amount and
2723 # usage details to it
2724 $u->{amount} += $amount;
2725 $u->{unit_amount} += $unit_amount,
2726 push @{ $u->{ext_description} }, @d;
2727 } elsif ( $amount ) {
2728 # create a new usage line
2730 description => $description,
2731 pkgpart => $pkgpart,
2732 pkgnum => $cust_bill_pkg->pkgnum,
2734 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2735 unit_amount => $unit_amount,
2736 quantity => $cust_bill_pkg->quantity,
2738 ext_description => \@d,
2740 } # else this has no usage, so don't create a usage section
2743 } # recurring or usage with recurring charge
2745 } else { #pkgnum tax or one-shot line item (??)
2747 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2750 if ( $cust_bill_pkg->setup != 0 ) {
2752 'description' => $desc,
2753 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2756 if ( $cust_bill_pkg->recur != 0 ) {
2758 'description' => "$desc (".
2759 $self->time2str_local($date_format, $cust_bill_pkg->sdate). ' - '.
2760 $self->time2str_local($date_format, $cust_bill_pkg->edate). ')',
2761 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2769 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2770 && $conf->exists('discount-show-always'));
2774 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2776 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2777 $_->{amount} =~ s/^\-0\.00$/0.00/;
2778 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2780 if $_->{amount} != 0
2781 || $discount_show_always
2782 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2783 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2787 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2794 =item _items_discounts_avail
2796 Returns an array of line item hashrefs representing available term discounts
2797 for this invoice. This makes the same assumptions that apply to term
2798 discounts in general: that the package is billed monthly, at a flat rate,
2799 with no usage charges. A prorated first month will be handled, as will
2800 a setup fee if the discount is allowed to apply to setup fees.
2804 sub _items_discounts_avail {
2807 #maybe move this method from cust_bill when quotations support discount_plans
2808 return () unless $self->can('discount_plans');
2809 my %plans = $self->discount_plans;
2811 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2812 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2816 my $plan = $plans{$months};
2818 my $term_total = sprintf('%.2f', $plan->discounted_total);
2819 my $percent = sprintf('%.0f',
2820 100 * (1 - $term_total / $plan->base_total) );
2821 my $permonth = sprintf('%.2f', $term_total / $months);
2822 my $detail = $self->mt('discount on item'). ' '.
2823 join(', ', map { "#$_" } $plan->pkgnums)
2826 # discounts for non-integer months don't work anyway
2827 $months = sprintf("%d", $months);
2830 description => $self->mt('Save [_1]% by paying for [_2] months',
2832 amount => $self->mt('[_1] ([_2] per month)',
2833 $term_total, $money_char.$permonth),
2834 ext_description => ($detail || ''),
2837 sort { $b <=> $a } keys %plans;