1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
9 use vars qw( $invoice_lines @buf ); #yuck
10 use List::Util qw(sum);
13 use Text::Template 1.20;
19 use FS::Record qw( qsearch qsearchs );
21 use FS::Misc qw( generate_ps generate_pdf );
28 $me = '[FS::Template_Mixin]';
29 FS::UID->install_callback( sub {
30 my $conf = new FS::Conf; #global
31 $money_char = $conf->config('money_char') || '$';
32 $date_format = $conf->config('date_format') || '%x'; #/YY
37 Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
39 If the "mode" pseudo-field is set on the object, the configuration handle
40 will be an L<FS::invoice_conf> for that invoice mode (and the customer's
47 my $mode = $self->get('mode');
48 if ($self->{_conf} and !defined($mode)) {
49 return $self->{_conf};
52 my $cust_main = $self->cust_main;
53 my $locale = $cust_main ? $cust_main->locale : '';
56 if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
57 $mode = $mode->modenum;
58 } elsif ( $mode =~ /\D/ ) {
59 die "invalid invoice mode $mode";
61 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
63 $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
64 # it doesn't have a locale, but system conf still might
65 $conf->set('locale' => $locale) if $conf;
68 # if $mode is unspecified, or if there is no invoice_conf matching this mode
69 # and locale, then use the system config only (but with the locale)
70 $conf ||= FS::Conf->new({ 'locale' => $locale });
72 return $self->{_conf} = $conf;
75 =item print_text OPTIONS
77 Returns an text invoice, as a list of lines.
79 Options can be passed as a hash.
81 I<time>, if specified, is used to control the printing of overdue messages. The
82 default is now. It isn't the date of the invoice; that's the `_date' field.
83 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
84 L<Time::Local> and L<Date::Parse> for conversion functions.
86 I<template>, if specified, is the name of a suffix for alternate invoices.
88 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
96 %params = %{ shift() };
101 $params{'format'} = 'template'; # for some reason
103 $self->print_generic( %params );
106 =item print_latex HASHREF
108 Internal method - returns a filename of a filled-in LaTeX template for this
109 invoice (Note: add ".tex" to get the actual filename), and a filename of
110 an associated logo (with the .eps extension included).
112 See print_ps and print_pdf for methods that return PostScript and PDF output.
114 Options can be passed as a hash.
116 I<time>, if specified, is used to control the printing of overdue messages. The
117 default is now. It isn't the date of the invoice; that's the `_date' field.
118 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
119 L<Time::Local> and L<Date::Parse> for conversion functions.
121 I<template>, if specified, is the name of a suffix for alternate invoices.
122 This is strongly deprecated; see L<FS::invoice_conf> for the right way to
123 customize invoice templates for different purposes.
125 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
134 %params = %{ shift() };
139 $params{'format'} = 'latex';
140 my $conf = $self->conf;
142 # this needs to go away
143 my $template = $params{'template'};
144 # and this especially
145 $template ||= $self->_agent_template
146 if $self->can('_agent_template');
148 my $pkey = $self->primary_key;
149 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
151 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
152 my $lh = new File::Temp(
153 TEMPLATE => $tmp_template,
157 ) or die "can't open temp file: $!\n";
159 my $agentnum = $self->agentnum;
161 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
162 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
163 or die "can't write temp file: $!\n";
165 print $lh $conf->config_binary('logo.eps', $agentnum)
166 or die "can't write temp file: $!\n";
169 $params{'logo_file'} = $lh->filename;
171 if( $conf->exists('invoice-barcode')
172 && $self->can('invoice_barcode')
173 && $self->invnum ) { # don't try to barcode statements
174 my $png_file = $self->invoice_barcode($dir);
175 my $eps_file = $png_file;
176 $eps_file =~ s/\.png$/.eps/g;
177 $png_file =~ /(barcode.*png)/;
179 $eps_file =~ /(barcode.*eps)/;
182 my $curr_dir = cwd();
184 # after painfuly long experimentation, it was determined that sam2p won't
185 # accept : and other chars in the path, no matter how hard I tried to
186 # escape them, hence the chdir (and chdir back, just to be safe)
187 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
188 or die "sam2p failed: $!\n";
192 $params{'barcode_file'} = $eps_file;
195 my @filled_in = $self->print_generic( %params );
197 my $fh = new File::Temp( TEMPLATE => $tmp_template,
201 ) or die "can't open temp file: $!\n";
202 binmode($fh, ':utf8'); # language support
203 print $fh join('', @filled_in );
206 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
207 return ($1, $params{'logo_file'}, $params{'barcode_file'});
213 my $cust_main = $self->cust_main;
214 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
217 =item print_generic OPTION => VALUE ...
219 Internal method - returns a filled-in template for this invoice as a scalar.
221 See print_ps and print_pdf for methods that return PostScript and PDF output.
229 The B<format> option is required and should be set to html, latex (print and PDF) or template (plaintext).
239 Overrides "Invoice" as the name of the sent document.
243 Used to control the printing of overdue messages. The
244 default is now. It isn't the date of the invoice; that's the `_date' field.
245 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
246 L<Time::Local> and L<Date::Parse> for conversion functions.
250 Logo file (path to temporary EPS file on the local filesystem)
254 CID for inline (emailed) images (logo)
258 Override customer's locale
262 Overrides any per customer cdr squelching when true
266 Supress the (invoice, quotation, statement, etc.) number
274 Supress the payment coupon
278 Barcode file (path to temporary EPS file on the local filesystem)
282 Flag indicating the barcode image should be a link (normal HTML dipaly)
286 Barcode CID for inline (emailed) images
288 =item preref_callback
290 Coderef run for each line item, code should return HTML to be displayed
291 before that line item (quotations only)
295 Dprecated. Used as a suffix for a configuration template. Please
296 don't use this, it deprecated in favor of more flexible alternatives.
302 #what's with all the sprintf('%10.2f')'s in here? will it cause any
303 # (alignment in text invoice?) problems to change them all to '%.2f' ?
304 # yes: fixed width/plain text printing will be borked
306 my( $self, %params ) = @_;
307 my $conf = $self->conf;
309 my $today = $params{today} ? $params{today} : time;
310 warn "$me print_generic called on $self with suffix $params{template}\n"
313 my $format = $params{format};
314 die "Unknown format: $format"
315 unless $format =~ /^(latex|html|template)$/;
317 my $cust_main = $self->cust_main || $self->prospect_main;
318 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
319 unless $cust_main->payname
320 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
322 my $locale = $params{'locale'} || $cust_main->locale;
324 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
325 'html' => [ '<%=', '%>' ],
326 'template' => [ '{', '}' ],
329 warn "$me print_generic creating template\n"
332 # set the notice name here, and nowhere else.
333 my $notice_name = $params{notice_name}
334 || $conf->config('notice_name')
335 || $self->notice_name;
338 my $template = $params{template} ? $params{template} : $self->_agent_template;
339 my $templatefile = $self->template_conf. $format;
340 $templatefile .= "_$template"
341 if length($template) && $conf->exists($templatefile."_$template");
344 my @invoice_template = map "$_\n", $conf->config($templatefile)
345 or die "cannot load config data $templatefile";
347 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
348 #change this to a die when the old code is removed
349 # it's been almost ten years, changing it to a die.
350 die "old-style invoice template $templatefile; ".
351 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
352 #$old_latex = 'true';
353 #@invoice_template = _translate_old_latex_format(@invoice_template);
356 warn "$me print_generic creating T:T object\n"
359 my $text_template = new Text::Template(
361 SOURCE => \@invoice_template,
362 DELIMITERS => $delimiters{$format},
365 warn "$me print_generic compiling T:T object\n"
368 $text_template->compile()
369 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
372 # additional substitution could possibly cause breakage in existing templates
375 'notes' => sub { map "$_", @_ },
376 'footer' => sub { map "$_", @_ },
377 'smallfooter' => sub { map "$_", @_ },
378 'returnaddress' => sub { map "$_", @_ },
379 'coupon' => sub { map "$_", @_ },
380 'summary' => sub { map "$_", @_ },
386 s/%%(.*)$/<!-- $1 -->/g;
387 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
388 s/\\begin\{enumerate\}/<ol>/g;
390 s/\\end\{enumerate\}/<\/ol>/g;
391 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
400 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
402 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
408 s/\\hyphenation\{[\w\s\-]+}//;
413 'coupon' => sub { "" },
414 'summary' => sub { "" },
421 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
422 s/\\begin\{enumerate\}//g;
424 s/\\end\{enumerate\}//g;
425 s/\\textbf\{(.*)\}/$1/g;
432 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
434 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
439 s/\\\\\*?\s*$/\n/; # dubious
440 s/\\hyphenation\{[\w\s\-]+}//;
444 'coupon' => sub { "" },
445 'summary' => sub { "" },
450 # hashes for differing output formats
451 my %nbsps = ( 'latex' => '~',
452 'html' => '', # '&nbps;' would be nice
453 'template' => '', # not used
455 my $nbsp = $nbsps{$format};
457 my %escape_functions = ( 'latex' => \&_latex_escape,
458 'html' => \&_html_escape_nbsp,#\&encode_entities,
459 'template' => sub { shift },
461 my $escape_function = $escape_functions{$format};
462 my $escape_function_nonbsp = ($format eq 'html')
463 ? \&_html_escape : $escape_function;
465 my %newline_tokens = ( 'latex' => '\\\\',
469 my $newline_token = $newline_tokens{$format};
471 warn "$me generating template variables\n"
474 # generate template variables
478 defined( $conf->config_orbase( "invoice_${format}returnaddress",
482 && length( $conf->config_orbase( "invoice_${format}returnaddress",
488 $returnaddress = join("\n",
489 $conf->config_orbase("invoice_${format}returnaddress", $template)
493 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
495 my $convert_map = $convert_maps{$format}{'returnaddress'};
498 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
503 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
505 my $convert_map = $convert_maps{$format}{'returnaddress'};
506 $returnaddress = join( "\n", &$convert_map(
507 map { s/( {2,})/'~' x length($1)/eg;
511 ( $conf->config('company_name', $cust_main->agentnum),
512 $conf->config('company_address', $cust_main->agentnum),
519 my $warning = "Couldn't find a return address; ".
520 "do you need to set the company_address configuration value?";
522 $returnaddress = $nbsp;
523 #$returnaddress = $warning;
527 warn "$me generating invoice data\n"
530 my $agentnum = $cust_main->agentnum;
535 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
536 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
537 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
538 'returnaddress' => $returnaddress,
539 'agent' => &$escape_function($cust_main->agent->agent),
541 #invoice/quotation info
542 'no_number' => $params{'no_number'},
543 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
544 'quotationnum' => $self->quotationnum,
545 'no_date' => $params{'no_date'},
546 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
547 # workaround for inconsistent behavior in the early plain text
548 # templates; see RT#28271
549 'date' => ( $params{'no_date'}
551 : ($format eq 'template'
553 : $self->time2str_local('long', $self->_date, $format)
556 'today' => $self->time2str_local('long', $today, $format),
557 'terms' => $self->terms,
558 'template' => $template, #params{'template'},
559 'notice_name' => $notice_name, # escape?
560 'current_charges' => sprintf("%.2f", $self->charged),
561 'duedate' => $self->due_date2str('rdate'), #date_format?
564 'custnum' => $cust_main->display_custnum,
565 'prospectnum' => $cust_main->prospectnum,
566 'agent_custid' => &$escape_function($cust_main->agent_custid),
567 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
568 payname company address1 address2 city state zip fax
572 'ship_enable' => $conf->exists('invoice-ship_address'),
573 'unitprices' => $conf->exists('invoice-unitprice'),
574 'smallernotes' => $conf->exists('invoice-smallernotes'),
575 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
576 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
578 #layout info -- would be fancy to calc some of this and bury the template
580 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
581 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
582 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
583 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
584 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
585 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
586 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
587 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
588 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
589 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
591 # better hang on to conf_dir for a while (for old templates)
592 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
594 #these are only used when doing paged plaintext
601 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
602 # prototype here to silence warnings
603 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
605 my $min_sdate = 999999999999;
607 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
608 next unless $cust_bill_pkg->pkgnum > 0;
609 $min_sdate = $cust_bill_pkg->sdate
610 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
611 $max_edate = $cust_bill_pkg->edate
612 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
615 $invoice_data{'bill_period'} = '';
616 $invoice_data{'bill_period'} =
617 $self->time2str_local('%e %h', $min_sdate, $format)
619 $self->time2str_local('%e %h', $max_edate, $format)
620 if ($max_edate != 0 && $min_sdate != 999999999999);
622 $invoice_data{finance_section} = '';
623 if ( $conf->config('finance_pkgclass') ) {
625 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
626 $invoice_data{finance_section} = $pkg_class->categoryname;
628 $invoice_data{finance_amount} = '0.00';
629 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
631 my $countrydefault = $conf->config('countrydefault') || 'US';
632 foreach ( qw( address1 address2 city state zip country fax) ){
633 my $method = 'ship_'.$_;
634 $invoice_data{"ship_$_"} = $escape_function->($cust_main->$method);
636 if ( length($cust_main->ship_company) ) {
637 $invoice_data{'ship_company'} = $escape_function->($cust_main->ship_company);
639 $invoice_data{'ship_company'} = $escape_function->($cust_main->company);
641 $invoice_data{'ship_contact'} = $escape_function->($cust_main->contact);
642 $invoice_data{'ship_country'} = ''
643 if ( $invoice_data{'ship_country'} eq $countrydefault );
645 $invoice_data{'cid'} = $params{'cid'}
648 if ( $cust_main->country eq $countrydefault ) {
649 $invoice_data{'country'} = '';
651 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
655 $invoice_data{'address'} = \@address;
658 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
659 ? " (P.O. #". $cust_main->payinfo. ")"
663 push @address, $cust_main->company
664 if $cust_main->company;
665 push @address, $cust_main->address1;
666 push @address, $cust_main->address2
667 if $cust_main->address2;
669 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
670 push @address, $invoice_data{'country'}
671 if $invoice_data{'country'};
673 while (scalar(@address) < 5);
675 $invoice_data{'logo_file'} = $params{'logo_file'}
676 if $params{'logo_file'};
677 $invoice_data{'barcode_file'} = $params{'barcode_file'}
678 if $params{'barcode_file'};
679 $invoice_data{'barcode_img'} = $params{'barcode_img'}
680 if $params{'barcode_img'};
681 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
682 if $params{'barcode_cid'};
684 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
685 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
686 #my $balance_due = $self->owed + $pr_total - $cr_total;
687 my $balance_due = $self->owed + $pr_total;
689 # the sum of amount owed on all invoices
690 # (this is used in the summary & on the payment coupon)
691 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
693 # info from customer's last invoice before this one, for some
695 $invoice_data{'last_bill'} = {};
697 if ( $self->custnum && $self->invnum ) {
699 my $last_bill = $self->previous_bill;
702 # "balance_date_range" unfortunately is unsuitable for this, since it
703 # cares about application dates. We want to know the sum of all
704 # _top-level transactions_ dated before the last invoice.
706 'SELECT SUM(charged) FROM cust_bill WHERE _date <= ? AND custnum = ?',
707 'SELECT -1*SUM(amount) FROM cust_credit WHERE _date <= ? AND custnum = ?',
708 'SELECT -1*SUM(paid) FROM cust_pay WHERE _date <= ? AND custnum = ?',
709 'SELECT SUM(refund) FROM cust_refund WHERE _date <= ? AND custnum = ?',
712 # the customer's current balance immediately after generating the last
715 my $last_bill_balance = $last_bill->charged;
718 my $delta = FS::Record->scalar_sql(
720 $last_bill->_date - 1,
724 $last_bill_balance += $delta;
727 $last_bill_balance = sprintf("%.2f", $last_bill_balance);
729 warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
731 $self->time2str_local('%D', $last_bill->_date),
734 # ("true_previous_balance" is a terrible name, but at least it's no
735 # longer stored in the database)
736 $invoice_data{'true_previous_balance'} = $last_bill_balance;
738 # the change in balance from immediately after that invoice
739 # to immediately before this one
740 my $before_this_bill_balance = 0;
743 my $delta = FS::Record->scalar_sql(
749 $before_this_bill_balance += $delta;
751 $invoice_data{'balance_adjustments'} =
752 sprintf("%.2f", $last_bill_balance - $before_this_bill_balance);
754 warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
755 $invoice_data{'balance_adjustments'}
758 # the sum of amount owed on all previous invoices
759 # ($pr_total is used elsewhere but not as $previous_balance)
760 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
762 $invoice_data{'last_bill'} = {
763 '_date' => $last_bill->_date, #unformatted
765 my (@payments, @credits);
766 # for formats that itemize previous payments
767 foreach my $cust_pay ( qsearch('cust_pay', {
768 'custnum' => $self->custnum,
769 '_date' => { op => '>=',
770 value => $last_bill->_date }
773 next if $cust_pay->_date > $self->_date;
775 '_date' => $cust_pay->_date,
776 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
777 'payinfo' => $cust_pay->payby_payinfo_pretty,
778 'amount' => sprintf('%.2f', $cust_pay->paid),
780 # not concerned about applications
782 foreach my $cust_credit ( qsearch('cust_credit', {
783 'custnum' => $self->custnum,
784 '_date' => { op => '>=',
785 value => $last_bill->_date }
788 next if $cust_credit->_date > $self->_date;
790 '_date' => $cust_credit->_date,
791 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
792 'creditreason'=> $cust_credit->reason,
793 'amount' => sprintf('%.2f', $cust_credit->amount),
796 $invoice_data{'previous_payments'} = \@payments;
797 $invoice_data{'previous_credits'} = \@credits;
799 # there is no $last_bill
800 $invoice_data{'true_previous_balance'} =
801 $invoice_data{'balance_adjustments'} =
802 $invoice_data{'previous_balance'} = '0.00';
803 $invoice_data{'previous_payments'} = [];
804 $invoice_data{'previous_credits'} = [];
806 } # if this is an invoice
808 my $summarypage = '';
809 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
812 $invoice_data{'summarypage'} = $summarypage;
814 warn "$me substituting variables in notes, footer, smallfooter\n"
817 my $tc = $self->template_conf;
818 my @include = ( [ $tc, 'notes' ],
819 [ 'invoice_', 'footer' ],
820 [ 'invoice_', 'smallfooter', ],
822 push @include, [ $tc, 'coupon', ]
823 unless $params{'no_coupon'};
825 foreach my $i (@include) {
827 my($base, $include) = @$i;
829 my $inc_file = $conf->key_orbase("$base$format$include", $template);
832 if ( $conf->exists($inc_file, $agentnum)
833 && length( $conf->config($inc_file, $agentnum) ) ) {
835 @inc_src = $conf->config($inc_file, $agentnum);
839 $inc_file = $conf->key_orbase("${base}latex$include", $template);
841 my $convert_map = $convert_maps{$format}{$include};
843 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
844 s/--\@\]/$delimiters{$format}[1]/g;
847 &$convert_map( $conf->config($inc_file, $agentnum) );
851 my $inc_tt = new Text::Template (
853 SOURCE => [ map "$_\n", @inc_src ],
854 DELIMITERS => $delimiters{$format},
855 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
857 unless ( $inc_tt->compile() ) {
858 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
859 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
863 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
865 $invoice_data{$include} =~ s/\n+$//
866 if ($format eq 'latex');
869 # let invoices use either of these as needed
870 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
871 ? $cust_main->payinfo : '';
872 $invoice_data{'po_line'} =
873 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
874 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
877 my %money_chars = ( 'latex' => '',
878 'html' => $conf->config('money_char') || '$',
881 my $money_char = $money_chars{$format};
884 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
885 'html' => $conf->config('money_char') || '$',
888 my $other_money_char = $other_money_chars{$format};
889 $invoice_data{'dollar'} = $other_money_char;
891 my %minus_signs = ( 'latex' => '$-$',
893 'template' => '- ' );
894 my $minus = $minus_signs{$format};
896 my @detail_items = ();
897 my @total_items = ();
901 $invoice_data{'detail_items'} = \@detail_items;
902 $invoice_data{'total_items'} = \@total_items;
903 $invoice_data{'buf'} = \@buf;
904 $invoice_data{'sections'} = \@sections;
906 warn "$me generating sections\n"
910 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
911 'subtotal' => $taxtotal, # adjusted below
914 my $tax_weight = _pkg_category($tax_section->{description})
915 ? _pkg_category($tax_section->{description})->weight
917 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
918 $tax_section->{'sort_weight'} = $tax_weight;
921 my $adjust_section = {
922 'description' => $self->mt('Credits, Payments, and Adjustments'),
923 'adjust_section' => 1,
924 'subtotal' => 0, # adjusted below
926 my $adjust_weight = _pkg_category($adjust_section->{description})
927 ? _pkg_category($adjust_section->{description})->weight
929 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
930 $adjust_section->{'sort_weight'} = $adjust_weight;
932 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
933 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
934 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
935 $invoice_data{'multisection'} = $multisection;
937 my $extra_sections = [];
938 my $extra_lines = ();
940 # default section ('Charges')
941 my $default_section = { 'description' => '',
946 # Previous Charges section
947 # subtotal is the first return value from $self->previous
948 my $previous_section;
949 # if the invoice has major sections, or if we're summarizing previous
950 # charges with a single line, or if we've been specifically told to put them
951 # in a section, create a section for previous charges:
952 if ( $multisection or
953 $conf->exists('previous_balance-summary_only') or
954 $conf->exists('previous_balance-section') ) {
956 $previous_section = { 'description' => $self->mt('Previous Charges'),
957 'subtotal' => $other_money_char.
958 sprintf('%.2f', $pr_total),
959 'summarized' => '', #why? $summarypage ? 'Y' : '',
961 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
962 join(' / ', map { $cust_main->balance_date_range(@$_) }
963 $self->_prior_month30s
965 if $conf->exists('invoice_include_aging');
968 # otherwise put them in the main section
969 $previous_section = $default_section;
972 if ( $multisection ) {
973 ($extra_sections, $extra_lines) =
974 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
975 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
976 && $self->can('_items_extra_usage_sections');
978 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
980 push @detail_items, @$extra_lines if $extra_lines;
982 # the code is written so that both methods can be used together, but
983 # we haven't yet changed the template to take advantage of that, so for
984 # now, treat them as mutually exclusive.
985 my %section_method = ( by_category => 1 );
986 if ( $conf->config($tc.'sections_method') eq 'location' ) {
987 %section_method = ( by_location => 1 );
990 $self->_items_sections( 'summary' => $summarypage,
991 'escape' => $escape_function_nonbsp,
992 'extra_sections' => $extra_sections,
996 push @sections, @$early;
997 $late_sections = $late;
999 if ( $conf->exists('svc_phone_sections')
1000 && $self->can('_items_svc_phone_sections')
1003 my ($phone_sections, $phone_lines) =
1004 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1005 push @{$late_sections}, @$phone_sections;
1006 push @detail_items, @$phone_lines;
1008 if ( $conf->exists('voip-cust_accountcode_cdr')
1009 && $cust_main->accountcode_cdr
1010 && $self->can('_items_accountcode_cdr')
1013 my ($accountcode_section, $accountcode_lines) =
1014 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1015 if ( scalar(@$accountcode_lines) ) {
1016 push @{$late_sections}, $accountcode_section;
1017 push @detail_items, @$accountcode_lines;
1020 } else {# not multisection
1021 # make a default section
1022 push @sections, $default_section;
1023 # and calculate the finance charge total, since it won't get done otherwise.
1024 # and the default section total
1025 # XXX possibly finance_pkgclass should not be used in this manner?
1026 my @finance_charges;
1028 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1029 if ( $invoice_data{finance_section} and
1030 grep { $_->section eq $invoice_data{finance_section} }
1031 $cust_bill_pkg->cust_bill_pkg_display ) {
1032 # I think these are always setup fees, but just to be sure...
1033 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1035 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1038 $invoice_data{finance_amount} =
1039 sprintf('%.2f', sum( @finance_charges ) || 0);
1040 $default_section->{subtotal} = $other_money_char.
1041 sprintf('%.2f', sum( @charges ) || 0);
1044 # start setting up summary subtotals
1045 my @summary_subtotals;
1046 my $method = $conf->config('summary_subtotals_method');
1047 if ( $method and $method ne $conf->config($tc.'sections_method') ) {
1048 # then re-section them by the correct method
1049 my %section_method = ( by_category => 1 );
1050 if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1051 %section_method = ( by_location => 1 );
1053 my ($early, $late) =
1054 $self->_items_sections( 'summary' => $summarypage,
1055 'escape' => $escape_function_nonbsp,
1056 'extra_sections' => $extra_sections,
1057 'format' => $format,
1060 foreach ( @$early ) {
1061 next if $_->{subtotal} == 0;
1062 $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1063 push @summary_subtotals, $_;
1066 # subtotal sectioning is the same as for the actual invoice sections
1067 @summary_subtotals = @sections;
1070 # Hereafter, push sections to both @sections and @summary_subtotals
1071 # if they belong in both places (e.g. tax section). Late sections are
1072 # never in @summary_subtotals.
1074 # previous invoice balances in the Previous Charges section if there
1075 # is one, otherwise in the main detail section
1076 # (except if summary_only is enabled, don't show them at all)
1077 if ( $self->can('_items_previous') &&
1078 $self->enable_previous &&
1079 ! $conf->exists('previous_balance-summary_only') ) {
1081 warn "$me adding previous balances\n"
1084 foreach my $line_item ( $self->_items_previous ) {
1087 ref => $line_item->{'pkgnum'},
1088 pkgpart => $line_item->{'pkgpart'},
1089 #quantity => 1, # not really correct
1090 section => $previous_section, # which might be $default_section
1091 description => &$escape_function($line_item->{'description'}),
1092 ext_description => [ map { &$escape_function($_) }
1093 @{ $line_item->{'ext_description'} || [] }
1095 amount => $money_char . $line_item->{'amount'},
1096 product_code => $line_item->{'pkgpart'} || 'N/A',
1099 push @detail_items, $detail;
1100 push @buf, [ $detail->{'description'},
1101 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1107 if ( @pr_cust_bill && $self->enable_previous ) {
1108 push @buf, ['','-----------'];
1109 push @buf, [ $self->mt('Total Previous Balance'),
1110 $money_char. sprintf("%10.2f", $pr_total) ];
1114 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1115 warn "$me adding DID summary\n"
1118 my ($didsummary,$minutes) = $self->_did_summary;
1119 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1121 { 'description' => $didsummary_desc,
1122 'ext_description' => [ $didsummary, $minutes ],
1126 foreach my $section (@sections, @$late_sections) {
1128 # begin some normalization
1129 $section->{'subtotal'} = $section->{'amount'}
1131 && !exists($section->{subtotal})
1132 && exists($section->{amount});
1134 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1135 if ( $invoice_data{finance_section} &&
1136 $section->{'description'} eq $invoice_data{finance_section} );
1138 $section->{'subtotal'} = $other_money_char.
1139 sprintf('%.2f', $section->{'subtotal'})
1142 # continue some normalization
1143 $section->{'amount'} = $section->{'subtotal'}
1147 if ( $section->{'description'} ) {
1148 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1153 warn "$me setting options\n"
1157 $options{'section'} = $section if $multisection;
1158 $options{'format'} = $format;
1159 $options{'escape_function'} = $escape_function;
1160 $options{'no_usage'} = 1 unless $unsquelched;
1161 $options{'unsquelched'} = $unsquelched;
1162 $options{'summary_page'} = $summarypage;
1163 $options{'skip_usage'} =
1164 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1165 $options{'preref_callback'} = $params{'preref_callback'};
1167 warn "$me searching for line items\n"
1170 foreach my $line_item ( $self->_items_pkg(%options),
1171 $self->_items_fee(%options) ) {
1173 warn "$me adding line item ".
1174 join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1177 $line_item->{'ref'} = $line_item->{'pkgnum'};
1178 $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1179 $line_item->{'section'} = $section;
1180 $line_item->{'description'} = &$escape_function($line_item->{'description'});
1181 $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1183 if ( length($line_item->{'unit_amount'}) ) {
1184 $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1186 $line_item->{'ext_description'} ||= [];
1188 push @detail_items, $line_item;
1189 push @buf, ( [ $line_item->{'description'},
1190 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1192 map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1196 if ( $section->{'description'} ) {
1197 push @buf, ( ['','-----------'],
1198 [ $section->{'description'}. ' sub-total',
1199 $section->{'subtotal'} # already formatted this
1208 $invoice_data{current_less_finance} =
1209 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1211 # if there's anything in the Previous Charges section, prepend it to the list
1212 if ( $pr_total and $previous_section ne $default_section ) {
1213 unshift @sections, $previous_section;
1214 # but not @summary_subtotals
1217 warn "$me adding taxes\n"
1220 my @items_tax = $self->_items_tax;
1221 foreach my $tax ( @items_tax ) {
1223 $taxtotal += $tax->{'amount'};
1225 my $description = &$escape_function( $tax->{'description'} );
1226 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1228 if ( $multisection ) {
1230 push @detail_items, {
1231 ext_description => [],
1234 description => $description,
1235 amount => $money_char. $amount,
1237 section => $tax_section,
1242 push @total_items, {
1243 'total_item' => $description,
1244 'total_amount' => $other_money_char. $amount,
1249 push @buf,[ $description,
1250 $money_char. $amount,
1257 $total->{'total_item'} = $self->mt('Sub-total');
1258 $total->{'total_amount'} =
1259 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1261 if ( $multisection ) {
1262 $tax_section->{'subtotal'} = $other_money_char.
1263 sprintf('%.2f', $taxtotal);
1264 $tax_section->{'pretotal'} = 'New charges sub-total '.
1265 $total->{'total_amount'};
1267 push @sections, $tax_section;
1268 push @summary_subtotals, $tax_section;
1271 unshift @total_items, $total;
1274 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1276 push @buf,['','-----------'];
1277 push @buf,[$self->mt(
1278 (!$self->enable_previous)
1280 : 'Total New Charges'
1282 $money_char. sprintf("%10.2f",$self->charged) ];
1290 my %embolden_functions = (
1291 'latex' => sub { return '\textbf{'. shift(). '}' },
1292 'html' => sub { return '<b>'. shift(). '</b>' },
1293 'template' => sub { shift },
1295 my $embolden_function = $embolden_functions{$format};
1297 if ( $self->can('_items_total') ) { # quotations
1299 $self->_items_total(\@total_items);
1301 foreach ( @total_items ) {
1302 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1303 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1304 $_->{'total_amount'}
1308 } else { #normal invoice case
1310 # calculate total, possibly including total owed on previous
1314 $item = $conf->config('previous_balance-exclude_from_total')
1315 || 'Total New Charges'
1316 if $conf->exists('previous_balance-exclude_from_total');
1317 my $amount = $self->charged;
1318 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1319 $amount += $pr_total;
1322 $total->{'total_item'} = &$embolden_function($self->mt($item));
1323 $total->{'total_amount'} =
1324 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1325 if ( $multisection ) {
1326 if ( $adjust_section->{'sort_weight'} ) {
1327 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1328 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1330 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1331 $other_money_char. sprintf('%.2f', $self->charged );
1334 push @total_items, $total;
1336 push @buf,['','-----------'];
1339 sprintf( '%10.2f', $amount )
1343 # if we're showing previous invoices, also show previous
1344 # credits and payments
1345 if ( $self->enable_previous
1346 and $self->can('_items_credits')
1347 and $self->can('_items_payments') )
1349 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1352 my $credittotal = 0;
1353 foreach my $credit (
1354 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1358 $total->{'total_item'} = &$escape_function($credit->{'description'});
1359 $credittotal += $credit->{'amount'};
1360 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1361 $adjusttotal += $credit->{'amount'};
1362 if ( $multisection ) {
1363 push @detail_items, {
1364 ext_description => [],
1367 description => &$escape_function($credit->{'description'}),
1368 amount => $money_char . $credit->{'amount'},
1370 section => $adjust_section,
1373 push @total_items, $total;
1377 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1380 foreach my $credit (
1381 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1383 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1387 my $paymenttotal = 0;
1388 foreach my $payment (
1389 $self->_items_payments( 'template' => $template )
1392 $total->{'total_item'} = &$escape_function($payment->{'description'});
1393 $paymenttotal += $payment->{'amount'};
1394 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1395 $adjusttotal += $payment->{'amount'};
1396 if ( $multisection ) {
1397 push @detail_items, {
1398 ext_description => [],
1401 description => &$escape_function($payment->{'description'}),
1402 amount => $money_char . $payment->{'amount'},
1404 section => $adjust_section,
1407 push @total_items, $total;
1409 push @buf, [ $payment->{'description'},
1410 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1413 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1415 if ( $multisection ) {
1416 $adjust_section->{'subtotal'} = $other_money_char.
1417 sprintf('%.2f', $adjusttotal);
1418 push @sections, $adjust_section
1419 unless $adjust_section->{sort_weight};
1420 # do not summarize; adjustments there are shown according to
1424 # create Balance Due message
1427 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1428 $total->{'total_amount'} =
1429 &$embolden_function(
1430 $other_money_char. sprintf('%.2f', #why? $summarypage
1431 # ? $self->charged +
1432 # $self->billing_balance
1434 $self->owed + $pr_total
1437 if ( $multisection && !$adjust_section->{sort_weight} ) {
1438 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1439 $total->{'total_amount'};
1441 push @total_items, $total;
1443 push @buf,['','-----------'];
1444 push @buf,[$self->balance_due_msg, $money_char.
1445 sprintf("%10.2f", $balance_due ) ];
1448 if ( $conf->exists('previous_balance-show_credit')
1449 and $cust_main->balance < 0 ) {
1450 my $credit_total = {
1451 'total_item' => &$embolden_function($self->credit_balance_msg),
1452 'total_amount' => &$embolden_function(
1453 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1456 if ( $multisection ) {
1457 $adjust_section->{'posttotal'} .= $newline_token .
1458 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1461 push @total_items, $credit_total;
1463 push @buf,['','-----------'];
1464 push @buf,[$self->credit_balance_msg, $money_char.
1465 sprintf("%10.2f", -$cust_main->balance ) ];
1469 } #end of default total adding ! can('_items_total')
1471 if ( $multisection ) {
1472 if ( $conf->exists('svc_phone_sections')
1473 && $self->can('_items_svc_phone_sections')
1477 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1478 $total->{'total_amount'} =
1479 &$embolden_function(
1480 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1482 my $last_section = pop @sections;
1483 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1484 $total->{'total_amount'};
1485 push @sections, $last_section;
1487 push @sections, @$late_sections
1491 # make a discounts-available section, even without multisection
1492 if ( $conf->exists('discount-show_available')
1493 and my @discounts_avail = $self->_items_discounts_avail ) {
1494 my $discount_section = {
1495 'description' => $self->mt('Discounts Available'),
1500 push @sections, $discount_section; # do not summarize
1501 push @detail_items, map { +{
1502 'ref' => '', #should this be something else?
1503 'section' => $discount_section,
1504 'description' => &$escape_function( $_->{description} ),
1505 'amount' => $money_char . &$escape_function( $_->{amount} ),
1506 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1507 } } @discounts_avail;
1510 # not adding any more sections after this
1511 $invoice_data{summary_subtotals} = \@summary_subtotals;
1514 if ( $conf->exists('usage_class_summary')
1515 and $self->can('_items_usage_class_summary') ) {
1516 my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
1517 if ( @usage_subtotals ) {
1518 unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1519 unshift @detail_items, @usage_subtotals;
1523 # invoice history "section" (not really a section)
1524 # not to be included in any subtotals, completely independent of
1526 if ( $conf->exists('previous_invoice_history') ) {
1529 foreach my $cust_bill ( $cust_main->cust_bill ) {
1530 # XXX hardcoded format, and currently only 'charged'; add other fields
1531 # if they become necessary
1532 my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1533 $history{$date} ||= 0;
1534 $history{$date} += $cust_bill->charged;
1535 # just so we have a numeric sort key
1536 $monthorder{$date} ||= $cust_bill->_date;
1538 my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1540 my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1541 $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1544 # service locations: another option for template customization
1546 foreach my $item (@detail_items) {
1547 if ( $item->{locationnum} ) {
1548 $location_info{ $item->{locationnum} } ||= {
1549 FS::cust_location->by_key( $item->{locationnum} )->location_hash
1553 $invoice_data{location_info} = \%location_info;
1555 # debugging hook: call this with 'diag' => 1 to just get a hash of
1556 # the invoice variables
1557 return \%invoice_data if ( $params{'diag'} );
1559 # All sections and items are built; now fill in templates.
1560 my @includelist = ();
1561 push @includelist, 'summary' if $summarypage;
1562 foreach my $include ( @includelist ) {
1564 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1567 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1569 @inc_src = $conf->config($inc_file, $agentnum);
1573 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1575 my $convert_map = $convert_maps{$format}{$include};
1577 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1578 s/--\@\]/$delimiters{$format}[1]/g;
1581 &$convert_map( $conf->config($inc_file, $agentnum) );
1585 my $inc_tt = new Text::Template (
1587 SOURCE => [ map "$_\n", @inc_src ],
1588 DELIMITERS => $delimiters{$format},
1589 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1591 unless ( $inc_tt->compile() ) {
1592 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1593 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1597 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1599 $invoice_data{$include} =~ s/\n+$//
1600 if ($format eq 'latex');
1605 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1606 /invoice_lines\((\d*)\)/;
1607 $invoice_lines += $1 || scalar(@buf);
1610 die "no invoice_lines() functions in template?"
1611 if ( $format eq 'template' && !$wasfunc );
1613 if ($format eq 'template') {
1615 if ( $invoice_lines ) {
1616 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1617 $invoice_data{'total_pages'}++
1618 if scalar(@buf) % $invoice_lines;
1621 #setup subroutine for the template
1622 $invoice_data{invoice_lines} = sub {
1623 my $lines = shift || scalar(@buf);
1635 push @collect, split("\n",
1636 $text_template->fill_in( HASH => \%invoice_data )
1638 $invoice_data{'page'}++;
1640 map "$_\n", @collect;
1642 } else { # this is where we actually create the invoice
1644 warn "filling in template for invoice ". $self->invnum. "\n"
1646 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1649 $text_template->fill_in(HASH => \%invoice_data);
1653 sub notice_name { '('.shift->table.')'; }
1655 sub template_conf { 'invoice_'; }
1657 # helper routine for generating date ranges
1658 sub _prior_month30s {
1661 [ 1, 2592000 ], # 0-30 days ago
1662 [ 2592000, 5184000 ], # 30-60 days ago
1663 [ 5184000, 7776000 ], # 60-90 days ago
1664 [ 7776000, 0 ], # 90+ days ago
1667 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1668 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1673 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1675 Returns an postscript invoice, as a scalar.
1677 Options can be passed as a hashref (recommended) or as a list of time, template
1678 and then any key/value pairs for any other options.
1680 I<time> an optional value used to control the printing of overdue messages. The
1681 default is now. It isn't the date of the invoice; that's the `_date' field.
1682 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1683 L<Time::Local> and L<Date::Parse> for conversion functions.
1685 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1692 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1693 my $ps = generate_ps($file);
1695 unlink($barcodefile) if $barcodefile;
1700 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1702 Returns an PDF invoice, as a scalar.
1704 Options can be passed as a hashref (recommended) or as a list of time, template
1705 and then any key/value pairs for any other options.
1707 I<time> an optional value used to control the printing of overdue messages. The
1708 default is now. It isn't the date of the invoice; that's the `_date' field.
1709 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1710 L<Time::Local> and L<Date::Parse> for conversion functions.
1712 I<template>, if specified, is the name of a suffix for alternate invoices.
1714 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1721 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1722 my $pdf = generate_pdf($file);
1724 unlink($barcodefile) if $barcodefile;
1729 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1731 Returns an HTML invoice, as a scalar.
1733 I<time> an optional value used to control the printing of overdue messages. The
1734 default is now. It isn't the date of the invoice; that's the `_date' field.
1735 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1736 L<Time::Local> and L<Date::Parse> for conversion functions.
1738 I<template>, if specified, is the name of a suffix for alternate invoices.
1740 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1742 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1743 when emailing the invoice as part of a multipart/related MIME email.
1751 %params = %{ shift() };
1755 $params{'format'} = 'html';
1757 $self->print_generic( %params );
1760 # quick subroutine for print_latex
1762 # There are ten characters that LaTeX treats as special characters, which
1763 # means that they do not simply typeset themselves:
1764 # # $ % & ~ _ ^ \ { }
1766 # TeX ignores blanks following an escaped character; if you want a blank (as
1767 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1771 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1772 $value =~ s/([<>])/\$$1\$/g;
1778 encode_entities($value);
1782 sub _html_escape_nbsp {
1783 my $value = _html_escape(shift);
1784 $value =~ s/ +/ /g;
1788 #utility methods for print_*
1790 sub _translate_old_latex_format {
1791 warn "_translate_old_latex_format called\n"
1798 if ( $line =~ /^%%Detail\s*$/ ) {
1800 push @template, q![@--!,
1801 q! foreach my $_tr_line (@detail_items) {!,
1802 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1803 q! $_tr_line->{'description'} .= !,
1804 q! "\\tabularnewline\n~~".!,
1805 q! join( "\\tabularnewline\n~~",!,
1806 q! @{$_tr_line->{'ext_description'}}!,
1810 while ( ( my $line_item_line = shift )
1811 !~ /^%%EndDetail\s*$/ ) {
1812 $line_item_line =~ s/'/\\'/g; # nice LTS
1813 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1814 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1815 push @template, " \$OUT .= '$line_item_line';";
1818 push @template, '}',
1821 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1823 push @template, '[@--',
1824 ' foreach my $_tr_line (@total_items) {';
1826 while ( ( my $total_item_line = shift )
1827 !~ /^%%EndTotalDetails\s*$/ ) {
1828 $total_item_line =~ s/'/\\'/g; # nice LTS
1829 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1830 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1831 push @template, " \$OUT .= '$total_item_line';";
1834 push @template, '}',
1838 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1839 push @template, $line;
1845 warn "$_\n" foreach @template;
1853 my $conf = $self->conf;
1855 #check for an invoice-specific override
1856 return $self->invoice_terms if $self->invoice_terms;
1858 #check for a customer- specific override
1859 my $cust_main = $self->cust_main;
1860 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1862 #use configured default
1863 $conf->config('invoice_default_terms') || '';
1869 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1870 $duedate = $self->_date() + ( $1 * 86400 );
1877 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1880 sub balance_due_msg {
1882 my $msg = $self->mt('Balance Due');
1883 return $msg unless $self->terms;
1884 if ( $self->due_date ) {
1885 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1886 $self->due_date2str('short');
1887 } elsif ( $self->terms ) {
1888 $msg .= ' - '. $self->terms;
1893 sub balance_due_date {
1895 my $conf = $self->conf;
1897 if ( $conf->exists('invoice_default_terms')
1898 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1899 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1904 sub credit_balance_msg {
1906 $self->mt('Credit Balance Remaining')
1911 Returns a string with the date, for example: "3/20/2008", localized for the
1912 customer. Use _date_pretty_unlocalized for non-end-customer display use.
1918 $self->time2str_local('short', $self->_date);
1921 =item _date_pretty_unlocalized
1923 Returns a string with the date, for example: "3/20/2008", in the format
1924 configured for the back-office. Use _date_pretty for end-customer display use.
1928 sub _date_pretty_unlocalized {
1930 time2str($date_format, $self->_date);
1933 =item _items_sections OPTIONS
1935 Generate section information for all items appearing on this invoice.
1936 This will only be called for multi-section invoices.
1938 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1939 related display records (L<FS::cust_bill_pkg_display>) and organize
1940 them into two groups ("early" and "late" according to whether they come
1941 before or after the total), then into sections. A subtotal is calculated
1944 Section descriptions are returned in sort weight order. Each consists
1945 of a hash containing:
1947 description: the package category name, escaped
1948 subtotal: the total charges in that section
1949 tax_section: a flag indicating that the section contains only tax charges
1950 summarized: same as tax_section, for some reason
1951 sort_weight: the package category's sort weight
1953 If 'condense' is set on the display record, it also contains everything
1954 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1955 coderefs to generate parts of the invoice. This is not advised.
1957 The method returns two arrayrefs, one of "early" sections and one of "late"
1960 OPTIONS may include:
1962 by_location: a flag to divide the invoice into sections by location.
1963 Each section hash will have a 'location' element containing a hashref of
1964 the location fields (see L<FS::cust_location>). The section description
1965 will be the location label, but the template can use any of the location
1966 fields to create a suitable label.
1968 by_category: a flag to divide the invoice into sections using display
1969 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1970 behavior. Each section hash will have a 'category' element containing
1971 the section name from the display record (which probably equals the
1972 category name of the package, but may not in some cases).
1974 summary: a flag indicating that this is a summary-format invoice.
1975 Turning this on has the following effects:
1976 - Ignores display items with the 'summary' flag.
1977 - Places all sections in the "early" group even if they have post_total.
1978 - Creates sections for all non-disabled package categories, even if they
1979 have no charges on this invoice, as well as a section with no name.
1981 escape: an escape function to use for section titles.
1983 extra_sections: an arrayref of additional sections to return after the
1984 sorted list. If there are any of these, section subtotals exclude
1987 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1988 passed through to C<_condense_section()>.
1992 use vars qw(%pkg_category_cache);
1993 sub _items_sections {
1997 my $escape = $opt{escape};
1998 my @extra_sections = @{ $opt{extra_sections} || [] };
2000 # $subtotal{$locationnum}{$categoryname} = amount.
2001 # if we're not using by_location, $locationnum is undef.
2002 # if we're not using by_category, you guessed it, $categoryname is undef.
2003 # if we're not using either one, we shouldn't be here in the first place...
2005 my %late_subtotal = ();
2008 # About tax items + multisection invoices:
2009 # If either invoice_*summary option is enabled, AND there is a
2010 # package category with the name of the tax, then there will be
2011 # a display record assigning the tax item to that category.
2013 # However, the taxes are always placed in the "Taxes, Surcharges,
2014 # and Fees" section regardless of that. The only effect of the
2015 # display record is to create a subtotal for the summary page.
2018 my $pkg_hash = $self->cust_pkg_hash;
2020 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2023 my $usage = $cust_bill_pkg->usage;
2026 if ( $opt{by_location} ) {
2027 if ( $cust_bill_pkg->pkgnum ) {
2028 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2033 $locationnum = undef;
2036 # as in _items_cust_pkg, if a line item has no display records,
2037 # cust_bill_pkg_display() returns a default record for it
2039 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2040 next if ( $display->summary && $opt{summary} );
2042 my $section = $display->section;
2043 my $type = $display->type;
2044 # Set $section = undef if we're sectioning by location and this
2045 # line item _has_ a location (i.e. isn't a fee).
2046 $section = undef if $locationnum;
2048 # set this flag if the section is not tax-only
2049 $not_tax{$locationnum}{$section} = 1
2050 if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
2052 # there's actually a very important piece of logic buried in here:
2053 # incrementing $late_subtotal{$section} CREATES
2054 # $late_subtotal{$section}. keys(%late_subtotal) is later used
2055 # to define the list of late sections, and likewise keys(%subtotal).
2056 # When _items_cust_bill_pkg is called to generate line items for
2057 # real, it will be called with 'section' => $section for each
2059 if ( $display->post_total && !$opt{summary} ) {
2060 if (! $type || $type eq 'S') {
2061 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2062 if $cust_bill_pkg->setup != 0
2063 || $cust_bill_pkg->setup_show_zero;
2067 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2068 if $cust_bill_pkg->recur != 0
2069 || $cust_bill_pkg->recur_show_zero;
2072 if ($type && $type eq 'R') {
2073 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2074 if $cust_bill_pkg->recur != 0
2075 || $cust_bill_pkg->recur_show_zero;
2078 if ($type && $type eq 'U') {
2079 $late_subtotal{$locationnum}{$section} += $usage
2080 unless scalar(@extra_sections);
2083 } else { # it's a pre-total (normal) section
2085 # skip tax items unless they're explicitly included in a section
2086 next if $cust_bill_pkg->pkgnum == 0 and
2087 ! $cust_bill_pkg->feepart and
2090 if ( $type eq 'S' ) {
2091 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2092 if $cust_bill_pkg->setup != 0
2093 || $cust_bill_pkg->setup_show_zero;
2094 } elsif ( $type eq 'R' ) {
2095 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2096 if $cust_bill_pkg->recur != 0
2097 || $cust_bill_pkg->recur_show_zero;
2098 } elsif ( $type eq 'U' ) {
2099 $subtotal{$locationnum}{$section} += $usage
2100 unless scalar(@extra_sections);
2101 } elsif ( !$type ) {
2102 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2103 + $cust_bill_pkg->recur;
2112 %pkg_category_cache = ();
2114 # summary invoices need subtotals for all non-disabled package categories,
2115 # even if they're zero
2116 # but currently assume that there are no location sections, or at least
2117 # that the summary page doesn't care about them
2118 if ( $opt{summary} ) {
2119 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2120 $subtotal{''}{$category->categoryname} ||= 0;
2122 $subtotal{''}{''} ||= 0;
2126 foreach my $post_total (0,1) {
2128 my $s = $post_total ? \%late_subtotal : \%subtotal;
2129 foreach my $locationnum (keys %$s) {
2130 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2132 'subtotal' => $s->{$locationnum}{$sectionname},
2133 'post_total' => $post_total,
2136 if ( $locationnum ) {
2137 $section->{'locationnum'} = $locationnum;
2138 my $location = FS::cust_location->by_key($locationnum);
2139 $section->{'description'} = &{ $escape }($location->location_label);
2140 # Better ideas? This will roughly group them by proximity,
2141 # which alpha sorting on any of the address fields won't.
2142 # Sorting by locationnum is meaningless.
2143 # We have to sort on _something_ or the order may change
2144 # randomly from one invoice to the next, which will confuse
2146 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2148 $section->{'location'} = {
2149 label_prefix => &{ $escape }($location->label_prefix),
2150 map { $_ => &{ $escape }($location->get($_)) }
2154 $section->{'category'} = $sectionname;
2155 $section->{'description'} = &{ $escape }($sectionname);
2156 if ( _pkg_category($_) ) {
2157 $section->{'sort_weight'} = _pkg_category($_)->weight;
2158 if ( _pkg_category($_)->condense ) {
2159 $section = { %$section, $self->_condense_section($opt{format}) };
2163 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2164 # then it's a tax-only section
2165 $section->{'summarized'} = 'Y';
2166 $section->{'tax_section'} = 'Y';
2168 push @these, $section;
2169 } # foreach $sectionname
2170 } #foreach $locationnum
2171 push @these, @extra_sections if $post_total == 0;
2172 # need an alpha sort for location sections, because postal codes can
2174 $sections[ $post_total ] = [ sort {
2175 $opt{'by_location'} ?
2176 ($a->{sort_weight} cmp $b->{sort_weight}) :
2177 ($a->{sort_weight} <=> $b->{sort_weight})
2179 } #foreach $post_total
2181 return @sections; # early, late
2184 #helper subs for above
2188 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2192 my $categoryname = shift;
2193 $pkg_category_cache{$categoryname} ||=
2194 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2197 my %condensed_format = (
2198 'label' => [ qw( Description Qty Amount ) ],
2200 sub { shift->{description} },
2201 sub { shift->{quantity} },
2202 sub { my($href, %opt) = @_;
2203 ($opt{dollar} || ''). $href->{amount};
2206 'align' => [ qw( l r r ) ],
2207 'span' => [ qw( 5 1 1 ) ], # unitprices?
2208 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2211 sub _condense_section {
2212 my ( $self, $format ) = ( shift, shift );
2214 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2215 qw( description_generator
2218 total_line_generator
2223 sub _condensed_generator_defaults {
2224 my ( $self, $format ) = ( shift, shift );
2225 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2234 sub _condensed_header_generator {
2235 my ( $self, $format ) = ( shift, shift );
2237 my ( $f, $prefix, $suffix, $separator, $column ) =
2238 _condensed_generator_defaults($format);
2240 if ($format eq 'latex') {
2241 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2242 $suffix = "\\\\\n\\hline";
2245 sub { my ($d,$a,$s,$w) = @_;
2246 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2248 } elsif ( $format eq 'html' ) {
2249 $prefix = '<th></th>';
2253 sub { my ($d,$a,$s,$w) = @_;
2254 return qq!<th align="$html_align{$a}">$d</th>!;
2262 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2264 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2267 $prefix. join($separator, @result). $suffix;
2272 sub _condensed_description_generator {
2273 my ( $self, $format ) = ( shift, shift );
2275 my ( $f, $prefix, $suffix, $separator, $column ) =
2276 _condensed_generator_defaults($format);
2278 my $money_char = '$';
2279 if ($format eq 'latex') {
2280 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2282 $separator = " & \n";
2284 sub { my ($d,$a,$s,$w) = @_;
2285 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2287 $money_char = '\\dollar';
2288 }elsif ( $format eq 'html' ) {
2289 $prefix = '"><td align="center"></td>';
2293 sub { my ($d,$a,$s,$w) = @_;
2294 return qq!<td align="$html_align{$a}">$d</td>!;
2296 #$money_char = $conf->config('money_char') || '$';
2297 $money_char = ''; # this is madness
2305 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2307 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2309 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2310 map { $f->{$_}->[$i] } qw(align span width)
2314 $prefix. join( $separator, @result ). $suffix;
2319 sub _condensed_total_generator {
2320 my ( $self, $format ) = ( shift, shift );
2322 my ( $f, $prefix, $suffix, $separator, $column ) =
2323 _condensed_generator_defaults($format);
2326 if ($format eq 'latex') {
2329 $separator = " & \n";
2331 sub { my ($d,$a,$s,$w) = @_;
2332 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2334 }elsif ( $format eq 'html' ) {
2338 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2340 sub { my ($d,$a,$s,$w) = @_;
2341 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2350 # my $r = &{$f->{fields}->[$i]}(@args);
2351 # $r .= ' Total' unless $i;
2353 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2355 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2356 map { $f->{$_}->[$i] } qw(align span width)
2360 $prefix. join( $separator, @result ). $suffix;
2365 =item total_line_generator FORMAT
2367 Returns a coderef used for generation of invoice total line items for this
2368 usage_class. FORMAT is either html or latex
2372 # should not be used: will have issues with hash element names (description vs
2373 # total_item and amount vs total_amount -- another array of functions?
2375 sub _condensed_total_line_generator {
2376 my ( $self, $format ) = ( shift, shift );
2378 my ( $f, $prefix, $suffix, $separator, $column ) =
2379 _condensed_generator_defaults($format);
2382 if ($format eq 'latex') {
2385 $separator = " & \n";
2387 sub { my ($d,$a,$s,$w) = @_;
2388 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2390 }elsif ( $format eq 'html' ) {
2394 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2396 sub { my ($d,$a,$s,$w) = @_;
2397 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2406 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2408 &{$column}( &{$f->{fields}->[$i]}(@args),
2409 map { $f->{$_}->[$i] } qw(align span width)
2413 $prefix. join( $separator, @result ). $suffix;
2418 =item _items_pkg [ OPTIONS ]
2420 Return line item hashes for each package item on this invoice. Nearly
2423 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2425 The only OPTIONS accepted is 'section', which may point to a hashref
2426 with a key named 'condensed', which may have a true value. If it
2427 does, this method tries to merge identical items into items with
2428 'quantity' equal to the number of items (not the sum of their
2429 separate quantities, for some reason).
2435 # The order of these is important. Bundled line items will be merged into
2436 # the most recent non-hidden item, so it needs to be the one with:
2438 # - the same start date
2439 # - no pkgpart_override
2441 # So: sort by pkgnum,
2443 # then sort the base line item before any overrides
2444 # then sort hidden before non-hidden add-ons
2445 # then sort by override pkgpart (for consistency)
2446 sort { $a->pkgnum <=> $b->pkgnum or
2447 $a->sdate <=> $b->sdate or
2448 ($a->pkgpart_override ? 0 : -1) or
2449 ($b->pkgpart_override ? 0 : 1) or
2450 $b->hidden cmp $a->hidden or
2451 $a->pkgpart_override <=> $b->pkgpart_override
2453 # and of course exclude taxes and fees
2454 grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2460 my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2462 foreach my $cust_bill_pkg (@cust_bill_pkg) {
2463 # cache this, so we don't look it up again in every section
2464 my $part_fee = $cust_bill_pkg->get('part_fee')
2465 || $cust_bill_pkg->part_fee;
2466 $cust_bill_pkg->set('part_fee', $part_fee);
2468 #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2469 warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2472 if ( exists($options{section}) and exists($options{section}{category}) )
2474 my $categoryname = $options{section}{category};
2475 # then filter for items that have that section
2476 if ( $part_fee->categoryname ne $categoryname ) {
2477 warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2480 } # otherwise include them all in the main section
2481 # XXX what to do when sectioning by location?
2484 my %base_invnums; # invnum => invoice date
2485 foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2486 if ($_->base_invnum) {
2487 my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2488 my $base_date = $self->time2str_local('short', $base_bill->_date)
2490 $base_invnums{$_->base_invnum} = $base_date || '';
2493 foreach (sort keys(%base_invnums)) {
2494 next if $_ == $self->invnum;
2496 $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2499 { feepart => $cust_bill_pkg->feepart,
2500 amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2501 description => $part_fee->itemdesc_locale($self->cust_main->locale),
2502 ext_description => \@ext_desc
2513 warn "$me _items_pkg searching for all package line items\n"
2516 my @cust_bill_pkg = $self->_items_nontax;
2518 warn "$me _items_pkg filtering line items\n"
2520 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2522 if ($options{section} && $options{section}->{condensed}) {
2524 warn "$me _items_pkg condensing section\n"
2528 local $Storable::canonical = 1;
2529 foreach ( @items ) {
2531 delete $item->{ref};
2532 delete $item->{ext_description};
2533 my $key = freeze($item);
2534 $itemshash{$key} ||= 0;
2535 $itemshash{$key} ++; # += $item->{quantity};
2537 @items = sort { $a->{description} cmp $b->{description} }
2538 map { my $i = thaw($_);
2539 $i->{quantity} = $itemshash{$_};
2541 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2547 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2554 return 0 unless $a->itemdesc cmp $b->itemdesc;
2555 return -1 if $b->itemdesc eq 'Tax';
2556 return 1 if $a->itemdesc eq 'Tax';
2557 return -1 if $b->itemdesc eq 'Other surcharges';
2558 return 1 if $a->itemdesc eq 'Other surcharges';
2559 $a->itemdesc cmp $b->itemdesc;
2564 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
2565 $self->cust_bill_pkg;
2566 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2568 if ( $self->conf->exists('always_show_tax') ) {
2569 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2570 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2572 { 'description' => $itemdesc,
2579 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2581 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2582 list of hashrefs describing the line items they generate on the invoice.
2584 OPTIONS may include:
2586 format: the invoice format.
2588 escape_function: the function used to escape strings.
2590 DEPRECATED? (expensive, mostly unused?)
2591 format_function: the function used to format CDRs.
2593 section: a hashref containing 'category' and/or 'locationnum'; if this
2594 is present, only returns line items that belong to that category and/or
2595 location (whichever is defined).
2597 multisection: a flag indicating that this is a multisection invoice,
2598 which does something complicated.
2600 preref_callback: coderef run for each line item, code should return HTML to be
2601 displayed before that line item (quotations only)
2603 Returns a list of hashrefs, each of which may contain:
2605 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2606 ext_description, which is an arrayref of detail lines to show below
2611 sub _items_cust_bill_pkg {
2613 my $conf = $self->conf;
2614 my $cust_bill_pkgs = shift;
2617 my $format = $opt{format} || '';
2618 my $escape_function = $opt{escape_function} || sub { shift };
2619 my $format_function = $opt{format_function} || '';
2620 my $no_usage = $opt{no_usage} || '';
2621 my $unsquelched = $opt{unsquelched} || ''; #unused
2622 my ($section, $locationnum, $category);
2623 if ( $opt{section} ) {
2624 $category = $opt{section}->{category};
2625 $locationnum = $opt{section}->{locationnum};
2627 my $summary_page = $opt{summary_page} || ''; #unused
2628 my $multisection = defined($category) || defined($locationnum);
2629 my $discount_show_always = 0;
2631 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2633 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2634 # and location labels
2636 my @b = (); # accumulator for the line item hashes that we'll return
2637 my ($s, $r, $u, $d) = ( undef, undef, undef );
2638 # the 'current' line item hashes for setup, recur, usage, discount
2639 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2641 # if the current line item is waiting to go out, and the one we're about
2642 # to start is not bundled, then push out the current one and start a new
2644 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) , $d ) {
2645 if ( $_ && !$cust_bill_pkg->hidden ) {
2646 $_->{amount} = sprintf( "%.2f", $_->{amount} );
2647 $_->{amount} =~ s/^\-0\.00$/0.00/;
2648 if (exists($_->{unit_amount})) {
2649 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
2652 if $_->{amount} != 0
2653 || $discount_show_always
2654 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2655 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2661 if ( $locationnum ) {
2662 # this is a location section; skip packages that aren't at this
2664 next if $cust_bill_pkg->pkgnum == 0; # skips fees...
2665 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2669 # Consider display records for this item to determine if it belongs
2670 # in this section. Note that if there are no display records, there
2671 # will be a default pseudo-record that includes all charge types
2672 # and has no section name.
2673 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2674 ? $cust_bill_pkg->cust_bill_pkg_display
2675 : ( $cust_bill_pkg );
2677 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2678 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2681 if ( defined($category) ) {
2682 # then this is a package category section; process all display records
2683 # that belong to this section.
2684 @cust_bill_pkg_display = grep { $_->section eq $category }
2685 @cust_bill_pkg_display;
2687 # otherwise, process all display records that aren't usage summaries
2688 # (I don't think there should be usage summaries if you aren't using
2689 # category sections, but this is the historical behavior)
2690 @cust_bill_pkg_display = grep { !$_->summary }
2691 @cust_bill_pkg_display;
2694 my $classname = ''; # package class name, will fill in later
2696 foreach my $display (@cust_bill_pkg_display) {
2698 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2699 $display->billpkgdisplaynum. "\n"
2702 my $type = $display->type;
2704 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2705 $desc = substr($desc, 0, $maxlength). '...'
2706 if $format eq 'latex' && length($desc) > $maxlength;
2708 my %details_opt = ( 'format' => $format,
2709 'escape_function' => $escape_function,
2710 'format_function' => $format_function,
2711 'no_usage' => $opt{'no_usage'},
2714 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2716 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2718 # quotation_pkgs are never fees, so don't worry about the case where
2719 # part_pkg is undefined
2721 # and I guess they're never bundled either?
2722 if ( $cust_bill_pkg->setup != 0 ) {
2723 my $description = $desc;
2724 $description .= ' Setup'
2725 if $cust_bill_pkg->recur != 0
2726 || $discount_show_always
2727 || $cust_bill_pkg->recur_show_zero;
2729 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2730 'description' => $description,
2731 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2732 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2733 'quantity' => $cust_bill_pkg->quantity,
2734 'preref_html' => ( $opt{preref_callback}
2735 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2740 if ( $cust_bill_pkg->recur != 0 ) {
2742 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2743 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2744 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2745 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2746 'quantity' => $cust_bill_pkg->quantity,
2747 'preref_html' => ( $opt{preref_callback}
2748 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2754 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2755 # a "normal" package line item (not a quotation, not a fee, not a tax)
2757 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2760 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2761 my $part_pkg = $cust_pkg->part_pkg;
2763 # which pkgpart to show for display purposes?
2764 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2766 # start/end dates for invoice formats that do nonstandard
2768 my %item_dates = ();
2769 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2770 unless $part_pkg->option('disable_line_item_date_ranges',1);
2772 # not normally used, but pass this to the template anyway
2773 $classname = $part_pkg->classname;
2775 if ( (!$type || $type eq 'S')
2776 && ( $cust_bill_pkg->setup != 0
2777 || $cust_bill_pkg->setup_show_zero
2782 warn "$me _items_cust_bill_pkg adding setup\n"
2785 my $description = $desc;
2786 $description .= ' Setup'
2787 if $cust_bill_pkg->recur != 0
2788 || $discount_show_always
2789 || $cust_bill_pkg->recur_show_zero;
2791 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2793 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2794 # triggered by the recurring charge freq
2796 && $cust_bill_pkg->recur == 0
2797 && ! $cust_bill_pkg->recur_show_zero;
2802 # always pass the svc_label through to the template, even if
2803 # not displaying it as an ext_description
2804 my @svc_labels = map &{$escape_function}($_),
2805 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2807 $svc_label = $svc_labels[0];
2809 unless ( $cust_pkg->part_pkg->hide_svc_detail
2810 || $cust_bill_pkg->hidden )
2813 push @d, @svc_labels
2814 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2815 my $lnum = $cust_main ? $cust_main->ship_locationnum
2816 : $self->prospect_main->locationnum;
2817 # show the location label if it's not the customer's default
2818 # location, and we're not grouping items by location already
2819 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2820 my $loc = $cust_pkg->location_label;
2821 $loc = substr($loc, 0, $maxlength). '...'
2822 if $format eq 'latex' && length($loc) > $maxlength;
2823 push @d, &{$escape_function}($loc);
2826 } #unless hiding service details
2828 push @d, $cust_bill_pkg->details(%details_opt)
2829 if $cust_bill_pkg->recur == 0;
2831 if ( $cust_bill_pkg->hidden ) {
2832 $s->{amount} += $cust_bill_pkg->setup;
2833 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2834 push @{ $s->{ext_description} }, @d;
2838 description => $description,
2839 pkgpart => $pkgpart,
2840 pkgnum => $cust_bill_pkg->pkgnum,
2841 amount => $cust_bill_pkg->setup,
2842 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2843 unit_amount => $cust_bill_pkg->unitsetup,
2844 quantity => $cust_bill_pkg->quantity,
2845 ext_description => \@d,
2846 svc_label => ($svc_label || ''),
2847 locationnum => $cust_pkg->locationnum, # sure, why not?
2853 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2855 $cust_bill_pkg->recur != 0
2856 || $cust_bill_pkg->setup == 0
2857 || $discount_show_always
2858 || $cust_bill_pkg->recur_show_zero
2863 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2866 my $is_summary = $display->summary;
2867 my $description = $desc;
2868 if ( $type eq 'U' and defined($r) ) {
2869 # don't just show the same description as the recur line
2870 $description = $self->mt('Usage charges');
2873 my $part_pkg = $cust_pkg->part_pkg;
2875 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2879 my @seconds = (); # for display of usage info
2882 #at least until cust_bill_pkg has "past" ranges in addition to
2883 #the "future" sdate/edate ones... see #3032
2884 my @dates = ( $self->_date );
2885 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2886 push @dates, $prev->sdate if $prev;
2887 push @dates, undef if !$prev;
2889 my @svc_labels = map &{$escape_function}($_),
2890 $cust_pkg->h_labels_short(@dates, 'I');
2891 $svc_label = $svc_labels[0];
2893 # show service labels, unless...
2894 # the package is set not to display them
2895 unless ( $part_pkg->hide_svc_detail
2896 # or this is a tax-like line item
2897 || $cust_bill_pkg->itemdesc
2898 # or this is a hidden (bundled) line item
2899 || $cust_bill_pkg->hidden
2900 # or this is a usage summary line
2901 || $is_summary && $type && $type eq 'U'
2902 # or this is a usage line and there's a recurring line
2903 # for the package in the same section (which will
2904 # have service labels already)
2905 || ($type eq 'U' and defined($r))
2909 warn "$me _items_cust_bill_pkg adding service details\n"
2912 push @d, @svc_labels
2913 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2914 warn "$me _items_cust_bill_pkg done adding service details\n"
2917 my $lnum = $cust_main ? $cust_main->ship_locationnum
2918 : $self->prospect_main->locationnum;
2919 # show the location label if it's not the customer's default
2920 # location, and we're not grouping items by location already
2921 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2922 my $loc = $cust_pkg->location_label;
2923 $loc = substr($loc, 0, $maxlength). '...'
2924 if $format eq 'latex' && length($loc) > $maxlength;
2925 push @d, &{$escape_function}($loc);
2928 # Display of seconds_since_sqlradacct:
2929 # On the invoice, when processing @detail_items, look for a field
2930 # named 'seconds'. This will contain total seconds for each
2931 # service, in the same order as @ext_description. For services
2932 # that don't support this it will show undef.
2933 if ( $conf->exists('svc_acct-usage_seconds')
2934 and ! $cust_bill_pkg->pkgpart_override ) {
2935 foreach my $cust_svc (
2936 $cust_pkg->h_cust_svc(@dates, 'I')
2939 # eval because not having any part_export_usage exports
2940 # is a fatal error, last_bill/_date because that's how
2941 # sqlradius_hour billing does it
2943 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2945 push @seconds, $sec;
2947 } #if svc_acct-usage_seconds
2949 } # if we are showing service labels
2951 unless ( $is_summary ) {
2952 warn "$me _items_cust_bill_pkg adding details\n"
2955 #instead of omitting details entirely in this case (unwanted side
2956 # effects), just omit CDRs
2957 $details_opt{'no_usage'} = 1
2958 if $type && $type eq 'R';
2960 push @d, $cust_bill_pkg->details(%details_opt);
2963 warn "$me _items_cust_bill_pkg calculating amount\n"
2968 $amount = $cust_bill_pkg->recur;
2969 } elsif ($type eq 'R') {
2970 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2971 } elsif ($type eq 'U') {
2972 $amount = $cust_bill_pkg->usage;
2975 if ( !$type || $type eq 'R' ) {
2977 warn "$me _items_cust_bill_pkg adding recur\n"
2981 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2984 if ( $cust_bill_pkg->hidden ) {
2985 $r->{amount} += $amount;
2986 $r->{unit_amount} += $unit_amount;
2987 push @{ $r->{ext_description} }, @d;
2990 description => $description,
2991 pkgpart => $pkgpart,
2992 pkgnum => $cust_bill_pkg->pkgnum,
2994 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2995 unit_amount => $unit_amount,
2996 quantity => $cust_bill_pkg->quantity,
2998 ext_description => \@d,
2999 svc_label => ($svc_label || ''),
3000 locationnum => $cust_pkg->locationnum,
3002 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3005 } else { # $type eq 'U'
3007 warn "$me _items_cust_bill_pkg adding usage\n"
3010 if ( $cust_bill_pkg->hidden and defined($u) ) {
3011 # if this is a hidden package and there's already a usage
3012 # line for the bundle, add this package's total amount and
3013 # usage details to it
3014 $u->{amount} += $amount;
3015 push @{ $u->{ext_description} }, @d;
3016 } elsif ( $amount ) {
3017 # create a new usage line
3019 description => $description,
3020 pkgpart => $pkgpart,
3021 pkgnum => $cust_bill_pkg->pkgnum,
3024 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3026 ext_description => \@d,
3027 locationnum => $cust_pkg->locationnum,
3029 } # else this has no usage, so don't create a usage section
3032 } # recurring or usage with recurring charge
3034 # decide whether to show active discounts here
3036 # case 1: we are showing a single line for the package
3038 # case 2: we are showing a setup line for a package that has
3039 # no base recurring fee
3040 or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3041 # case 3: we are showing a recur line for a package that has
3042 # a base recurring fee
3043 or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3046 my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
3047 # special case: if there are old "discount details" on this line
3048 # item, don't show discount line items
3049 if ( FS::cust_bill_pkg_detail->count(
3050 "detail LIKE 'Includes discount%' AND billpkgnum = " .
3051 $cust_bill_pkg->billpkgnum
3056 warn "$me _items_cust_bill_pkg including discounts for ".
3057 $cust_bill_pkg->billpkgnum."\n"
3059 my $discount_amount = sum( map {$_->amount} @discounts );
3060 my $orig_amount = $cust_bill_pkg->setup + $cust_bill_pkg->recur
3062 # if multiple discounts apply to the same package, how to display
3063 # them? ext_description lines, apparently
3064 if ( $d and $cust_bill_pkg->hidden ) {
3065 $d->{amount} += $discount_amount;
3066 $d->{orig_amount} += $orig_amount;
3069 # make a placeholder for the original price, if necessary
3070 # (if unit prices are enabled, it won't be necessary)
3071 push @ext, '' if !$conf->exists('invoice-unitprice');
3074 description => $self->mt('Discount included'),
3075 amount => $discount_amount,
3076 orig_amount => $orig_amount,
3077 ext_description => \@ext,
3079 foreach my $cust_bill_pkg_discount (@discounts) {
3080 my $def = $cust_bill_pkg_discount->cust_pkg_discount->discount;
3081 push @ext, &{$escape_function}( $def->description );
3085 # update the placeholder to show the original price in the
3086 # first ext_description line
3087 if ( !$conf->exists('invoice-unitprice') ) {
3088 $d->{ext_description}->[0] =
3089 sprintf('Original price: %.2f', $d->{orig_amount});
3091 } # if there are any discounts
3092 } # if this is an appropriate place to show discounts
3094 } else { # taxes and fees
3096 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3099 # items of this kind should normally not have sdate/edate.
3101 'description' => $desc,
3102 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
3103 + $cust_bill_pkg->recur)
3106 } # if quotation / package line item / other line item
3108 } # foreach $display
3110 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
3111 && $conf->exists('discount-show-always'));
3115 foreach ( $s, $r, ($opt{skip_usage} ? () : $u, $d ) ) {
3117 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3118 if exists($_->{amount});
3119 $_->{amount} =~ s/^\-0\.00$/0.00/;
3120 if (exists($_->{unit_amount})) {
3121 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3125 if $_->{amount} != 0
3126 || $discount_show_always
3127 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3128 || ( $_->{_is_setup} && $_->{setup_show_zero} )
3132 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3139 =item _items_discounts_avail
3141 Returns an array of line item hashrefs representing available term discounts
3142 for this invoice. This makes the same assumptions that apply to term
3143 discounts in general: that the package is billed monthly, at a flat rate,
3144 with no usage charges. A prorated first month will be handled, as will
3145 a setup fee if the discount is allowed to apply to setup fees.
3149 sub _items_discounts_avail {
3152 #maybe move this method from cust_bill when quotations support discount_plans
3153 return () unless $self->can('discount_plans');
3154 my %plans = $self->discount_plans;
3156 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3157 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3161 my $plan = $plans{$months};
3163 my $term_total = sprintf('%.2f', $plan->discounted_total);
3164 my $percent = sprintf('%.0f',
3165 100 * (1 - $term_total / $plan->base_total) );
3166 my $permonth = sprintf('%.2f', $term_total / $months);
3167 my $detail = $self->mt('discount on item'). ' '.
3168 join(', ', map { "#$_" } $plan->pkgnums)
3171 # discounts for non-integer months don't work anyway
3172 $months = sprintf("%d", $months);
3175 description => $self->mt('Save [_1]% by paying for [_2] months',
3177 amount => $self->mt('[_1] ([_2] per month)',
3178 $term_total, $money_char.$permonth),
3179 ext_description => ($detail || ''),
3182 sort { $b <=> $a } keys %plans;