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 : time2str($date_format, $self->_date)
503 'today' => time2str($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 my $lh = FS::L10N->get_handle( $locale );
549 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
550 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
551 # eval to avoid death for unimplemented languages
552 my $dh = eval { Date::Language->new($info{'name'}) } ||
553 Date::Language->new(); # fall back to English
554 # prototype here to silence warnings
555 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
556 # eventually use this date handle everywhere in here, too
558 my $min_sdate = 999999999999;
560 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
561 next unless $cust_bill_pkg->pkgnum > 0;
562 $min_sdate = $cust_bill_pkg->sdate
563 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
564 $max_edate = $cust_bill_pkg->edate
565 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
568 $invoice_data{'bill_period'} = '';
569 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
570 . " to " . time2str('%e %h', $max_edate)
571 if ($max_edate != 0 && $min_sdate != 999999999999);
573 $invoice_data{finance_section} = '';
574 if ( $conf->config('finance_pkgclass') ) {
576 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
577 $invoice_data{finance_section} = $pkg_class->categoryname;
579 $invoice_data{finance_amount} = '0.00';
580 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
582 my $countrydefault = $conf->config('countrydefault') || 'US';
583 foreach ( qw( address1 address2 city state zip country fax) ){
584 my $method = 'ship_'.$_;
585 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
587 if ( length($cust_main->ship_company) ) {
588 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
590 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
592 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
593 $invoice_data{'ship_country'} = ''
594 if ( $invoice_data{'ship_country'} eq $countrydefault );
596 $invoice_data{'cid'} = $params{'cid'}
599 if ( $cust_main->country eq $countrydefault ) {
600 $invoice_data{'country'} = '';
602 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
606 $invoice_data{'address'} = \@address;
609 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
610 ? " (P.O. #". $cust_main->payinfo. ")"
614 push @address, $cust_main->company
615 if $cust_main->company;
616 push @address, $cust_main->address1;
617 push @address, $cust_main->address2
618 if $cust_main->address2;
620 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
621 push @address, $invoice_data{'country'}
622 if $invoice_data{'country'};
624 while (scalar(@address) < 5);
626 $invoice_data{'logo_file'} = $params{'logo_file'}
627 if $params{'logo_file'};
628 $invoice_data{'barcode_file'} = $params{'barcode_file'}
629 if $params{'barcode_file'};
630 $invoice_data{'barcode_img'} = $params{'barcode_img'}
631 if $params{'barcode_img'};
632 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
633 if $params{'barcode_cid'};
635 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
636 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
637 #my $balance_due = $self->owed + $pr_total - $cr_total;
638 my $balance_due = $self->owed + $pr_total;
640 #these are used on the summary page only
642 # the customer's current balance as shown on the invoice before this one
643 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
645 # the change in balance from that invoice to this one
646 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
648 # the sum of amount owed on all previous invoices
649 # ($pr_total is used elsewhere but not as $previous_balance)
650 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
652 # the sum of amount owed on all invoices
653 # (this is used in the summary & on the payment coupon)
654 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
656 # info from customer's last invoice before this one, for some
658 $invoice_data{'last_bill'} = {};
660 if ( $self->custnum && $self->invnum ) {
662 if ( $self->previous_bill ) {
663 my $last_bill = $self->previous_bill;
664 $invoice_data{'last_bill'} = {
665 '_date' => $last_bill->_date, #unformatted
667 my (@payments, @credits);
668 # for formats that itemize previous payments
669 foreach my $cust_pay ( qsearch('cust_pay', {
670 'custnum' => $self->custnum,
671 '_date' => { op => '>=',
672 value => $last_bill->_date }
675 next if $cust_pay->_date > $self->_date;
677 '_date' => $cust_pay->_date,
678 'date' => time2str($date_format, $cust_pay->_date),
679 'payinfo' => $cust_pay->payby_payinfo_pretty,
680 'amount' => sprintf('%.2f', $cust_pay->paid),
682 # not concerned about applications
684 foreach my $cust_credit ( qsearch('cust_credit', {
685 'custnum' => $self->custnum,
686 '_date' => { op => '>=',
687 value => $last_bill->_date }
690 next if $cust_credit->_date > $self->_date;
692 '_date' => $cust_credit->_date,
693 'date' => time2str($date_format, $cust_credit->_date),
694 'creditreason'=> $cust_credit->reason,
695 'amount' => sprintf('%.2f', $cust_credit->amount),
698 $invoice_data{'previous_payments'} = \@payments;
699 $invoice_data{'previous_credits'} = \@credits;
704 my $summarypage = '';
705 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
708 $invoice_data{'summarypage'} = $summarypage;
710 warn "$me substituting variables in notes, footer, smallfooter\n"
713 my $tc = $self->template_conf;
714 my @include = ( [ $tc, 'notes' ],
715 [ 'invoice_', 'footer' ],
716 [ 'invoice_', 'smallfooter', ],
718 push @include, [ $tc, 'coupon', ]
719 unless $params{'no_coupon'};
721 foreach my $i (@include) {
723 my($base, $include) = @$i;
725 my $inc_file = $conf->key_orbase("$base$format$include", $template);
728 if ( $conf->exists($inc_file, $agentnum)
729 && length( $conf->config($inc_file, $agentnum) ) ) {
731 @inc_src = $conf->config($inc_file, $agentnum);
735 $inc_file = $conf->key_orbase("${base}latex$include", $template);
737 my $convert_map = $convert_maps{$format}{$include};
739 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
740 s/--\@\]/$delimiters{$format}[1]/g;
743 &$convert_map( $conf->config($inc_file, $agentnum) );
747 my $inc_tt = new Text::Template (
749 SOURCE => [ map "$_\n", @inc_src ],
750 DELIMITERS => $delimiters{$format},
751 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
753 unless ( $inc_tt->compile() ) {
754 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
755 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
759 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
761 $invoice_data{$include} =~ s/\n+$//
762 if ($format eq 'latex');
765 # let invoices use either of these as needed
766 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
767 ? $cust_main->payinfo : '';
768 $invoice_data{'po_line'} =
769 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
770 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
773 my %money_chars = ( 'latex' => '',
774 'html' => $conf->config('money_char') || '$',
777 my $money_char = $money_chars{$format};
779 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
780 'html' => $conf->config('money_char') || '$',
783 my $other_money_char = $other_money_chars{$format};
784 $invoice_data{'dollar'} = $other_money_char;
786 my %minus_signs = ( 'latex' => '$-$',
788 'template' => '- ' );
789 my $minus = $minus_signs{$format};
791 my @detail_items = ();
792 my @total_items = ();
796 $invoice_data{'detail_items'} = \@detail_items;
797 $invoice_data{'total_items'} = \@total_items;
798 $invoice_data{'buf'} = \@buf;
799 $invoice_data{'sections'} = \@sections;
801 warn "$me generating sections\n"
805 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
806 'subtotal' => $taxtotal, # adjusted below
809 my $tax_weight = _pkg_category($tax_section->{description})
810 ? _pkg_category($tax_section->{description})->weight
812 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
813 $tax_section->{'sort_weight'} = $tax_weight;
816 my $adjust_section = {
817 'description' => $self->mt('Credits, Payments, and Adjustments'),
818 'adjust_section' => 1,
819 'subtotal' => 0, # adjusted below
821 my $adjust_weight = _pkg_category($adjust_section->{description})
822 ? _pkg_category($adjust_section->{description})->weight
824 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
825 $adjust_section->{'sort_weight'} = $adjust_weight;
827 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
828 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
829 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
830 $invoice_data{'multisection'} = $multisection;
832 my $extra_sections = [];
833 my $extra_lines = ();
835 # default section ('Charges')
836 my $default_section = { 'description' => '',
841 # Previous Charges section
842 # subtotal is the first return value from $self->previous
843 my $previous_section;
844 # if the invoice has major sections, or if we're summarizing previous
845 # charges with a single line, or if we've been specifically told to put them
846 # in a section, create a section for previous charges:
847 if ( $multisection or
848 $conf->exists('previous_balance-summary_only') or
849 $conf->exists('previous_balance-section') ) {
851 $previous_section = { 'description' => $self->mt('Previous Charges'),
852 'subtotal' => $other_money_char.
853 sprintf('%.2f', $pr_total),
854 'summarized' => '', #why? $summarypage ? 'Y' : '',
856 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
857 join(' / ', map { $cust_main->balance_date_range(@$_) }
858 $self->_prior_month30s
860 if $conf->exists('invoice_include_aging');
863 # otherwise put them in the main section
864 $previous_section = $default_section;
867 if ( $multisection ) {
868 ($extra_sections, $extra_lines) =
869 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
870 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
871 && $self->can('_items_extra_usage_sections');
873 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
875 push @detail_items, @$extra_lines if $extra_lines;
877 # the code is written so that both methods can be used together, but
878 # we haven't yet changed the template to take advantage of that, so for
879 # now, treat them as mutually exclusive.
880 my %section_method = ( by_category => 1 );
881 if ( $conf->exists($tc.'sections_by_location') ) {
882 %section_method = ( by_location => 1 );
885 $self->_items_sections( 'summary' => $summarypage,
886 'escape' => $escape_function_nonbsp,
887 'extra_sections' => $extra_sections,
891 push @sections, @$early;
892 $late_sections = $late;
894 if ( $conf->exists('svc_phone_sections')
895 && $self->can('_items_svc_phone_sections')
898 my ($phone_sections, $phone_lines) =
899 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
900 push @{$late_sections}, @$phone_sections;
901 push @detail_items, @$phone_lines;
903 if ( $conf->exists('voip-cust_accountcode_cdr')
904 && $cust_main->accountcode_cdr
905 && $self->can('_items_accountcode_cdr')
908 my ($accountcode_section, $accountcode_lines) =
909 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
910 if ( scalar(@$accountcode_lines) ) {
911 push @{$late_sections}, $accountcode_section;
912 push @detail_items, @$accountcode_lines;
915 } else {# not multisection
916 # make a default section
917 push @sections, $default_section;
918 # and calculate the finance charge total, since it won't get done otherwise.
919 # and the default section total
920 # XXX possibly finance_pkgclass should not be used in this manner?
923 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
924 if ( $invoice_data{finance_section} and
925 grep { $_->section eq $invoice_data{finance_section} }
926 $cust_bill_pkg->cust_bill_pkg_display ) {
927 # I think these are always setup fees, but just to be sure...
928 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
930 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
933 $invoice_data{finance_amount} =
934 sprintf('%.2f', sum( @finance_charges ) || 0);
935 $default_section->{subtotal} = $other_money_char.
936 sprintf('%.2f', sum( @charges ) || 0);
939 # previous invoice balances in the Previous Charges section if there
940 # is one, otherwise in the main detail section
941 # (except if summary_only is enabled, don't show them at all)
942 if ( $self->can('_items_previous') &&
943 $self->enable_previous &&
944 ! $conf->exists('previous_balance-summary_only') ) {
946 warn "$me adding previous balances\n"
949 foreach my $line_item ( $self->_items_previous ) {
952 ref => $line_item->{'pkgnum'},
953 pkgpart => $line_item->{'pkgpart'},
955 section => $previous_section, # which might be $default_section
956 description => &$escape_function($line_item->{'description'}),
957 ext_description => [ map { &$escape_function($_) }
958 @{ $line_item->{'ext_description'} || [] }
960 amount => ( $old_latex ? '' : $money_char).
961 $line_item->{'amount'},
962 product_code => $line_item->{'pkgpart'} || 'N/A',
965 push @detail_items, $detail;
966 push @buf, [ $detail->{'description'},
967 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
973 if ( @pr_cust_bill && $self->enable_previous ) {
974 push @buf, ['','-----------'];
975 push @buf, [ $self->mt('Total Previous Balance'),
976 $money_char. sprintf("%10.2f", $pr_total) ];
980 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
981 warn "$me adding DID summary\n"
984 my ($didsummary,$minutes) = $self->_did_summary;
985 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
987 { 'description' => $didsummary_desc,
988 'ext_description' => [ $didsummary, $minutes ],
992 foreach my $section (@sections, @$late_sections) {
994 # begin some normalization
995 $section->{'subtotal'} = $section->{'amount'}
997 && !exists($section->{subtotal})
998 && exists($section->{amount});
1000 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1001 if ( $invoice_data{finance_section} &&
1002 $section->{'description'} eq $invoice_data{finance_section} );
1004 $section->{'subtotal'} = $other_money_char.
1005 sprintf('%.2f', $section->{'subtotal'})
1008 # continue some normalization
1009 $section->{'amount'} = $section->{'subtotal'}
1013 if ( $section->{'description'} ) {
1014 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1019 warn "$me setting options\n"
1023 $options{'section'} = $section if $multisection;
1024 $options{'format'} = $format;
1025 $options{'escape_function'} = $escape_function;
1026 $options{'no_usage'} = 1 unless $unsquelched;
1027 $options{'unsquelched'} = $unsquelched;
1028 $options{'summary_page'} = $summarypage;
1029 $options{'skip_usage'} =
1030 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1032 warn "$me searching for line items\n"
1035 foreach my $line_item ( $self->_items_pkg(%options) ) {
1037 warn "$me adding line item $line_item\n"
1041 ext_description => [],
1043 $detail->{'ref'} = $line_item->{'pkgnum'};
1044 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
1045 $detail->{'quantity'} = $line_item->{'quantity'};
1046 $detail->{'section'} = $section;
1047 $detail->{'description'} = &$escape_function($line_item->{'description'});
1048 if ( exists $line_item->{'ext_description'} ) {
1049 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1051 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1052 $line_item->{'amount'};
1053 if ( exists $line_item->{'unit_amount'} ) {
1054 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1055 $line_item->{'unit_amount'};
1057 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1059 $detail->{'sdate'} = $line_item->{'sdate'};
1060 $detail->{'edate'} = $line_item->{'edate'};
1061 $detail->{'seconds'} = $line_item->{'seconds'};
1062 $detail->{'svc_label'} = $line_item->{'svc_label'};
1064 push @detail_items, $detail;
1065 push @buf, ( [ $detail->{'description'},
1066 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1068 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1072 if ( $section->{'description'} ) {
1073 push @buf, ( ['','-----------'],
1074 [ $section->{'description'}. ' sub-total',
1075 $section->{'subtotal'} # already formatted this
1084 $invoice_data{current_less_finance} =
1085 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1087 # if there's anything in the Previous Charges section, prepend it to the list
1088 if ( $pr_total and $previous_section ne $default_section ) {
1089 unshift @sections, $previous_section;
1092 warn "$me adding taxes\n"
1095 my @items_tax = $self->_items_tax;
1096 foreach my $tax ( @items_tax ) {
1098 $taxtotal += $tax->{'amount'};
1100 my $description = &$escape_function( $tax->{'description'} );
1101 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1103 if ( $multisection ) {
1105 my $money = $old_latex ? '' : $money_char;
1106 push @detail_items, {
1107 ext_description => [],
1110 description => $description,
1111 amount => $money. $amount,
1113 section => $tax_section,
1118 push @total_items, {
1119 'total_item' => $description,
1120 'total_amount' => $other_money_char. $amount,
1125 push @buf,[ $description,
1126 $money_char. $amount,
1133 $total->{'total_item'} = $self->mt('Sub-total');
1134 $total->{'total_amount'} =
1135 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1137 if ( $multisection ) {
1138 $tax_section->{'subtotal'} = $other_money_char.
1139 sprintf('%.2f', $taxtotal);
1140 $tax_section->{'pretotal'} = 'New charges sub-total '.
1141 $total->{'total_amount'};
1142 push @sections, $tax_section if $taxtotal;
1144 unshift @total_items, $total;
1147 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1149 push @buf,['','-----------'];
1150 push @buf,[$self->mt(
1151 (!$self->enable_previous)
1153 : 'Total New Charges'
1155 $money_char. sprintf("%10.2f",$self->charged) ];
1163 my %embolden_functions = (
1164 'latex' => sub { return '\textbf{'. shift(). '}' },
1165 'html' => sub { return '<b>'. shift(). '</b>' },
1166 'template' => sub { shift },
1168 my $embolden_function = $embolden_functions{$format};
1170 if ( $self->can('_items_total') ) { # quotations
1172 $self->_items_total(\@total_items);
1174 foreach ( @total_items ) {
1175 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1176 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1177 $_->{'total_amount'}
1181 } else { #normal invoice case
1183 # calculate total, possibly including total owed on previous
1187 $item = $conf->config('previous_balance-exclude_from_total')
1188 || 'Total New Charges'
1189 if $conf->exists('previous_balance-exclude_from_total');
1190 my $amount = $self->charged;
1191 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1192 $amount += $pr_total;
1195 $total->{'total_item'} = &$embolden_function($self->mt($item));
1196 $total->{'total_amount'} =
1197 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1198 if ( $multisection ) {
1199 if ( $adjust_section->{'sort_weight'} ) {
1200 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1201 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1203 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1204 $other_money_char. sprintf('%.2f', $self->charged );
1207 push @total_items, $total;
1209 push @buf,['','-----------'];
1212 sprintf( '%10.2f', $amount )
1216 # if we're showing previous invoices, also show previous
1217 # credits and payments
1218 if ( $self->enable_previous
1219 and $self->can('_items_credits')
1220 and $self->can('_items_payments') )
1222 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1225 my $credittotal = 0;
1226 foreach my $credit (
1227 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1231 $total->{'total_item'} = &$escape_function($credit->{'description'});
1232 $credittotal += $credit->{'amount'};
1233 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1234 $adjusttotal += $credit->{'amount'};
1235 if ( $multisection ) {
1236 my $money = $old_latex ? '' : $money_char;
1237 push @detail_items, {
1238 ext_description => [],
1241 description => &$escape_function($credit->{'description'}),
1242 amount => $money. $credit->{'amount'},
1244 section => $adjust_section,
1247 push @total_items, $total;
1251 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1254 foreach my $credit (
1255 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1257 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1261 my $paymenttotal = 0;
1262 foreach my $payment (
1263 $self->_items_payments( 'template' => $template )
1266 $total->{'total_item'} = &$escape_function($payment->{'description'});
1267 $paymenttotal += $payment->{'amount'};
1268 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1269 $adjusttotal += $payment->{'amount'};
1270 if ( $multisection ) {
1271 my $money = $old_latex ? '' : $money_char;
1272 push @detail_items, {
1273 ext_description => [],
1276 description => &$escape_function($payment->{'description'}),
1277 amount => $money. $payment->{'amount'},
1279 section => $adjust_section,
1282 push @total_items, $total;
1284 push @buf, [ $payment->{'description'},
1285 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1288 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1290 if ( $multisection ) {
1291 $adjust_section->{'subtotal'} = $other_money_char.
1292 sprintf('%.2f', $adjusttotal);
1293 push @sections, $adjust_section
1294 unless $adjust_section->{sort_weight};
1297 # create Balance Due message
1300 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1301 $total->{'total_amount'} =
1302 &$embolden_function(
1303 $other_money_char. sprintf('%.2f', #why? $summarypage
1304 # ? $self->charged +
1305 # $self->billing_balance
1307 $self->owed + $pr_total
1310 if ( $multisection && !$adjust_section->{sort_weight} ) {
1311 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1312 $total->{'total_amount'};
1314 push @total_items, $total;
1316 push @buf,['','-----------'];
1317 push @buf,[$self->balance_due_msg, $money_char.
1318 sprintf("%10.2f", $balance_due ) ];
1321 if ( $conf->exists('previous_balance-show_credit')
1322 and $cust_main->balance < 0 ) {
1323 my $credit_total = {
1324 'total_item' => &$embolden_function($self->credit_balance_msg),
1325 'total_amount' => &$embolden_function(
1326 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1329 if ( $multisection ) {
1330 $adjust_section->{'posttotal'} .= $newline_token .
1331 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1334 push @total_items, $credit_total;
1336 push @buf,['','-----------'];
1337 push @buf,[$self->credit_balance_msg, $money_char.
1338 sprintf("%10.2f", -$cust_main->balance ) ];
1342 } #end of default total adding ! can('_items_total')
1344 if ( $multisection ) {
1345 if ( $conf->exists('svc_phone_sections')
1346 && $self->can('_items_svc_phone_sections')
1350 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1351 $total->{'total_amount'} =
1352 &$embolden_function(
1353 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1355 my $last_section = pop @sections;
1356 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1357 $total->{'total_amount'};
1358 push @sections, $last_section;
1360 push @sections, @$late_sections
1364 # make a discounts-available section, even without multisection
1365 if ( $conf->exists('discount-show_available')
1366 and my @discounts_avail = $self->_items_discounts_avail ) {
1367 my $discount_section = {
1368 'description' => $self->mt('Discounts Available'),
1373 push @sections, $discount_section;
1374 push @detail_items, map { +{
1375 'ref' => '', #should this be something else?
1376 'section' => $discount_section,
1377 'description' => &$escape_function( $_->{description} ),
1378 'amount' => $money_char . &$escape_function( $_->{amount} ),
1379 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1380 } } @discounts_avail;
1383 my @summary_subtotals;
1384 # the templates say "$_->{tax_section} || !$_->{summarized}"
1385 # except 'summarized' is only true when tax_section is true, so this
1386 # is always true, so what's the deal?
1387 foreach my $s (@sections) {
1388 # not to include in the "summary of new charges" block:
1389 # finance charges, adjustments, previous charges,
1390 # and itemized phone usage sections
1391 if ( $s eq $adjust_section or
1392 ($s eq $previous_section and $s ne $default_section) or
1393 ($invoice_data{'finance_section'} and
1394 $invoice_data{'finance_section'} eq $s->{description}) or
1395 $s->{'description'} =~ /^\d+ $/ ) {
1398 push @summary_subtotals, $s;
1400 $invoice_data{summary_subtotals} = \@summary_subtotals;
1402 # debugging hook: call this with 'diag' => 1 to just get a hash of
1403 # the invoice variables
1404 return \%invoice_data if ( $params{'diag'} );
1406 # All sections and items are built; now fill in templates.
1407 my @includelist = ();
1408 push @includelist, 'summary' if $summarypage;
1409 foreach my $include ( @includelist ) {
1411 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1414 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1416 @inc_src = $conf->config($inc_file, $agentnum);
1420 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1422 my $convert_map = $convert_maps{$format}{$include};
1424 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1425 s/--\@\]/$delimiters{$format}[1]/g;
1428 &$convert_map( $conf->config($inc_file, $agentnum) );
1432 my $inc_tt = new Text::Template (
1434 SOURCE => [ map "$_\n", @inc_src ],
1435 DELIMITERS => $delimiters{$format},
1436 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1438 unless ( $inc_tt->compile() ) {
1439 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1440 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1444 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1446 $invoice_data{$include} =~ s/\n+$//
1447 if ($format eq 'latex');
1452 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1453 /invoice_lines\((\d*)\)/;
1454 $invoice_lines += $1 || scalar(@buf);
1457 die "no invoice_lines() functions in template?"
1458 if ( $format eq 'template' && !$wasfunc );
1460 if ($format eq 'template') {
1462 if ( $invoice_lines ) {
1463 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1464 $invoice_data{'total_pages'}++
1465 if scalar(@buf) % $invoice_lines;
1468 #setup subroutine for the template
1469 $invoice_data{invoice_lines} = sub {
1470 my $lines = shift || scalar(@buf);
1482 push @collect, split("\n",
1483 $text_template->fill_in( HASH => \%invoice_data )
1485 $invoice_data{'page'}++;
1487 map "$_\n", @collect;
1489 } else { # this is where we actually create the invoice
1491 warn "filling in template for invoice ". $self->invnum. "\n"
1493 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1496 $text_template->fill_in(HASH => \%invoice_data);
1500 sub notice_name { '('.shift->table.')'; }
1502 sub template_conf { 'invoice_'; }
1504 # helper routine for generating date ranges
1505 sub _prior_month30s {
1508 [ 1, 2592000 ], # 0-30 days ago
1509 [ 2592000, 5184000 ], # 30-60 days ago
1510 [ 5184000, 7776000 ], # 60-90 days ago
1511 [ 7776000, 0 ], # 90+ days ago
1514 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1515 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1520 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1522 Returns an postscript invoice, as a scalar.
1524 Options can be passed as a hashref (recommended) or as a list of time, template
1525 and then any key/value pairs for any other options.
1527 I<time> an optional value used to control the printing of overdue messages. The
1528 default is now. It isn't the date of the invoice; that's the `_date' field.
1529 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1530 L<Time::Local> and L<Date::Parse> for conversion functions.
1532 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1539 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1540 my $ps = generate_ps($file);
1542 unlink($barcodefile) if $barcodefile;
1547 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1549 Returns an PDF invoice, as a scalar.
1551 Options can be passed as a hashref (recommended) or as a list of time, template
1552 and then any key/value pairs for any other options.
1554 I<time> an optional value used to control the printing of overdue messages. The
1555 default is now. It isn't the date of the invoice; that's the `_date' field.
1556 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1557 L<Time::Local> and L<Date::Parse> for conversion functions.
1559 I<template>, if specified, is the name of a suffix for alternate invoices.
1561 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1568 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1569 my $pdf = generate_pdf($file);
1571 unlink($barcodefile) if $barcodefile;
1576 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1578 Returns an HTML invoice, as a scalar.
1580 I<time> an optional value used to control the printing of overdue messages. The
1581 default is now. It isn't the date of the invoice; that's the `_date' field.
1582 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1583 L<Time::Local> and L<Date::Parse> for conversion functions.
1585 I<template>, if specified, is the name of a suffix for alternate invoices.
1587 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1589 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1590 when emailing the invoice as part of a multipart/related MIME email.
1598 %params = %{ shift() };
1602 $params{'format'} = 'html';
1604 $self->print_generic( %params );
1607 # quick subroutine for print_latex
1609 # There are ten characters that LaTeX treats as special characters, which
1610 # means that they do not simply typeset themselves:
1611 # # $ % & ~ _ ^ \ { }
1613 # TeX ignores blanks following an escaped character; if you want a blank (as
1614 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1618 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1619 $value =~ s/([<>])/\$$1\$/g;
1625 encode_entities($value);
1629 sub _html_escape_nbsp {
1630 my $value = _html_escape(shift);
1631 $value =~ s/ +/ /g;
1635 #utility methods for print_*
1637 sub _translate_old_latex_format {
1638 warn "_translate_old_latex_format called\n"
1645 if ( $line =~ /^%%Detail\s*$/ ) {
1647 push @template, q![@--!,
1648 q! foreach my $_tr_line (@detail_items) {!,
1649 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1650 q! $_tr_line->{'description'} .= !,
1651 q! "\\tabularnewline\n~~".!,
1652 q! join( "\\tabularnewline\n~~",!,
1653 q! @{$_tr_line->{'ext_description'}}!,
1657 while ( ( my $line_item_line = shift )
1658 !~ /^%%EndDetail\s*$/ ) {
1659 $line_item_line =~ s/'/\\'/g; # nice LTS
1660 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1661 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1662 push @template, " \$OUT .= '$line_item_line';";
1665 push @template, '}',
1668 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1670 push @template, '[@--',
1671 ' foreach my $_tr_line (@total_items) {';
1673 while ( ( my $total_item_line = shift )
1674 !~ /^%%EndTotalDetails\s*$/ ) {
1675 $total_item_line =~ s/'/\\'/g; # nice LTS
1676 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1677 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1678 push @template, " \$OUT .= '$total_item_line';";
1681 push @template, '}',
1685 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1686 push @template, $line;
1692 warn "$_\n" foreach @template;
1700 my $conf = $self->conf;
1702 #check for an invoice-specific override
1703 return $self->invoice_terms if $self->invoice_terms;
1705 #check for a customer- specific override
1706 my $cust_main = $self->cust_main;
1707 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1709 #use configured default
1710 $conf->config('invoice_default_terms') || '';
1716 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1717 $duedate = $self->_date() + ( $1 * 86400 );
1724 $self->due_date ? time2str(shift, $self->due_date) : '';
1727 sub balance_due_msg {
1729 my $msg = $self->mt('Balance Due');
1730 return $msg unless $self->terms;
1731 if ( $self->due_date ) {
1732 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1733 $self->due_date2str($date_format);
1734 } elsif ( $self->terms ) {
1735 $msg .= ' - '. $self->terms;
1740 sub balance_due_date {
1742 my $conf = $self->conf;
1744 if ( $conf->exists('invoice_default_terms')
1745 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1746 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1751 sub credit_balance_msg {
1753 $self->mt('Credit Balance Remaining')
1758 Returns a string with the date, for example: "3/20/2008"
1764 time2str($date_format, $self->_date);
1767 =item _items_sections OPTIONS
1769 Generate section information for all items appearing on this invoice.
1770 This will only be called for multi-section invoices.
1772 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1773 related display records (L<FS::cust_bill_pkg_display>) and organize
1774 them into two groups ("early" and "late" according to whether they come
1775 before or after the total), then into sections. A subtotal is calculated
1778 Section descriptions are returned in sort weight order. Each consists
1779 of a hash containing:
1781 description: the package category name, escaped
1782 subtotal: the total charges in that section
1783 tax_section: a flag indicating that the section contains only tax charges
1784 summarized: same as tax_section, for some reason
1785 sort_weight: the package category's sort weight
1787 If 'condense' is set on the display record, it also contains everything
1788 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1789 coderefs to generate parts of the invoice. This is not advised.
1791 The method returns two arrayrefs, one of "early" sections and one of "late"
1794 OPTIONS may include:
1796 by_location: a flag to divide the invoice into sections by location.
1797 Each section hash will have a 'location' element containing a hashref of
1798 the location fields (see L<FS::cust_location>). The section description
1799 will be the location label, but the template can use any of the location
1800 fields to create a suitable label.
1802 by_category: a flag to divide the invoice into sections using display
1803 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1804 behavior. Each section hash will have a 'category' element containing
1805 the section name from the display record (which probably equals the
1806 category name of the package, but may not in some cases).
1808 summary: a flag indicating that this is a summary-format invoice.
1809 Turning this on has the following effects:
1810 - Ignores display items with the 'summary' flag.
1811 - Places all sections in the "early" group even if they have post_total.
1812 - Creates sections for all non-disabled package categories, even if they
1813 have no charges on this invoice, as well as a section with no name.
1815 escape: an escape function to use for section titles.
1817 extra_sections: an arrayref of additional sections to return after the
1818 sorted list. If there are any of these, section subtotals exclude
1821 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1822 passed through to C<_condense_section()>.
1826 use vars qw(%pkg_category_cache);
1827 sub _items_sections {
1831 my $escape = $opt{escape};
1832 my @extra_sections = @{ $opt{extra_sections} || [] };
1834 # $subtotal{$locationnum}{$categoryname} = amount.
1835 # if we're not using by_location, $locationnum is undef.
1836 # if we're not using by_category, you guessed it, $categoryname is undef.
1837 # if we're not using either one, we shouldn't be here in the first place...
1839 my %late_subtotal = ();
1842 # About tax items + multisection invoices:
1843 # If either invoice_*summary option is enabled, AND there is a
1844 # package category with the name of the tax, then there will be
1845 # a display record assigning the tax item to that category.
1847 # However, the taxes are always placed in the "Taxes, Surcharges,
1848 # and Fees" section regardless of that. The only effect of the
1849 # display record is to create a subtotal for the summary page.
1852 my $pkg_hash = $self->cust_pkg_hash;
1854 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1857 my $usage = $cust_bill_pkg->usage;
1860 if ( $opt{by_location} ) {
1861 if ( $cust_bill_pkg->pkgnum ) {
1862 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1867 $locationnum = undef;
1870 # as in _items_cust_pkg, if a line item has no display records,
1871 # cust_bill_pkg_display() returns a default record for it
1873 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1874 next if ( $display->summary && $opt{summary} );
1876 my $section = $display->section;
1877 my $type = $display->type;
1878 $section = undef unless $opt{by_category};
1880 $not_tax{$locationnum}{$section} = 1
1881 unless $cust_bill_pkg->pkgnum == 0;
1883 # there's actually a very important piece of logic buried in here:
1884 # incrementing $late_subtotal{$section} CREATES
1885 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1886 # to define the list of late sections, and likewise keys(%subtotal).
1887 # When _items_cust_bill_pkg is called to generate line items for
1888 # real, it will be called with 'section' => $section for each
1890 if ( $display->post_total && !$opt{summary} ) {
1891 if (! $type || $type eq 'S') {
1892 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1893 if $cust_bill_pkg->setup != 0
1894 || $cust_bill_pkg->setup_show_zero;
1898 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1899 if $cust_bill_pkg->recur != 0
1900 || $cust_bill_pkg->recur_show_zero;
1903 if ($type && $type eq 'R') {
1904 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1905 if $cust_bill_pkg->recur != 0
1906 || $cust_bill_pkg->recur_show_zero;
1909 if ($type && $type eq 'U') {
1910 $late_subtotal{$locationnum}{$section} += $usage
1911 unless scalar(@extra_sections);
1914 } else { # it's a pre-total (normal) section
1916 # skip tax items unless they're explicitly included in a section
1917 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1919 if (! $type || $type eq 'S') {
1920 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1921 if $cust_bill_pkg->setup != 0
1922 || $cust_bill_pkg->setup_show_zero;
1926 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1927 if $cust_bill_pkg->recur != 0
1928 || $cust_bill_pkg->recur_show_zero;
1931 if ($type && $type eq 'R') {
1932 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1933 if $cust_bill_pkg->recur != 0
1934 || $cust_bill_pkg->recur_show_zero;
1937 if ($type && $type eq 'U') {
1938 $subtotal{$locationnum}{$section} += $usage
1939 unless scalar(@extra_sections);
1948 %pkg_category_cache = ();
1950 # summary invoices need subtotals for all non-disabled package categories,
1951 # even if they're zero
1952 # but currently assume that there are no location sections, or at least
1953 # that the summary page doesn't care about them
1954 if ( $opt{summary} ) {
1955 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1956 $subtotal{''}{$category->categoryname} ||= 0;
1958 $subtotal{''}{''} ||= 0;
1962 foreach my $post_total (0,1) {
1964 my $s = $post_total ? \%late_subtotal : \%subtotal;
1965 foreach my $locationnum (keys %$s) {
1966 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1968 'subtotal' => $s->{$locationnum}{$sectionname},
1969 'post_total' => $post_total,
1972 if ( $locationnum ) {
1973 $section->{'locationnum'} = $locationnum;
1974 my $location = FS::cust_location->by_key($locationnum);
1975 $section->{'description'} = &{ $escape }($location->location_label);
1976 # Better ideas? This will roughly group them by proximity,
1977 # which alpha sorting on any of the address fields won't.
1978 # Sorting by locationnum is meaningless.
1979 # We have to sort on _something_ or the order may change
1980 # randomly from one invoice to the next, which will confuse
1982 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1984 $section->{'location'} = {
1985 map { $_ => &{ $escape }($location->get($_)) }
1989 $section->{'category'} = $sectionname;
1990 $section->{'description'} = &{ $escape }($sectionname);
1991 if ( _pkg_category($_) ) {
1992 $section->{'sort_weight'} = _pkg_category($_)->weight;
1993 if ( _pkg_category($_)->condense ) {
1994 $section = { %$section, $self->_condense_section($opt{format}) };
1998 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1999 # then it's a tax-only section
2000 $section->{'summarized'} = 'Y';
2001 $section->{'tax_section'} = 'Y';
2003 push @these, $section;
2004 } # foreach $sectionname
2005 } #foreach $locationnum
2006 push @these, @extra_sections if $post_total == 0;
2007 # need an alpha sort for location sections, because postal codes can
2009 $sections[ $post_total ] = [ sort {
2010 $opt{'by_location'} ?
2011 ($a->{sort_weight} cmp $b->{sort_weight}) :
2012 ($a->{sort_weight} <=> $b->{sort_weight})
2014 } #foreach $post_total
2016 return @sections; # early, late
2019 #helper subs for above
2023 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2027 my $categoryname = shift;
2028 $pkg_category_cache{$categoryname} ||=
2029 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2032 my %condensed_format = (
2033 'label' => [ qw( Description Qty Amount ) ],
2035 sub { shift->{description} },
2036 sub { shift->{quantity} },
2037 sub { my($href, %opt) = @_;
2038 ($opt{dollar} || ''). $href->{amount};
2041 'align' => [ qw( l r r ) ],
2042 'span' => [ qw( 5 1 1 ) ], # unitprices?
2043 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2046 sub _condense_section {
2047 my ( $self, $format ) = ( shift, shift );
2049 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2050 qw( description_generator
2053 total_line_generator
2058 sub _condensed_generator_defaults {
2059 my ( $self, $format ) = ( shift, shift );
2060 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2069 sub _condensed_header_generator {
2070 my ( $self, $format ) = ( shift, shift );
2072 my ( $f, $prefix, $suffix, $separator, $column ) =
2073 _condensed_generator_defaults($format);
2075 if ($format eq 'latex') {
2076 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2077 $suffix = "\\\\\n\\hline";
2080 sub { my ($d,$a,$s,$w) = @_;
2081 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2083 } elsif ( $format eq 'html' ) {
2084 $prefix = '<th></th>';
2088 sub { my ($d,$a,$s,$w) = @_;
2089 return qq!<th align="$html_align{$a}">$d</th>!;
2097 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2099 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2102 $prefix. join($separator, @result). $suffix;
2107 sub _condensed_description_generator {
2108 my ( $self, $format ) = ( shift, shift );
2110 my ( $f, $prefix, $suffix, $separator, $column ) =
2111 _condensed_generator_defaults($format);
2113 my $money_char = '$';
2114 if ($format eq 'latex') {
2115 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2117 $separator = " & \n";
2119 sub { my ($d,$a,$s,$w) = @_;
2120 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2122 $money_char = '\\dollar';
2123 }elsif ( $format eq 'html' ) {
2124 $prefix = '"><td align="center"></td>';
2128 sub { my ($d,$a,$s,$w) = @_;
2129 return qq!<td align="$html_align{$a}">$d</td>!;
2131 #$money_char = $conf->config('money_char') || '$';
2132 $money_char = ''; # this is madness
2140 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2142 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2144 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2145 map { $f->{$_}->[$i] } qw(align span width)
2149 $prefix. join( $separator, @result ). $suffix;
2154 sub _condensed_total_generator {
2155 my ( $self, $format ) = ( shift, shift );
2157 my ( $f, $prefix, $suffix, $separator, $column ) =
2158 _condensed_generator_defaults($format);
2161 if ($format eq 'latex') {
2164 $separator = " & \n";
2166 sub { my ($d,$a,$s,$w) = @_;
2167 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2169 }elsif ( $format eq 'html' ) {
2173 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2175 sub { my ($d,$a,$s,$w) = @_;
2176 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2185 # my $r = &{$f->{fields}->[$i]}(@args);
2186 # $r .= ' Total' unless $i;
2188 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2190 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2191 map { $f->{$_}->[$i] } qw(align span width)
2195 $prefix. join( $separator, @result ). $suffix;
2200 =item total_line_generator FORMAT
2202 Returns a coderef used for generation of invoice total line items for this
2203 usage_class. FORMAT is either html or latex
2207 # should not be used: will have issues with hash element names (description vs
2208 # total_item and amount vs total_amount -- another array of functions?
2210 sub _condensed_total_line_generator {
2211 my ( $self, $format ) = ( shift, shift );
2213 my ( $f, $prefix, $suffix, $separator, $column ) =
2214 _condensed_generator_defaults($format);
2217 if ($format eq 'latex') {
2220 $separator = " & \n";
2222 sub { my ($d,$a,$s,$w) = @_;
2223 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2225 }elsif ( $format eq 'html' ) {
2229 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2231 sub { my ($d,$a,$s,$w) = @_;
2232 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2241 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2243 &{$column}( &{$f->{fields}->[$i]}(@args),
2244 map { $f->{$_}->[$i] } qw(align span width)
2248 $prefix. join( $separator, @result ). $suffix;
2253 =item _items_pkg [ OPTIONS ]
2255 Return line item hashes for each package item on this invoice. Nearly
2258 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2260 The only OPTIONS accepted is 'section', which may point to a hashref
2261 with a key named 'condensed', which may have a true value. If it
2262 does, this method tries to merge identical items into items with
2263 'quantity' equal to the number of items (not the sum of their
2264 separate quantities, for some reason).
2270 grep { $_->pkgnum } $self->cust_bill_pkg;
2277 warn "$me _items_pkg searching for all package line items\n"
2280 my @cust_bill_pkg = $self->_items_nontax;
2282 warn "$me _items_pkg filtering line items\n"
2284 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2286 if ($options{section} && $options{section}->{condensed}) {
2288 warn "$me _items_pkg condensing section\n"
2292 local $Storable::canonical = 1;
2293 foreach ( @items ) {
2295 delete $item->{ref};
2296 delete $item->{ext_description};
2297 my $key = freeze($item);
2298 $itemshash{$key} ||= 0;
2299 $itemshash{$key} ++; # += $item->{quantity};
2301 @items = sort { $a->{description} cmp $b->{description} }
2302 map { my $i = thaw($_);
2303 $i->{quantity} = $itemshash{$_};
2305 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2311 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2318 return 0 unless $a->itemdesc cmp $b->itemdesc;
2319 return -1 if $b->itemdesc eq 'Tax';
2320 return 1 if $a->itemdesc eq 'Tax';
2321 return -1 if $b->itemdesc eq 'Other surcharges';
2322 return 1 if $a->itemdesc eq 'Other surcharges';
2323 $a->itemdesc cmp $b->itemdesc;
2328 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2329 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2331 if ( $self->conf->exists('always_show_tax') ) {
2332 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2333 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2335 { 'description' => $itemdesc,
2342 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2344 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2345 list of hashrefs describing the line items they generate on the invoice.
2347 OPTIONS may include:
2349 format: the invoice format.
2351 escape_function: the function used to escape strings.
2353 DEPRECATED? (expensive, mostly unused?)
2354 format_function: the function used to format CDRs.
2356 section: a hashref containing 'category' and/or 'locationnum'; if this
2357 is present, only returns line items that belong to that category and/or
2358 location (whichever is defined).
2360 multisection: a flag indicating that this is a multisection invoice,
2361 which does something complicated.
2363 Returns a list of hashrefs, each of which may contain:
2365 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2366 ext_description, which is an arrayref of detail lines to show below
2371 sub _items_cust_bill_pkg {
2373 my $conf = $self->conf;
2374 my $cust_bill_pkgs = shift;
2377 my $format = $opt{format} || '';
2378 my $escape_function = $opt{escape_function} || sub { shift };
2379 my $format_function = $opt{format_function} || '';
2380 my $no_usage = $opt{no_usage} || '';
2381 my $unsquelched = $opt{unsquelched} || ''; #unused
2382 my ($section, $locationnum, $category);
2383 if ( $opt{section} ) {
2384 $category = $opt{section}->{category};
2385 $locationnum = $opt{section}->{locationnum};
2387 my $summary_page = $opt{summary_page} || ''; #unused
2388 my $multisection = defined($category) || defined($locationnum);
2389 my $discount_show_always = 0;
2391 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2393 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2394 # and location labels
2397 my ($s, $r, $u) = ( undef, undef, undef );
2398 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2401 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2402 if ( $_ && !$cust_bill_pkg->hidden ) {
2403 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2404 $_->{amount} =~ s/^\-0\.00$/0.00/;
2405 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2407 if $_->{amount} != 0
2408 || $discount_show_always
2409 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2410 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2416 if ( $locationnum ) {
2417 # this is a location section; skip packages that aren't at this
2419 next if $cust_bill_pkg->pkgnum == 0;
2420 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2424 # Consider display records for this item to determine if it belongs
2425 # in this section. Note that if there are no display records, there
2426 # will be a default pseudo-record that includes all charge types
2427 # and has no section name.
2428 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2429 ? $cust_bill_pkg->cust_bill_pkg_display
2430 : ( $cust_bill_pkg );
2432 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2433 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2436 if ( defined($category) ) {
2437 # then this is a package category section; process all display records
2438 # that belong to this section.
2439 @cust_bill_pkg_display = grep { $_->section eq $category }
2440 @cust_bill_pkg_display;
2442 # otherwise, process all display records that aren't usage summaries
2443 # (I don't think there should be usage summaries if you aren't using
2444 # category sections, but this is the historical behavior)
2445 @cust_bill_pkg_display = grep { !$_->summary }
2446 @cust_bill_pkg_display;
2448 foreach my $display (@cust_bill_pkg_display) {
2450 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2451 $display->billpkgdisplaynum. "\n"
2454 my $type = $display->type;
2456 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2457 $desc = substr($desc, 0, $maxlength). '...'
2458 if $format eq 'latex' && length($desc) > $maxlength;
2460 my %details_opt = ( 'format' => $format,
2461 'escape_function' => $escape_function,
2462 'format_function' => $format_function,
2463 'no_usage' => $opt{'no_usage'},
2466 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2468 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2471 if ( $cust_bill_pkg->setup != 0 ) {
2472 my $description = $desc;
2473 $description .= ' Setup'
2474 if $cust_bill_pkg->recur != 0
2475 || $discount_show_always
2476 || $cust_bill_pkg->recur_show_zero;
2478 'description' => $description,
2479 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2482 if ( $cust_bill_pkg->recur != 0 ) {
2484 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2485 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2489 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2491 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2494 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2495 my $part_pkg = $cust_pkg->part_pkg;
2497 # which pkgpart to show for display purposes?
2498 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2500 # start/end dates for invoice formats that do nonstandard
2502 my %item_dates = ();
2503 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2504 unless $part_pkg->option('disable_line_item_date_ranges',1);
2506 if ( (!$type || $type eq 'S')
2507 && ( $cust_bill_pkg->setup != 0
2508 || $cust_bill_pkg->setup_show_zero
2513 warn "$me _items_cust_bill_pkg adding setup\n"
2516 my $description = $desc;
2517 $description .= ' Setup'
2518 if $cust_bill_pkg->recur != 0
2519 || $discount_show_always
2520 || $cust_bill_pkg->recur_show_zero;
2522 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2524 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2525 # triggered by the recurring charge freq
2527 && $cust_bill_pkg->recur == 0
2528 && ! $cust_bill_pkg->recur_show_zero;
2532 unless ( $cust_pkg->part_pkg->hide_svc_detail
2533 || $cust_bill_pkg->hidden )
2536 my @svc_labels = map &{$escape_function}($_),
2537 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2538 push @d, @svc_labels
2539 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2540 $svc_label = $svc_labels[0];
2542 my $lnum = $cust_main ? $cust_main->ship_locationnum
2543 : $self->prospect_main->locationnum;
2544 # show the location label if it's not the customer's default
2545 # location, and we're not grouping items by location already
2546 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2547 my $loc = $cust_pkg->location_label;
2548 $loc = substr($loc, 0, $maxlength). '...'
2549 if $format eq 'latex' && length($loc) > $maxlength;
2550 push @d, &{$escape_function}($loc);
2553 } #unless hiding service details
2555 push @d, $cust_bill_pkg->details(%details_opt)
2556 if $cust_bill_pkg->recur == 0;
2558 if ( $cust_bill_pkg->hidden ) {
2559 $s->{amount} += $cust_bill_pkg->setup;
2560 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2561 push @{ $s->{ext_description} }, @d;
2565 description => $description,
2566 pkgpart => $pkgpart,
2567 pkgnum => $cust_bill_pkg->pkgnum,
2568 amount => $cust_bill_pkg->setup,
2569 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2570 unit_amount => $cust_bill_pkg->unitsetup,
2571 quantity => $cust_bill_pkg->quantity,
2572 ext_description => \@d,
2573 svc_label => ($svc_label || ''),
2579 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2581 $cust_bill_pkg->recur != 0
2582 || $cust_bill_pkg->setup == 0
2583 || $discount_show_always
2584 || $cust_bill_pkg->recur_show_zero
2589 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2592 my $is_summary = $display->summary;
2593 my $description = $desc;
2594 if ( $type eq 'U' and ($is_summary or $cust_bill_pkg->hidden) ) {
2595 $description = $self->mt('Usage charges');
2598 my $part_pkg = $cust_pkg->part_pkg;
2600 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2604 my @seconds = (); # for display of usage info
2607 #at least until cust_bill_pkg has "past" ranges in addition to
2608 #the "future" sdate/edate ones... see #3032
2609 my @dates = ( $self->_date );
2610 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2611 push @dates, $prev->sdate if $prev;
2612 push @dates, undef if !$prev;
2614 unless ( $part_pkg->hide_svc_detail
2615 || $cust_bill_pkg->itemdesc
2616 || $cust_bill_pkg->hidden
2617 || $is_summary && $type && $type eq 'U'
2621 warn "$me _items_cust_bill_pkg adding service details\n"
2624 my @svc_labels = map &{$escape_function}($_),
2625 $cust_pkg->h_labels_short(@dates, 'I');
2626 push @d, @svc_labels
2627 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2628 $svc_label = $svc_labels[0];
2630 warn "$me _items_cust_bill_pkg done adding service details\n"
2633 my $lnum = $cust_main ? $cust_main->ship_locationnum
2634 : $self->prospect_main->locationnum;
2635 # show the location label if it's not the customer's default
2636 # location, and we're not grouping items by location already
2637 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2638 my $loc = $cust_pkg->location_label;
2639 $loc = substr($loc, 0, $maxlength). '...'
2640 if $format eq 'latex' && length($loc) > $maxlength;
2641 push @d, &{$escape_function}($loc);
2644 # Display of seconds_since_sqlradacct:
2645 # On the invoice, when processing @detail_items, look for a field
2646 # named 'seconds'. This will contain total seconds for each
2647 # service, in the same order as @ext_description. For services
2648 # that don't support this it will show undef.
2649 if ( $conf->exists('svc_acct-usage_seconds')
2650 and ! $cust_bill_pkg->pkgpart_override ) {
2651 foreach my $cust_svc (
2652 $cust_pkg->h_cust_svc(@dates, 'I')
2655 # eval because not having any part_export_usage exports
2656 # is a fatal error, last_bill/_date because that's how
2657 # sqlradius_hour billing does it
2659 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2661 push @seconds, $sec;
2663 } #if svc_acct-usage_seconds
2667 unless ( $is_summary ) {
2668 warn "$me _items_cust_bill_pkg adding details\n"
2671 #instead of omitting details entirely in this case (unwanted side
2672 # effects), just omit CDRs
2673 $details_opt{'no_usage'} = 1
2674 if $type && $type eq 'R';
2676 push @d, $cust_bill_pkg->details(%details_opt);
2679 warn "$me _items_cust_bill_pkg calculating amount\n"
2684 $amount = $cust_bill_pkg->recur;
2685 } elsif ($type eq 'R') {
2686 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2687 } elsif ($type eq 'U') {
2688 $amount = $cust_bill_pkg->usage;
2692 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2695 if ( !$type || $type eq 'R' ) {
2697 warn "$me _items_cust_bill_pkg adding recur\n"
2700 if ( $cust_bill_pkg->hidden ) {
2701 $r->{amount} += $amount;
2702 $r->{unit_amount} += $unit_amount;
2703 push @{ $r->{ext_description} }, @d;
2706 description => $description,
2707 pkgpart => $pkgpart,
2708 pkgnum => $cust_bill_pkg->pkgnum,
2710 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2711 unit_amount => $unit_amount,
2712 quantity => $cust_bill_pkg->quantity,
2714 ext_description => \@d,
2715 svc_label => ($svc_label || ''),
2717 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2720 } else { # $type eq 'U'
2722 warn "$me _items_cust_bill_pkg adding usage\n"
2725 if ( $cust_bill_pkg->hidden and defined($u) ) {
2726 # if this is a hidden package and there's already a usage
2727 # line for the bundle, add this package's total amount and
2728 # usage details to it
2729 $u->{amount} += $amount;
2730 $u->{unit_amount} += $unit_amount,
2731 push @{ $u->{ext_description} }, @d;
2732 } elsif ( $amount ) {
2733 # create a new usage line
2735 description => $description,
2736 pkgpart => $pkgpart,
2737 pkgnum => $cust_bill_pkg->pkgnum,
2739 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2740 unit_amount => $unit_amount,
2741 quantity => $cust_bill_pkg->quantity,
2743 ext_description => \@d,
2745 } # else this has no usage, so don't create a usage section
2748 } # recurring or usage with recurring charge
2750 } else { #pkgnum tax or one-shot line item (??)
2752 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2755 if ( $cust_bill_pkg->setup != 0 ) {
2757 'description' => $desc,
2758 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2761 if ( $cust_bill_pkg->recur != 0 ) {
2763 'description' => "$desc (".
2764 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2765 time2str($date_format, $cust_bill_pkg->edate). ')',
2766 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2774 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2775 && $conf->exists('discount-show-always'));
2779 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2781 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2782 $_->{amount} =~ s/^\-0\.00$/0.00/;
2783 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2785 if $_->{amount} != 0
2786 || $discount_show_always
2787 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2788 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2792 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2799 =item _items_discounts_avail
2801 Returns an array of line item hashrefs representing available term discounts
2802 for this invoice. This makes the same assumptions that apply to term
2803 discounts in general: that the package is billed monthly, at a flat rate,
2804 with no usage charges. A prorated first month will be handled, as will
2805 a setup fee if the discount is allowed to apply to setup fees.
2809 sub _items_discounts_avail {
2812 #maybe move this method from cust_bill when quotations support discount_plans
2813 return () unless $self->can('discount_plans');
2814 my %plans = $self->discount_plans;
2816 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2817 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2821 my $plan = $plans{$months};
2823 my $term_total = sprintf('%.2f', $plan->discounted_total);
2824 my $percent = sprintf('%.0f',
2825 100 * (1 - $term_total / $plan->base_total) );
2826 my $permonth = sprintf('%.2f', $term_total / $months);
2827 my $detail = $self->mt('discount on item'). ' '.
2828 join(', ', map { "#$_" } $plan->pkgnums)
2831 # discounts for non-integer months don't work anyway
2832 $months = sprintf("%d", $months);
2835 description => $self->mt('Save [_1]% by paying for [_2] months',
2837 amount => $self->mt('[_1] ([_2] per month)',
2838 $term_total, $money_char.$permonth),
2839 ext_description => ($detail || ''),
2842 sort { $b <=> $a } keys %plans;