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_$_"} = $escape_function->($cust_main->$method);
586 if ( length($cust_main->ship_company) ) {
587 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
589 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
591 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
592 $invoice_data{'ship_country'} = ''
593 if ( $invoice_data{'ship_country'} eq $countrydefault );
595 $invoice_data{'cid'} = $params{'cid'}
598 if ( $cust_main->country eq $countrydefault ) {
599 $invoice_data{'country'} = '';
601 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
605 $invoice_data{'address'} = \@address;
608 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
609 ? " (P.O. #". $cust_main->payinfo. ")"
613 push @address, $cust_main->company
614 if $cust_main->company;
615 push @address, $cust_main->address1;
616 push @address, $cust_main->address2
617 if $cust_main->address2;
619 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
620 push @address, $invoice_data{'country'}
621 if $invoice_data{'country'};
623 while (scalar(@address) < 5);
625 $invoice_data{'logo_file'} = $params{'logo_file'}
626 if $params{'logo_file'};
627 $invoice_data{'barcode_file'} = $params{'barcode_file'}
628 if $params{'barcode_file'};
629 $invoice_data{'barcode_img'} = $params{'barcode_img'}
630 if $params{'barcode_img'};
631 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
632 if $params{'barcode_cid'};
634 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
635 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
636 #my $balance_due = $self->owed + $pr_total - $cr_total;
637 my $balance_due = $self->owed + $pr_total;
639 #these are used on the summary page only
641 # the customer's current balance as shown on the invoice before this one
642 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
644 # the change in balance from that invoice to this one
645 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
647 # the sum of amount owed on all previous invoices
648 # ($pr_total is used elsewhere but not as $previous_balance)
649 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
651 # the sum of amount owed on all invoices
652 # (this is used in the summary & on the payment coupon)
653 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
655 # info from customer's last invoice before this one, for some
657 $invoice_data{'last_bill'} = {};
659 if ( $self->custnum && $self->invnum ) {
661 if ( $self->previous_bill ) {
662 my $last_bill = $self->previous_bill;
663 $invoice_data{'last_bill'} = {
664 '_date' => $last_bill->_date, #unformatted
666 my (@payments, @credits);
667 # for formats that itemize previous payments
668 foreach my $cust_pay ( qsearch('cust_pay', {
669 'custnum' => $self->custnum,
670 '_date' => { op => '>=',
671 value => $last_bill->_date }
674 next if $cust_pay->_date > $self->_date;
676 '_date' => $cust_pay->_date,
677 'date' => time2str($date_format, $cust_pay->_date),
678 'payinfo' => $cust_pay->payby_payinfo_pretty,
679 'amount' => sprintf('%.2f', $cust_pay->paid),
681 # not concerned about applications
683 foreach my $cust_credit ( qsearch('cust_credit', {
684 'custnum' => $self->custnum,
685 '_date' => { op => '>=',
686 value => $last_bill->_date }
689 next if $cust_credit->_date > $self->_date;
691 '_date' => $cust_credit->_date,
692 'date' => time2str($date_format, $cust_credit->_date),
693 'creditreason'=> $cust_credit->reason,
694 'amount' => sprintf('%.2f', $cust_credit->amount),
697 $invoice_data{'previous_payments'} = \@payments;
698 $invoice_data{'previous_credits'} = \@credits;
703 my $summarypage = '';
704 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
707 $invoice_data{'summarypage'} = $summarypage;
709 warn "$me substituting variables in notes, footer, smallfooter\n"
712 my $tc = $self->template_conf;
713 my @include = ( [ $tc, 'notes' ],
714 [ 'invoice_', 'footer' ],
715 [ 'invoice_', 'smallfooter', ],
717 push @include, [ $tc, 'coupon', ]
718 unless $params{'no_coupon'};
720 foreach my $i (@include) {
722 my($base, $include) = @$i;
724 my $inc_file = $conf->key_orbase("$base$format$include", $template);
727 if ( $conf->exists($inc_file, $agentnum)
728 && length( $conf->config($inc_file, $agentnum) ) ) {
730 @inc_src = $conf->config($inc_file, $agentnum);
734 $inc_file = $conf->key_orbase("${base}latex$include", $template);
736 my $convert_map = $convert_maps{$format}{$include};
738 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
739 s/--\@\]/$delimiters{$format}[1]/g;
742 &$convert_map( $conf->config($inc_file, $agentnum) );
746 my $inc_tt = new Text::Template (
748 SOURCE => [ map "$_\n", @inc_src ],
749 DELIMITERS => $delimiters{$format},
750 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
752 unless ( $inc_tt->compile() ) {
753 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
754 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
758 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
760 $invoice_data{$include} =~ s/\n+$//
761 if ($format eq 'latex');
764 # let invoices use either of these as needed
765 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
766 ? $cust_main->payinfo : '';
767 $invoice_data{'po_line'} =
768 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
769 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
772 my %money_chars = ( 'latex' => '',
773 'html' => $conf->config('money_char') || '$',
776 my $money_char = $money_chars{$format};
778 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
779 'html' => $conf->config('money_char') || '$',
782 my $other_money_char = $other_money_chars{$format};
783 $invoice_data{'dollar'} = $other_money_char;
785 my %minus_signs = ( 'latex' => '$-$',
787 'template' => '- ' );
788 my $minus = $minus_signs{$format};
790 my @detail_items = ();
791 my @total_items = ();
795 $invoice_data{'detail_items'} = \@detail_items;
796 $invoice_data{'total_items'} = \@total_items;
797 $invoice_data{'buf'} = \@buf;
798 $invoice_data{'sections'} = \@sections;
800 warn "$me generating sections\n"
804 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
805 'subtotal' => $taxtotal, # adjusted below
808 my $tax_weight = _pkg_category($tax_section->{description})
809 ? _pkg_category($tax_section->{description})->weight
811 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
812 $tax_section->{'sort_weight'} = $tax_weight;
815 my $adjust_section = {
816 'description' => $self->mt('Credits, Payments, and Adjustments'),
817 'adjust_section' => 1,
818 'subtotal' => 0, # adjusted below
820 my $adjust_weight = _pkg_category($adjust_section->{description})
821 ? _pkg_category($adjust_section->{description})->weight
823 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
824 $adjust_section->{'sort_weight'} = $adjust_weight;
826 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
827 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
828 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
829 $invoice_data{'multisection'} = $multisection;
831 my $extra_sections = [];
832 my $extra_lines = ();
834 # default section ('Charges')
835 my $default_section = { 'description' => '',
840 # Previous Charges section
841 # subtotal is the first return value from $self->previous
842 my $previous_section;
843 # if the invoice has major sections, or if we're summarizing previous
844 # charges with a single line, or if we've been specifically told to put them
845 # in a section, create a section for previous charges:
846 if ( $multisection or
847 $conf->exists('previous_balance-summary_only') or
848 $conf->exists('previous_balance-section') ) {
850 $previous_section = { 'description' => $self->mt('Previous Charges'),
851 'subtotal' => $other_money_char.
852 sprintf('%.2f', $pr_total),
853 'summarized' => '', #why? $summarypage ? 'Y' : '',
855 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
856 join(' / ', map { $cust_main->balance_date_range(@$_) }
857 $self->_prior_month30s
859 if $conf->exists('invoice_include_aging');
862 # otherwise put them in the main section
863 $previous_section = $default_section;
866 if ( $multisection ) {
867 ($extra_sections, $extra_lines) =
868 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
869 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
870 && $self->can('_items_extra_usage_sections');
872 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
874 push @detail_items, @$extra_lines if $extra_lines;
876 # the code is written so that both methods can be used together, but
877 # we haven't yet changed the template to take advantage of that, so for
878 # now, treat them as mutually exclusive.
879 my %section_method = ( by_category => 1 );
880 if ( $conf->exists($tc.'sections_by_location') ) {
881 %section_method = ( by_location => 1 );
884 $self->_items_sections( 'summary' => $summarypage,
885 'escape' => $escape_function_nonbsp,
886 'extra_sections' => $extra_sections,
890 push @sections, @$early;
891 $late_sections = $late;
893 if ( $conf->exists('svc_phone_sections')
894 && $self->can('_items_svc_phone_sections')
897 my ($phone_sections, $phone_lines) =
898 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
899 push @{$late_sections}, @$phone_sections;
900 push @detail_items, @$phone_lines;
902 if ( $conf->exists('voip-cust_accountcode_cdr')
903 && $cust_main->accountcode_cdr
904 && $self->can('_items_accountcode_cdr')
907 my ($accountcode_section, $accountcode_lines) =
908 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
909 if ( scalar(@$accountcode_lines) ) {
910 push @{$late_sections}, $accountcode_section;
911 push @detail_items, @$accountcode_lines;
914 } else {# not multisection
915 # make a default section
916 push @sections, $default_section;
917 # and calculate the finance charge total, since it won't get done otherwise.
918 # and the default section total
919 # XXX possibly finance_pkgclass should not be used in this manner?
922 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
923 if ( $invoice_data{finance_section} and
924 grep { $_->section eq $invoice_data{finance_section} }
925 $cust_bill_pkg->cust_bill_pkg_display ) {
926 # I think these are always setup fees, but just to be sure...
927 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
929 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
932 $invoice_data{finance_amount} =
933 sprintf('%.2f', sum( @finance_charges ) || 0);
934 $default_section->{subtotal} = $other_money_char.
935 sprintf('%.2f', sum( @charges ) || 0);
938 # previous invoice balances in the Previous Charges section if there
939 # is one, otherwise in the main detail section
940 # (except if summary_only is enabled, don't show them at all)
941 if ( $self->can('_items_previous') &&
942 $self->enable_previous &&
943 ! $conf->exists('previous_balance-summary_only') ) {
945 warn "$me adding previous balances\n"
948 foreach my $line_item ( $self->_items_previous ) {
951 ref => $line_item->{'pkgnum'},
952 pkgpart => $line_item->{'pkgpart'},
954 section => $previous_section, # which might be $default_section
955 description => &$escape_function($line_item->{'description'}),
956 ext_description => [ map { &$escape_function($_) }
957 @{ $line_item->{'ext_description'} || [] }
959 amount => ( $old_latex ? '' : $money_char).
960 $line_item->{'amount'},
961 product_code => $line_item->{'pkgpart'} || 'N/A',
964 push @detail_items, $detail;
965 push @buf, [ $detail->{'description'},
966 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
972 if ( @pr_cust_bill && $self->enable_previous ) {
973 push @buf, ['','-----------'];
974 push @buf, [ $self->mt('Total Previous Balance'),
975 $money_char. sprintf("%10.2f", $pr_total) ];
979 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
980 warn "$me adding DID summary\n"
983 my ($didsummary,$minutes) = $self->_did_summary;
984 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
986 { 'description' => $didsummary_desc,
987 'ext_description' => [ $didsummary, $minutes ],
991 foreach my $section (@sections, @$late_sections) {
993 # begin some normalization
994 $section->{'subtotal'} = $section->{'amount'}
996 && !exists($section->{subtotal})
997 && exists($section->{amount});
999 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1000 if ( $invoice_data{finance_section} &&
1001 $section->{'description'} eq $invoice_data{finance_section} );
1003 $section->{'subtotal'} = $other_money_char.
1004 sprintf('%.2f', $section->{'subtotal'})
1007 # continue some normalization
1008 $section->{'amount'} = $section->{'subtotal'}
1012 if ( $section->{'description'} ) {
1013 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1018 warn "$me setting options\n"
1022 $options{'section'} = $section if $multisection;
1023 $options{'format'} = $format;
1024 $options{'escape_function'} = $escape_function;
1025 $options{'no_usage'} = 1 unless $unsquelched;
1026 $options{'unsquelched'} = $unsquelched;
1027 $options{'summary_page'} = $summarypage;
1028 $options{'skip_usage'} =
1029 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1031 warn "$me searching for line items\n"
1034 foreach my $line_item ( $self->_items_pkg(%options) ) {
1036 warn "$me adding line item $line_item\n"
1040 ext_description => [],
1042 $detail->{'ref'} = $line_item->{'pkgnum'};
1043 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1044 $detail->{'quantity'} = $line_item->{'quantity'};
1045 $detail->{'section'} = $section;
1046 $detail->{'description'} = &$escape_function($line_item->{'description'});
1047 if ( exists $line_item->{'ext_description'} ) {
1048 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1050 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1051 $line_item->{'amount'};
1052 if ( exists $line_item->{'unit_amount'} ) {
1053 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1054 $line_item->{'unit_amount'};
1056 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1058 $detail->{'sdate'} = $line_item->{'sdate'};
1059 $detail->{'edate'} = $line_item->{'edate'};
1060 $detail->{'seconds'} = $line_item->{'seconds'};
1061 $detail->{'svc_label'} = $line_item->{'svc_label'};
1063 push @detail_items, $detail;
1064 push @buf, ( [ $detail->{'description'},
1065 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1067 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1071 if ( $section->{'description'} ) {
1072 push @buf, ( ['','-----------'],
1073 [ $section->{'description'}. ' sub-total',
1074 $section->{'subtotal'} # already formatted this
1083 $invoice_data{current_less_finance} =
1084 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1086 # if there's anything in the Previous Charges section, prepend it to the list
1087 if ( $pr_total and $previous_section ne $default_section ) {
1088 unshift @sections, $previous_section;
1091 warn "$me adding taxes\n"
1094 my @items_tax = $self->_items_tax;
1095 foreach my $tax ( @items_tax ) {
1097 $taxtotal += $tax->{'amount'};
1099 my $description = &$escape_function( $tax->{'description'} );
1100 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1102 if ( $multisection ) {
1104 my $money = $old_latex ? '' : $money_char;
1105 push @detail_items, {
1106 ext_description => [],
1109 description => $description,
1110 amount => $money. $amount,
1112 section => $tax_section,
1117 push @total_items, {
1118 'total_item' => $description,
1119 'total_amount' => $other_money_char. $amount,
1124 push @buf,[ $description,
1125 $money_char. $amount,
1132 $total->{'total_item'} = $self->mt('Sub-total');
1133 $total->{'total_amount'} =
1134 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1136 if ( $multisection ) {
1137 $tax_section->{'subtotal'} = $other_money_char.
1138 sprintf('%.2f', $taxtotal);
1139 $tax_section->{'pretotal'} = 'New charges sub-total '.
1140 $total->{'total_amount'};
1141 push @sections, $tax_section if $taxtotal;
1143 unshift @total_items, $total;
1146 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1148 push @buf,['','-----------'];
1149 push @buf,[$self->mt(
1150 (!$self->enable_previous)
1152 : 'Total New Charges'
1154 $money_char. sprintf("%10.2f",$self->charged) ];
1162 my %embolden_functions = (
1163 'latex' => sub { return '\textbf{'. shift(). '}' },
1164 'html' => sub { return '<b>'. shift(). '</b>' },
1165 'template' => sub { shift },
1167 my $embolden_function = $embolden_functions{$format};
1169 if ( $self->can('_items_total') ) { # quotations
1171 $self->_items_total(\@total_items);
1173 foreach ( @total_items ) {
1174 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1175 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1176 $_->{'total_amount'}
1180 } else { #normal invoice case
1182 # calculate total, possibly including total owed on previous
1186 $item = $conf->config('previous_balance-exclude_from_total')
1187 || 'Total New Charges'
1188 if $conf->exists('previous_balance-exclude_from_total');
1189 my $amount = $self->charged;
1190 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1191 $amount += $pr_total;
1194 $total->{'total_item'} = &$embolden_function($self->mt($item));
1195 $total->{'total_amount'} =
1196 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1197 if ( $multisection ) {
1198 if ( $adjust_section->{'sort_weight'} ) {
1199 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1200 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1202 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1203 $other_money_char. sprintf('%.2f', $self->charged );
1206 push @total_items, $total;
1208 push @buf,['','-----------'];
1211 sprintf( '%10.2f', $amount )
1215 # if we're showing previous invoices, also show previous
1216 # credits and payments
1217 if ( $self->enable_previous
1218 and $self->can('_items_credits')
1219 and $self->can('_items_payments') )
1221 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1224 my $credittotal = 0;
1225 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1228 $total->{'total_item'} = &$escape_function($credit->{'description'});
1229 $credittotal += $credit->{'amount'};
1230 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1231 $adjusttotal += $credit->{'amount'};
1232 if ( $multisection ) {
1233 my $money = $old_latex ? '' : $money_char;
1234 push @detail_items, {
1235 ext_description => [],
1238 description => &$escape_function($credit->{'description'}),
1239 amount => $money. $credit->{'amount'},
1241 section => $adjust_section,
1244 push @total_items, $total;
1248 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1251 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1252 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1256 my $paymenttotal = 0;
1257 foreach my $payment ( $self->_items_payments ) {
1259 $total->{'total_item'} = &$escape_function($payment->{'description'});
1260 $paymenttotal += $payment->{'amount'};
1261 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1262 $adjusttotal += $payment->{'amount'};
1263 if ( $multisection ) {
1264 my $money = $old_latex ? '' : $money_char;
1265 push @detail_items, {
1266 ext_description => [],
1269 description => &$escape_function($payment->{'description'}),
1270 amount => $money. $payment->{'amount'},
1272 section => $adjust_section,
1275 push @total_items, $total;
1277 push @buf, [ $payment->{'description'},
1278 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1281 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1283 if ( $multisection ) {
1284 $adjust_section->{'subtotal'} = $other_money_char.
1285 sprintf('%.2f', $adjusttotal);
1286 push @sections, $adjust_section
1287 unless $adjust_section->{sort_weight};
1290 # create Balance Due message
1293 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1294 $total->{'total_amount'} =
1295 &$embolden_function(
1296 $other_money_char. sprintf('%.2f', #why? $summarypage
1297 # ? $self->charged +
1298 # $self->billing_balance
1300 $self->owed + $pr_total
1303 if ( $multisection && !$adjust_section->{sort_weight} ) {
1304 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1305 $total->{'total_amount'};
1307 push @total_items, $total;
1309 push @buf,['','-----------'];
1310 push @buf,[$self->balance_due_msg, $money_char.
1311 sprintf("%10.2f", $balance_due ) ];
1314 if ( $conf->exists('previous_balance-show_credit')
1315 and $cust_main->balance < 0 ) {
1316 my $credit_total = {
1317 'total_item' => &$embolden_function($self->credit_balance_msg),
1318 'total_amount' => &$embolden_function(
1319 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1322 if ( $multisection ) {
1323 $adjust_section->{'posttotal'} .= $newline_token .
1324 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1327 push @total_items, $credit_total;
1329 push @buf,['','-----------'];
1330 push @buf,[$self->credit_balance_msg, $money_char.
1331 sprintf("%10.2f", -$cust_main->balance ) ];
1335 } #end of default total adding ! can('_items_total')
1337 if ( $multisection ) {
1338 if ( $conf->exists('svc_phone_sections')
1339 && $self->can('_items_svc_phone_sections')
1343 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1344 $total->{'total_amount'} =
1345 &$embolden_function(
1346 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1348 my $last_section = pop @sections;
1349 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1350 $total->{'total_amount'};
1351 push @sections, $last_section;
1353 push @sections, @$late_sections
1357 # make a discounts-available section, even without multisection
1358 if ( $conf->exists('discount-show_available')
1359 and my @discounts_avail = $self->_items_discounts_avail ) {
1360 my $discount_section = {
1361 'description' => $self->mt('Discounts Available'),
1366 push @sections, $discount_section;
1367 push @detail_items, map { +{
1368 'ref' => '', #should this be something else?
1369 'section' => $discount_section,
1370 'description' => &$escape_function( $_->{description} ),
1371 'amount' => $money_char . &$escape_function( $_->{amount} ),
1372 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1373 } } @discounts_avail;
1376 my @summary_subtotals;
1377 # the templates say "$_->{tax_section} || !$_->{summarized}"
1378 # except 'summarized' is only true when tax_section is true, so this
1379 # is always true, so what's the deal?
1380 foreach my $s (@sections) {
1381 # not to include in the "summary of new charges" block:
1382 # finance charges, adjustments, previous charges,
1383 # and itemized phone usage sections
1384 if ( $s eq $adjust_section or
1385 ($s eq $previous_section and $s ne $default_section) or
1386 ($invoice_data{'finance_section'} and
1387 $invoice_data{'finance_section'} eq $s->{description}) or
1388 $s->{'description'} =~ /^\d+ $/ ) {
1391 push @summary_subtotals, $s;
1393 $invoice_data{summary_subtotals} = \@summary_subtotals;
1395 # debugging hook: call this with 'diag' => 1 to just get a hash of
1396 # the invoice variables
1397 return \%invoice_data if ( $params{'diag'} );
1399 # All sections and items are built; now fill in templates.
1400 my @includelist = ();
1401 push @includelist, 'summary' if $summarypage;
1402 foreach my $include ( @includelist ) {
1404 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1407 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1409 @inc_src = $conf->config($inc_file, $agentnum);
1413 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1415 my $convert_map = $convert_maps{$format}{$include};
1417 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1418 s/--\@\]/$delimiters{$format}[1]/g;
1421 &$convert_map( $conf->config($inc_file, $agentnum) );
1425 my $inc_tt = new Text::Template (
1427 SOURCE => [ map "$_\n", @inc_src ],
1428 DELIMITERS => $delimiters{$format},
1429 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1431 unless ( $inc_tt->compile() ) {
1432 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1433 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1437 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1439 $invoice_data{$include} =~ s/\n+$//
1440 if ($format eq 'latex');
1445 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1446 /invoice_lines\((\d*)\)/;
1447 $invoice_lines += $1 || scalar(@buf);
1450 die "no invoice_lines() functions in template?"
1451 if ( $format eq 'template' && !$wasfunc );
1453 if ($format eq 'template') {
1455 if ( $invoice_lines ) {
1456 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1457 $invoice_data{'total_pages'}++
1458 if scalar(@buf) % $invoice_lines;
1461 #setup subroutine for the template
1462 $invoice_data{invoice_lines} = sub {
1463 my $lines = shift || scalar(@buf);
1475 push @collect, split("\n",
1476 $text_template->fill_in( HASH => \%invoice_data )
1478 $invoice_data{'page'}++;
1480 map "$_\n", @collect;
1482 } else { # this is where we actually create the invoice
1484 warn "filling in template for invoice ". $self->invnum. "\n"
1486 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1489 $text_template->fill_in(HASH => \%invoice_data);
1493 sub notice_name { '('.shift->table.')'; }
1495 sub template_conf { 'invoice_'; }
1497 # helper routine for generating date ranges
1498 sub _prior_month30s {
1501 [ 1, 2592000 ], # 0-30 days ago
1502 [ 2592000, 5184000 ], # 30-60 days ago
1503 [ 5184000, 7776000 ], # 60-90 days ago
1504 [ 7776000, 0 ], # 90+ days ago
1507 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1508 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1513 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1515 Returns an postscript invoice, as a scalar.
1517 Options can be passed as a hashref (recommended) or as a list of time, template
1518 and then any key/value pairs for any other options.
1520 I<time> an optional value used to control the printing of overdue messages. The
1521 default is now. It isn't the date of the invoice; that's the `_date' field.
1522 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1523 L<Time::Local> and L<Date::Parse> for conversion functions.
1525 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1532 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1533 my $ps = generate_ps($file);
1535 unlink($barcodefile) if $barcodefile;
1540 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1542 Returns an PDF invoice, as a scalar.
1544 Options can be passed as a hashref (recommended) or as a list of time, template
1545 and then any key/value pairs for any other options.
1547 I<time> an optional value used to control the printing of overdue messages. The
1548 default is now. It isn't the date of the invoice; that's the `_date' field.
1549 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1550 L<Time::Local> and L<Date::Parse> for conversion functions.
1552 I<template>, if specified, is the name of a suffix for alternate invoices.
1554 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1561 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1562 my $pdf = generate_pdf($file);
1564 unlink($barcodefile) if $barcodefile;
1569 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1571 Returns an HTML invoice, as a scalar.
1573 I<time> an optional value used to control the printing of overdue messages. The
1574 default is now. It isn't the date of the invoice; that's the `_date' field.
1575 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1576 L<Time::Local> and L<Date::Parse> for conversion functions.
1578 I<template>, if specified, is the name of a suffix for alternate invoices.
1580 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1582 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1583 when emailing the invoice as part of a multipart/related MIME email.
1591 %params = %{ shift() };
1595 $params{'format'} = 'html';
1597 $self->print_generic( %params );
1600 # quick subroutine for print_latex
1602 # There are ten characters that LaTeX treats as special characters, which
1603 # means that they do not simply typeset themselves:
1604 # # $ % & ~ _ ^ \ { }
1606 # TeX ignores blanks following an escaped character; if you want a blank (as
1607 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1611 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1612 $value =~ s/([<>])/\$$1\$/g;
1618 encode_entities($value);
1622 sub _html_escape_nbsp {
1623 my $value = _html_escape(shift);
1624 $value =~ s/ +/ /g;
1628 #utility methods for print_*
1630 sub _translate_old_latex_format {
1631 warn "_translate_old_latex_format called\n"
1638 if ( $line =~ /^%%Detail\s*$/ ) {
1640 push @template, q![@--!,
1641 q! foreach my $_tr_line (@detail_items) {!,
1642 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1643 q! $_tr_line->{'description'} .= !,
1644 q! "\\tabularnewline\n~~".!,
1645 q! join( "\\tabularnewline\n~~",!,
1646 q! @{$_tr_line->{'ext_description'}}!,
1650 while ( ( my $line_item_line = shift )
1651 !~ /^%%EndDetail\s*$/ ) {
1652 $line_item_line =~ s/'/\\'/g; # nice LTS
1653 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1654 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1655 push @template, " \$OUT .= '$line_item_line';";
1658 push @template, '}',
1661 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1663 push @template, '[@--',
1664 ' foreach my $_tr_line (@total_items) {';
1666 while ( ( my $total_item_line = shift )
1667 !~ /^%%EndTotalDetails\s*$/ ) {
1668 $total_item_line =~ s/'/\\'/g; # nice LTS
1669 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1670 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1671 push @template, " \$OUT .= '$total_item_line';";
1674 push @template, '}',
1678 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1679 push @template, $line;
1685 warn "$_\n" foreach @template;
1693 my $conf = $self->conf;
1695 #check for an invoice-specific override
1696 return $self->invoice_terms if $self->invoice_terms;
1698 #check for a customer- specific override
1699 my $cust_main = $self->cust_main;
1700 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1702 #use configured default
1703 $conf->config('invoice_default_terms') || '';
1709 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1710 $duedate = $self->_date() + ( $1 * 86400 );
1717 $self->due_date ? time2str(shift, $self->due_date) : '';
1720 sub balance_due_msg {
1722 my $msg = $self->mt('Balance Due');
1723 return $msg unless $self->terms;
1724 if ( $self->due_date ) {
1725 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1726 $self->due_date2str($date_format);
1727 } elsif ( $self->terms ) {
1728 $msg .= ' - '. $self->terms;
1733 sub balance_due_date {
1735 my $conf = $self->conf;
1737 if ( $conf->exists('invoice_default_terms')
1738 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1739 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1744 sub credit_balance_msg {
1746 $self->mt('Credit Balance Remaining')
1751 Returns a string with the date, for example: "3/20/2008"
1757 time2str($date_format, $self->_date);
1760 =item _items_sections OPTIONS
1762 Generate section information for all items appearing on this invoice.
1763 This will only be called for multi-section invoices.
1765 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1766 related display records (L<FS::cust_bill_pkg_display>) and organize
1767 them into two groups ("early" and "late" according to whether they come
1768 before or after the total), then into sections. A subtotal is calculated
1771 Section descriptions are returned in sort weight order. Each consists
1772 of a hash containing:
1774 description: the package category name, escaped
1775 subtotal: the total charges in that section
1776 tax_section: a flag indicating that the section contains only tax charges
1777 summarized: same as tax_section, for some reason
1778 sort_weight: the package category's sort weight
1780 If 'condense' is set on the display record, it also contains everything
1781 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1782 coderefs to generate parts of the invoice. This is not advised.
1784 The method returns two arrayrefs, one of "early" sections and one of "late"
1787 OPTIONS may include:
1789 by_location: a flag to divide the invoice into sections by location.
1790 Each section hash will have a 'location' element containing a hashref of
1791 the location fields (see L<FS::cust_location>). The section description
1792 will be the location label, but the template can use any of the location
1793 fields to create a suitable label.
1795 by_category: a flag to divide the invoice into sections using display
1796 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1797 behavior. Each section hash will have a 'category' element containing
1798 the section name from the display record (which probably equals the
1799 category name of the package, but may not in some cases).
1801 summary: a flag indicating that this is a summary-format invoice.
1802 Turning this on has the following effects:
1803 - Ignores display items with the 'summary' flag.
1804 - Places all sections in the "early" group even if they have post_total.
1805 - Creates sections for all non-disabled package categories, even if they
1806 have no charges on this invoice, as well as a section with no name.
1808 escape: an escape function to use for section titles.
1810 extra_sections: an arrayref of additional sections to return after the
1811 sorted list. If there are any of these, section subtotals exclude
1814 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1815 passed through to C<_condense_section()>.
1819 use vars qw(%pkg_category_cache);
1820 sub _items_sections {
1824 my $escape = $opt{escape};
1825 my @extra_sections = @{ $opt{extra_sections} || [] };
1827 # $subtotal{$locationnum}{$categoryname} = amount.
1828 # if we're not using by_location, $locationnum is undef.
1829 # if we're not using by_category, you guessed it, $categoryname is undef.
1830 # if we're not using either one, we shouldn't be here in the first place...
1832 my %late_subtotal = ();
1835 # About tax items + multisection invoices:
1836 # If either invoice_*summary option is enabled, AND there is a
1837 # package category with the name of the tax, then there will be
1838 # a display record assigning the tax item to that category.
1840 # However, the taxes are always placed in the "Taxes, Surcharges,
1841 # and Fees" section regardless of that. The only effect of the
1842 # display record is to create a subtotal for the summary page.
1845 my $pkg_hash = $self->cust_pkg_hash;
1847 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1850 my $usage = $cust_bill_pkg->usage;
1853 if ( $opt{by_location} ) {
1854 if ( $cust_bill_pkg->pkgnum ) {
1855 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1860 $locationnum = undef;
1863 # as in _items_cust_pkg, if a line item has no display records,
1864 # cust_bill_pkg_display() returns a default record for it
1866 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1867 next if ( $display->summary && $opt{summary} );
1869 my $section = $display->section;
1870 my $type = $display->type;
1871 $section = undef unless $opt{by_category};
1873 $not_tax{$locationnum}{$section} = 1
1874 unless $cust_bill_pkg->pkgnum == 0;
1876 # there's actually a very important piece of logic buried in here:
1877 # incrementing $late_subtotal{$section} CREATES
1878 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1879 # to define the list of late sections, and likewise keys(%subtotal).
1880 # When _items_cust_bill_pkg is called to generate line items for
1881 # real, it will be called with 'section' => $section for each
1883 if ( $display->post_total && !$opt{summary} ) {
1884 if (! $type || $type eq 'S') {
1885 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1886 if $cust_bill_pkg->setup != 0
1887 || $cust_bill_pkg->setup_show_zero;
1891 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1892 if $cust_bill_pkg->recur != 0
1893 || $cust_bill_pkg->recur_show_zero;
1896 if ($type && $type eq 'R') {
1897 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1898 if $cust_bill_pkg->recur != 0
1899 || $cust_bill_pkg->recur_show_zero;
1902 if ($type && $type eq 'U') {
1903 $late_subtotal{$locationnum}{$section} += $usage
1904 unless scalar(@extra_sections);
1907 } else { # it's a pre-total (normal) section
1909 # skip tax items unless they're explicitly included in a section
1910 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1912 if (! $type || $type eq 'S') {
1913 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1914 if $cust_bill_pkg->setup != 0
1915 || $cust_bill_pkg->setup_show_zero;
1919 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1920 if $cust_bill_pkg->recur != 0
1921 || $cust_bill_pkg->recur_show_zero;
1924 if ($type && $type eq 'R') {
1925 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1926 if $cust_bill_pkg->recur != 0
1927 || $cust_bill_pkg->recur_show_zero;
1930 if ($type && $type eq 'U') {
1931 $subtotal{$locationnum}{$section} += $usage
1932 unless scalar(@extra_sections);
1941 %pkg_category_cache = ();
1943 # summary invoices need subtotals for all non-disabled package categories,
1944 # even if they're zero
1945 # but currently assume that there are no location sections, or at least
1946 # that the summary page doesn't care about them
1947 if ( $opt{summary} ) {
1948 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1949 $subtotal{''}{$category->categoryname} ||= 0;
1951 $subtotal{''}{''} ||= 0;
1955 foreach my $post_total (0,1) {
1957 my $s = $post_total ? \%late_subtotal : \%subtotal;
1958 foreach my $locationnum (keys %$s) {
1959 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1961 'subtotal' => $s->{$locationnum}{$sectionname},
1962 'post_total' => $post_total,
1965 if ( $locationnum ) {
1966 $section->{'locationnum'} = $locationnum;
1967 my $location = FS::cust_location->by_key($locationnum);
1968 $section->{'description'} = &{ $escape }($location->location_label);
1969 # Better ideas? This will roughly group them by proximity,
1970 # which alpha sorting on any of the address fields won't.
1971 # Sorting by locationnum is meaningless.
1972 # We have to sort on _something_ or the order may change
1973 # randomly from one invoice to the next, which will confuse
1975 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1977 $section->{'location'} = {
1978 map { $_ => &{ $escape }($location->get($_)) }
1982 $section->{'category'} = $sectionname;
1983 $section->{'description'} = &{ $escape }($sectionname);
1984 if ( _pkg_category($_) ) {
1985 $section->{'sort_weight'} = _pkg_category($_)->weight;
1986 if ( _pkg_category($_)->condense ) {
1987 $section = { %$section, $self->_condense_section($opt{format}) };
1991 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1992 # then it's a tax-only section
1993 $section->{'summarized'} = 'Y';
1994 $section->{'tax_section'} = 'Y';
1996 push @these, $section;
1997 } # foreach $sectionname
1998 } #foreach $locationnum
1999 push @these, @extra_sections if $post_total == 0;
2000 # need an alpha sort for location sections, because postal codes can
2002 $sections[ $post_total ] = [ sort {
2003 $opt{'by_location'} ?
2004 ($a->{sort_weight} cmp $b->{sort_weight}) :
2005 ($a->{sort_weight} <=> $b->{sort_weight})
2007 } #foreach $post_total
2009 return @sections; # early, late
2012 #helper subs for above
2016 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2020 my $categoryname = shift;
2021 $pkg_category_cache{$categoryname} ||=
2022 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2025 my %condensed_format = (
2026 'label' => [ qw( Description Qty Amount ) ],
2028 sub { shift->{description} },
2029 sub { shift->{quantity} },
2030 sub { my($href, %opt) = @_;
2031 ($opt{dollar} || ''). $href->{amount};
2034 'align' => [ qw( l r r ) ],
2035 'span' => [ qw( 5 1 1 ) ], # unitprices?
2036 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2039 sub _condense_section {
2040 my ( $self, $format ) = ( shift, shift );
2042 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2043 qw( description_generator
2046 total_line_generator
2051 sub _condensed_generator_defaults {
2052 my ( $self, $format ) = ( shift, shift );
2053 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2062 sub _condensed_header_generator {
2063 my ( $self, $format ) = ( shift, shift );
2065 my ( $f, $prefix, $suffix, $separator, $column ) =
2066 _condensed_generator_defaults($format);
2068 if ($format eq 'latex') {
2069 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2070 $suffix = "\\\\\n\\hline";
2073 sub { my ($d,$a,$s,$w) = @_;
2074 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2076 } elsif ( $format eq 'html' ) {
2077 $prefix = '<th></th>';
2081 sub { my ($d,$a,$s,$w) = @_;
2082 return qq!<th align="$html_align{$a}">$d</th>!;
2090 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2092 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2095 $prefix. join($separator, @result). $suffix;
2100 sub _condensed_description_generator {
2101 my ( $self, $format ) = ( shift, shift );
2103 my ( $f, $prefix, $suffix, $separator, $column ) =
2104 _condensed_generator_defaults($format);
2106 my $money_char = '$';
2107 if ($format eq 'latex') {
2108 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2110 $separator = " & \n";
2112 sub { my ($d,$a,$s,$w) = @_;
2113 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2115 $money_char = '\\dollar';
2116 }elsif ( $format eq 'html' ) {
2117 $prefix = '"><td align="center"></td>';
2121 sub { my ($d,$a,$s,$w) = @_;
2122 return qq!<td align="$html_align{$a}">$d</td>!;
2124 #$money_char = $conf->config('money_char') || '$';
2125 $money_char = ''; # this is madness
2133 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2135 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2137 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2138 map { $f->{$_}->[$i] } qw(align span width)
2142 $prefix. join( $separator, @result ). $suffix;
2147 sub _condensed_total_generator {
2148 my ( $self, $format ) = ( shift, shift );
2150 my ( $f, $prefix, $suffix, $separator, $column ) =
2151 _condensed_generator_defaults($format);
2154 if ($format eq 'latex') {
2157 $separator = " & \n";
2159 sub { my ($d,$a,$s,$w) = @_;
2160 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2162 }elsif ( $format eq 'html' ) {
2166 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2168 sub { my ($d,$a,$s,$w) = @_;
2169 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2178 # my $r = &{$f->{fields}->[$i]}(@args);
2179 # $r .= ' Total' unless $i;
2181 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2183 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2184 map { $f->{$_}->[$i] } qw(align span width)
2188 $prefix. join( $separator, @result ). $suffix;
2193 =item total_line_generator FORMAT
2195 Returns a coderef used for generation of invoice total line items for this
2196 usage_class. FORMAT is either html or latex
2200 # should not be used: will have issues with hash element names (description vs
2201 # total_item and amount vs total_amount -- another array of functions?
2203 sub _condensed_total_line_generator {
2204 my ( $self, $format ) = ( shift, shift );
2206 my ( $f, $prefix, $suffix, $separator, $column ) =
2207 _condensed_generator_defaults($format);
2210 if ($format eq 'latex') {
2213 $separator = " & \n";
2215 sub { my ($d,$a,$s,$w) = @_;
2216 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2218 }elsif ( $format eq 'html' ) {
2222 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2224 sub { my ($d,$a,$s,$w) = @_;
2225 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2234 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2236 &{$column}( &{$f->{fields}->[$i]}(@args),
2237 map { $f->{$_}->[$i] } qw(align span width)
2241 $prefix. join( $separator, @result ). $suffix;
2246 =item _items_pkg [ OPTIONS ]
2248 Return line item hashes for each package item on this invoice. Nearly
2251 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2253 The only OPTIONS accepted is 'section', which may point to a hashref
2254 with a key named 'condensed', which may have a true value. If it
2255 does, this method tries to merge identical items into items with
2256 'quantity' equal to the number of items (not the sum of their
2257 separate quantities, for some reason).
2263 grep { $_->pkgnum } $self->cust_bill_pkg;
2270 warn "$me _items_pkg searching for all package line items\n"
2273 my @cust_bill_pkg = $self->_items_nontax;
2275 warn "$me _items_pkg filtering line items\n"
2277 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2279 if ($options{section} && $options{section}->{condensed}) {
2281 warn "$me _items_pkg condensing section\n"
2285 local $Storable::canonical = 1;
2286 foreach ( @items ) {
2288 delete $item->{ref};
2289 delete $item->{ext_description};
2290 my $key = freeze($item);
2291 $itemshash{$key} ||= 0;
2292 $itemshash{$key} ++; # += $item->{quantity};
2294 @items = sort { $a->{description} cmp $b->{description} }
2295 map { my $i = thaw($_);
2296 $i->{quantity} = $itemshash{$_};
2298 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2304 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2311 return 0 unless $a->itemdesc cmp $b->itemdesc;
2312 return -1 if $b->itemdesc eq 'Tax';
2313 return 1 if $a->itemdesc eq 'Tax';
2314 return -1 if $b->itemdesc eq 'Other surcharges';
2315 return 1 if $a->itemdesc eq 'Other surcharges';
2316 $a->itemdesc cmp $b->itemdesc;
2321 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2322 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2324 if ( $self->conf->exists('always_show_tax') ) {
2325 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2326 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2328 { 'description' => $itemdesc,
2335 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2337 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2338 list of hashrefs describing the line items they generate on the invoice.
2340 OPTIONS may include:
2342 format: the invoice format.
2344 escape_function: the function used to escape strings.
2346 DEPRECATED? (expensive, mostly unused?)
2347 format_function: the function used to format CDRs.
2349 section: a hashref containing 'category' and/or 'locationnum'; if this
2350 is present, only returns line items that belong to that category and/or
2351 location (whichever is defined).
2353 multisection: a flag indicating that this is a multisection invoice,
2354 which does something complicated.
2356 Returns a list of hashrefs, each of which may contain:
2358 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2359 ext_description, which is an arrayref of detail lines to show below
2364 sub _items_cust_bill_pkg {
2366 my $conf = $self->conf;
2367 my $cust_bill_pkgs = shift;
2370 my $format = $opt{format} || '';
2371 my $escape_function = $opt{escape_function} || sub { shift };
2372 my $format_function = $opt{format_function} || '';
2373 my $no_usage = $opt{no_usage} || '';
2374 my $unsquelched = $opt{unsquelched} || ''; #unused
2375 my ($section, $locationnum, $category);
2376 if ( $opt{section} ) {
2377 $category = $opt{section}->{category};
2378 $locationnum = $opt{section}->{locationnum};
2380 my $summary_page = $opt{summary_page} || ''; #unused
2381 my $multisection = defined($category) || defined($locationnum);
2382 my $discount_show_always = 0;
2384 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2386 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2387 # and location labels
2390 my ($s, $r, $u) = ( undef, undef, undef );
2391 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2394 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2395 if ( $_ && !$cust_bill_pkg->hidden ) {
2396 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2397 $_->{amount} =~ s/^\-0\.00$/0.00/;
2398 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2400 if $_->{amount} != 0
2401 || $discount_show_always
2402 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2403 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2409 if ( $locationnum ) {
2410 # this is a location section; skip packages that aren't at this
2412 next if $cust_bill_pkg->pkgnum == 0;
2413 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2417 # Consider display records for this item to determine if it belongs
2418 # in this section. Note that if there are no display records, there
2419 # will be a default pseudo-record that includes all charge types
2420 # and has no section name.
2421 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2422 ? $cust_bill_pkg->cust_bill_pkg_display
2423 : ( $cust_bill_pkg );
2425 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2426 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2429 if ( defined($category) ) {
2430 # then this is a package category section; process all display records
2431 # that belong to this section.
2432 @cust_bill_pkg_display = grep { $_->section eq $category }
2433 @cust_bill_pkg_display;
2435 # otherwise, process all display records that aren't usage summaries
2436 # (I don't think there should be usage summaries if you aren't using
2437 # category sections, but this is the historical behavior)
2438 @cust_bill_pkg_display = grep { !$_->summary }
2439 @cust_bill_pkg_display;
2441 foreach my $display (@cust_bill_pkg_display) {
2443 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2444 $display->billpkgdisplaynum. "\n"
2447 my $type = $display->type;
2449 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2450 $desc = substr($desc, 0, $maxlength). '...'
2451 if $format eq 'latex' && length($desc) > $maxlength;
2453 my %details_opt = ( 'format' => $format,
2454 'escape_function' => $escape_function,
2455 'format_function' => $format_function,
2456 'no_usage' => $opt{'no_usage'},
2459 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2461 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2464 if ( $cust_bill_pkg->setup != 0 ) {
2465 my $description = $desc;
2466 $description .= ' Setup'
2467 if $cust_bill_pkg->recur != 0
2468 || $discount_show_always
2469 || $cust_bill_pkg->recur_show_zero;
2471 'description' => $description,
2472 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2475 if ( $cust_bill_pkg->recur != 0 ) {
2477 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2478 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2482 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2484 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2487 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2489 # which pkgpart to show for display purposes?
2490 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2492 # start/end dates for invoice formats that do nonstandard
2494 my %item_dates = ();
2495 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2496 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2498 if ( (!$type || $type eq 'S')
2499 && ( $cust_bill_pkg->setup != 0
2500 || $cust_bill_pkg->setup_show_zero
2505 warn "$me _items_cust_bill_pkg adding setup\n"
2508 my $description = $desc;
2509 $description .= ' Setup'
2510 if $cust_bill_pkg->recur != 0
2511 || $discount_show_always
2512 || $cust_bill_pkg->recur_show_zero;
2516 unless ( $cust_pkg->part_pkg->hide_svc_detail
2517 || $cust_bill_pkg->hidden )
2520 my @svc_labels = map &{$escape_function}($_),
2521 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2522 push @d, @svc_labels
2523 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2524 $svc_label = $svc_labels[0];
2526 my $lnum = $cust_main ? $cust_main->ship_locationnum
2527 : $self->prospect_main->locationnum;
2528 # show the location label if it's not the customer's default
2529 # location, and we're not grouping items by location already
2530 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2531 my $loc = $cust_pkg->location_label;
2532 $loc = substr($loc, 0, $maxlength). '...'
2533 if $format eq 'latex' && length($loc) > $maxlength;
2534 push @d, &{$escape_function}($loc);
2537 } #unless hiding service details
2539 push @d, $cust_bill_pkg->details(%details_opt)
2540 if $cust_bill_pkg->recur == 0;
2542 if ( $cust_bill_pkg->hidden ) {
2543 $s->{amount} += $cust_bill_pkg->setup;
2544 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2545 push @{ $s->{ext_description} }, @d;
2549 description => $description,
2550 pkgpart => $pkgpart,
2551 pkgnum => $cust_bill_pkg->pkgnum,
2552 amount => $cust_bill_pkg->setup,
2553 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2554 unit_amount => $cust_bill_pkg->unitsetup,
2555 quantity => $cust_bill_pkg->quantity,
2556 ext_description => \@d,
2557 svc_label => ($svc_label || ''),
2563 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2565 $cust_bill_pkg->recur != 0
2566 || $cust_bill_pkg->setup == 0
2567 || $discount_show_always
2568 || $cust_bill_pkg->recur_show_zero
2573 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2576 my $is_summary = $display->summary;
2577 my $description = $desc;
2578 if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2579 $description = $self->mt('Usage charges');
2582 my $part_pkg = $cust_pkg->part_pkg;
2584 #pry be a bit more efficient to look some of this conf stuff up
2587 $conf->exists('disable_line_item_date_ranges')
2588 || $part_pkg->option('disable_line_item_date_ranges',1)
2589 || ! $cust_bill_pkg->sdate
2590 || ! $cust_bill_pkg->edate
2593 my $date_style = '';
2594 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2597 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2598 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2601 if ( defined($date_style) && $date_style eq 'month_of' ) {
2602 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2603 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2604 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2607 $desc .= ' ' unless $desc =~ /\s$/;
2608 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2610 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2611 " - ". time2str($date_format, $cust_bill_pkg->edate);
2613 $description .= " ($time_period)";
2617 my @seconds = (); # for display of usage info
2620 #at least until cust_bill_pkg has "past" ranges in addition to
2621 #the "future" sdate/edate ones... see #3032
2622 my @dates = ( $self->_date );
2623 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2624 push @dates, $prev->sdate if $prev;
2625 push @dates, undef if !$prev;
2627 unless ( $part_pkg->hide_svc_detail
2628 || $cust_bill_pkg->itemdesc
2629 || $cust_bill_pkg->hidden
2630 || $is_summary && $type && $type eq 'U'
2634 warn "$me _items_cust_bill_pkg adding service details\n"
2637 my @svc_labels = map &{$escape_function}($_),
2638 $cust_pkg->h_labels_short(@dates, 'I');
2639 push @d, @svc_labels
2640 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2641 $svc_label = $svc_labels[0];
2643 warn "$me _items_cust_bill_pkg done adding service details\n"
2646 my $lnum = $cust_main ? $cust_main->ship_locationnum
2647 : $self->prospect_main->locationnum;
2648 # show the location label if it's not the customer's default
2649 # location, and we're not grouping items by location already
2650 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2651 my $loc = $cust_pkg->location_label;
2652 $loc = substr($loc, 0, $maxlength). '...'
2653 if $format eq 'latex' && length($loc) > $maxlength;
2654 push @d, &{$escape_function}($loc);
2657 # Display of seconds_since_sqlradacct:
2658 # On the invoice, when processing @detail_items, look for a field
2659 # named 'seconds'. This will contain total seconds for each
2660 # service, in the same order as @ext_description. For services
2661 # that don't support this it will show undef.
2662 if ( $conf->exists('svc_acct-usage_seconds')
2663 and ! $cust_bill_pkg->pkgpart_override ) {
2664 foreach my $cust_svc (
2665 $cust_pkg->h_cust_svc(@dates, 'I')
2668 # eval because not having any part_export_usage exports
2669 # is a fatal error, last_bill/_date because that's how
2670 # sqlradius_hour billing does it
2672 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2674 push @seconds, $sec;
2676 } #if svc_acct-usage_seconds
2680 unless ( $is_summary ) {
2681 warn "$me _items_cust_bill_pkg adding details\n"
2684 #instead of omitting details entirely in this case (unwanted side
2685 # effects), just omit CDRs
2686 $details_opt{'no_usage'} = 1
2687 if $type && $type eq 'R';
2689 push @d, $cust_bill_pkg->details(%details_opt);
2692 warn "$me _items_cust_bill_pkg calculating amount\n"
2697 $amount = $cust_bill_pkg->recur;
2698 } elsif ($type eq 'R') {
2699 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2700 } elsif ($type eq 'U') {
2701 $amount = $cust_bill_pkg->usage;
2705 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2708 if ( !$type || $type eq 'R' ) {
2710 warn "$me _items_cust_bill_pkg adding recur\n"
2713 if ( $cust_bill_pkg->hidden ) {
2714 $r->{amount} += $amount;
2715 $r->{unit_amount} += $unit_amount;
2716 push @{ $r->{ext_description} }, @d;
2719 description => $description,
2720 pkgpart => $pkgpart,
2721 pkgnum => $cust_bill_pkg->pkgnum,
2723 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2724 unit_amount => $unit_amount,
2725 quantity => $cust_bill_pkg->quantity,
2727 ext_description => \@d,
2728 svc_label => ($svc_label || ''),
2730 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2733 } else { # $type eq 'U'
2735 warn "$me _items_cust_bill_pkg adding usage\n"
2738 if ( $cust_bill_pkg->hidden and defined($u) ) {
2739 # if this is a hidden package and there's already a usage
2740 # line for the bundle, add this package's total amount and
2741 # usage details to it
2742 $u->{amount} += $amount;
2743 $u->{unit_amount} += $unit_amount,
2744 push @{ $u->{ext_description} }, @d;
2745 } elsif ( $amount ) {
2746 # create a new usage line
2748 description => $description,
2749 pkgpart => $pkgpart,
2750 pkgnum => $cust_bill_pkg->pkgnum,
2752 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2753 unit_amount => $unit_amount,
2754 quantity => $cust_bill_pkg->quantity,
2756 ext_description => \@d,
2758 } # else this has no usage, so don't create a usage section
2761 } # recurring or usage with recurring charge
2763 } else { #pkgnum tax or one-shot line item (??)
2765 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2768 if ( $cust_bill_pkg->setup != 0 ) {
2770 'description' => $desc,
2771 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2774 if ( $cust_bill_pkg->recur != 0 ) {
2776 'description' => "$desc (".
2777 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2778 time2str($date_format, $cust_bill_pkg->edate). ')',
2779 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2787 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2788 && $conf->exists('discount-show-always'));
2792 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2794 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2795 $_->{amount} =~ s/^\-0\.00$/0.00/;
2796 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2798 if $_->{amount} != 0
2799 || $discount_show_always
2800 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2801 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2805 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2812 =item _items_discounts_avail
2814 Returns an array of line item hashrefs representing available term discounts
2815 for this invoice. This makes the same assumptions that apply to term
2816 discounts in general: that the package is billed monthly, at a flat rate,
2817 with no usage charges. A prorated first month will be handled, as will
2818 a setup fee if the discount is allowed to apply to setup fees.
2822 sub _items_discounts_avail {
2825 #maybe move this method from cust_bill when quotations support discount_plans
2826 return () unless $self->can('discount_plans');
2827 my %plans = $self->discount_plans;
2829 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2830 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2834 my $plan = $plans{$months};
2836 my $term_total = sprintf('%.2f', $plan->discounted_total);
2837 my $percent = sprintf('%.0f',
2838 100 * (1 - $term_total / $plan->base_total) );
2839 my $permonth = sprintf('%.2f', $term_total / $months);
2840 my $detail = $self->mt('discount on item'). ' '.
2841 join(', ', map { "#$_" } $plan->pkgnums)
2844 # discounts for non-integer months don't work anyway
2845 $months = sprintf("%d", $months);
2848 description => $self->mt('Save [_1]% by paying for [_2] months',
2850 amount => $self->mt('[_1] ([_2] per month)',
2851 $term_total, $money_char.$permonth),
2852 ext_description => ($detail || ''),
2855 sort { $b <=> $a } keys %plans;