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 on the next release.
350 warn "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 map "$_ WHERE _date <= ? AND custnum = ?", (
707 "SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill",
708 "SELECT -1 * COALESCE( SUM(amount), 0 ) FROM cust_credit",
709 "SELECT -1 * COALESCE( SUM(paid), 0 ) FROM cust_pay",
710 "SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund",
713 # the customer's current balance immediately after generating the last
716 my $last_bill_balance = $last_bill->charged;
718 my $delta = FS::Record->scalar_sql(
720 $last_bill->_date - 1,
723 $last_bill_balance += $delta;
726 $last_bill_balance = sprintf("%.2f", $last_bill_balance);
728 warn sprintf("LAST BILL: INVNUM %d, DATE %s, BALANCE %.2f\n\n",
730 $self->time2str_local('%D', $last_bill->_date),
733 # ("true_previous_balance" is a terrible name, but at least it's no
734 # longer stored in the database)
735 $invoice_data{'true_previous_balance'} = $last_bill_balance;
737 # the change in balance from immediately after that invoice
738 # to immediately before this one
739 my $before_this_bill_balance = 0;
741 my $delta = FS::Record->scalar_sql(
746 $before_this_bill_balance += $delta;
748 $invoice_data{'balance_adjustments'} =
749 sprintf("%.2f", $last_bill_balance - $before_this_bill_balance);
751 warn sprintf("BALANCE ADJUSTMENTS: %.2f\n\n",
752 $invoice_data{'balance_adjustments'}
755 # the sum of amount owed on all previous invoices
756 # ($pr_total is used elsewhere but not as $previous_balance)
757 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
759 $invoice_data{'last_bill'} = {
760 '_date' => $last_bill->_date, #unformatted
762 my (@payments, @credits);
763 # for formats that itemize previous payments
764 foreach my $cust_pay ( qsearch('cust_pay', {
765 'custnum' => $self->custnum,
766 '_date' => { op => '>=',
767 value => $last_bill->_date }
770 next if $cust_pay->_date > $self->_date;
772 '_date' => $cust_pay->_date,
773 'date' => $self->time2str_local('long', $cust_pay->_date, $format),
774 'payinfo' => $cust_pay->payby_payinfo_pretty,
775 'amount' => sprintf('%.2f', $cust_pay->paid),
777 # not concerned about applications
779 foreach my $cust_credit ( qsearch('cust_credit', {
780 'custnum' => $self->custnum,
781 '_date' => { op => '>=',
782 value => $last_bill->_date }
785 next if $cust_credit->_date > $self->_date;
787 '_date' => $cust_credit->_date,
788 'date' => $self->time2str_local('long', $cust_credit->_date, $format),
789 'creditreason'=> $cust_credit->reason,
790 'amount' => sprintf('%.2f', $cust_credit->amount),
793 $invoice_data{'previous_payments'} = \@payments;
794 $invoice_data{'previous_credits'} = \@credits;
796 # there is no $last_bill
797 $invoice_data{'true_previous_balance'} =
798 $invoice_data{'balance_adjustments'} =
799 $invoice_data{'previous_balance'} = '0.00';
800 $invoice_data{'previous_payments'} = [];
801 $invoice_data{'previous_credits'} = [];
803 } # if this is an invoice
805 my $summarypage = '';
806 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
809 $invoice_data{'summarypage'} = $summarypage;
811 warn "$me substituting variables in notes, footer, smallfooter\n"
814 my $tc = $self->template_conf;
815 my @include = ( [ $tc, 'notes' ],
816 [ 'invoice_', 'footer' ],
817 [ 'invoice_', 'smallfooter', ],
819 push @include, [ $tc, 'coupon', ]
820 unless $params{'no_coupon'};
822 foreach my $i (@include) {
824 my($base, $include) = @$i;
826 my $inc_file = $conf->key_orbase("$base$format$include", $template);
829 if ( $conf->exists($inc_file, $agentnum)
830 && length( $conf->config($inc_file, $agentnum) ) ) {
832 @inc_src = $conf->config($inc_file, $agentnum);
836 $inc_file = $conf->key_orbase("${base}latex$include", $template);
838 my $convert_map = $convert_maps{$format}{$include};
840 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
841 s/--\@\]/$delimiters{$format}[1]/g;
844 &$convert_map( $conf->config($inc_file, $agentnum) );
848 my $inc_tt = new Text::Template (
850 SOURCE => [ map "$_\n", @inc_src ],
851 DELIMITERS => $delimiters{$format},
852 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
854 unless ( $inc_tt->compile() ) {
855 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
856 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
860 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
862 $invoice_data{$include} =~ s/\n+$//
863 if ($format eq 'latex');
866 # let invoices use either of these as needed
867 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
868 ? $cust_main->payinfo : '';
869 $invoice_data{'po_line'} =
870 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
871 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
874 my %money_chars = ( 'latex' => '',
875 'html' => $conf->config('money_char') || '$',
878 my $money_char = $money_chars{$format};
881 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
882 'html' => $conf->config('money_char') || '$',
885 my $other_money_char = $other_money_chars{$format};
886 $invoice_data{'dollar'} = $other_money_char;
888 my %minus_signs = ( 'latex' => '$-$',
890 'template' => '- ' );
891 my $minus = $minus_signs{$format};
893 my @detail_items = ();
894 my @total_items = ();
898 $invoice_data{'detail_items'} = \@detail_items;
899 $invoice_data{'total_items'} = \@total_items;
900 $invoice_data{'buf'} = \@buf;
901 $invoice_data{'sections'} = \@sections;
903 warn "$me generating sections\n"
907 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
908 'subtotal' => $taxtotal, # adjusted below
911 my $tax_weight = _pkg_category($tax_section->{description})
912 ? _pkg_category($tax_section->{description})->weight
914 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
915 $tax_section->{'sort_weight'} = $tax_weight;
918 my $adjust_section = {
919 'description' => $self->mt('Credits, Payments, and Adjustments'),
920 'adjust_section' => 1,
921 'subtotal' => 0, # adjusted below
923 my $adjust_weight = _pkg_category($adjust_section->{description})
924 ? _pkg_category($adjust_section->{description})->weight
926 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
927 $adjust_section->{'sort_weight'} = $adjust_weight;
929 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
930 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
931 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
932 $invoice_data{'multisection'} = $multisection;
934 my $extra_sections = [];
935 my $extra_lines = ();
937 # default section ('Charges')
938 my $default_section = { 'description' => '',
943 # Previous Charges section
944 # subtotal is the first return value from $self->previous
945 my $previous_section;
946 # if the invoice has major sections, or if we're summarizing previous
947 # charges with a single line, or if we've been specifically told to put them
948 # in a section, create a section for previous charges:
949 if ( $multisection or
950 $conf->exists('previous_balance-summary_only') or
951 $conf->exists('previous_balance-section') ) {
953 $previous_section = { 'description' => $self->mt('Previous Charges'),
954 'subtotal' => $other_money_char.
955 sprintf('%.2f', $pr_total),
956 'summarized' => '', #why? $summarypage ? 'Y' : '',
958 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
959 join(' / ', map { $cust_main->balance_date_range(@$_) }
960 $self->_prior_month30s
962 if $conf->exists('invoice_include_aging');
965 # otherwise put them in the main section
966 $previous_section = $default_section;
969 if ( $multisection ) {
970 ($extra_sections, $extra_lines) =
971 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
972 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
973 && $self->can('_items_extra_usage_sections');
975 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
977 push @detail_items, @$extra_lines if $extra_lines;
979 # the code is written so that both methods can be used together, but
980 # we haven't yet changed the template to take advantage of that, so for
981 # now, treat them as mutually exclusive.
982 my %section_method = ( by_category => 1 );
983 if ( $conf->config($tc.'sections_method') eq 'location' ) {
984 %section_method = ( by_location => 1 );
987 $self->_items_sections( 'summary' => $summarypage,
988 'escape' => $escape_function_nonbsp,
989 'extra_sections' => $extra_sections,
993 push @sections, @$early;
994 $late_sections = $late;
996 if ( $conf->exists('svc_phone_sections')
997 && $self->can('_items_svc_phone_sections')
1000 my ($phone_sections, $phone_lines) =
1001 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
1002 push @{$late_sections}, @$phone_sections;
1003 push @detail_items, @$phone_lines;
1005 if ( $conf->exists('voip-cust_accountcode_cdr')
1006 && $cust_main->accountcode_cdr
1007 && $self->can('_items_accountcode_cdr')
1010 my ($accountcode_section, $accountcode_lines) =
1011 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
1012 if ( scalar(@$accountcode_lines) ) {
1013 push @{$late_sections}, $accountcode_section;
1014 push @detail_items, @$accountcode_lines;
1017 } else {# not multisection
1018 # make a default section
1019 push @sections, $default_section;
1020 # and calculate the finance charge total, since it won't get done otherwise.
1021 # and the default section total
1022 # XXX possibly finance_pkgclass should not be used in this manner?
1023 my @finance_charges;
1025 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1026 if ( $invoice_data{finance_section} and
1027 grep { $_->section eq $invoice_data{finance_section} }
1028 $cust_bill_pkg->cust_bill_pkg_display ) {
1029 # I think these are always setup fees, but just to be sure...
1030 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1032 push @charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
1035 $invoice_data{finance_amount} =
1036 sprintf('%.2f', sum( @finance_charges ) || 0);
1037 $default_section->{subtotal} = $other_money_char.
1038 sprintf('%.2f', sum( @charges ) || 0);
1041 # start setting up summary subtotals
1042 my @summary_subtotals;
1043 my $method = $conf->config('summary_subtotals_method');
1044 if ( $method and $method ne $conf->config($tc.'sections_method') ) {
1045 # then re-section them by the correct method
1046 my %section_method = ( by_category => 1 );
1047 if ( $conf->config('summary_subtotals_method') eq 'location' ) {
1048 %section_method = ( by_location => 1 );
1050 my ($early, $late) =
1051 $self->_items_sections( 'summary' => $summarypage,
1052 'escape' => $escape_function_nonbsp,
1053 'extra_sections' => $extra_sections,
1054 'format' => $format,
1057 foreach ( @$early ) {
1058 next if $_->{subtotal} == 0;
1059 $_->{subtotal} = $other_money_char.sprintf('%.2f', $_->{subtotal});
1060 push @summary_subtotals, $_;
1063 # subtotal sectioning is the same as for the actual invoice sections
1064 @summary_subtotals = @sections;
1067 # Hereafter, push sections to both @sections and @summary_subtotals
1068 # if they belong in both places (e.g. tax section). Late sections are
1069 # never in @summary_subtotals.
1071 # previous invoice balances in the Previous Charges section if there
1072 # is one, otherwise in the main detail section
1073 # (except if summary_only is enabled, don't show them at all)
1074 if ( $self->can('_items_previous') &&
1075 $self->enable_previous &&
1076 ! $conf->exists('previous_balance-summary_only') ) {
1078 warn "$me adding previous balances\n"
1081 foreach my $line_item ( $self->_items_previous ) {
1084 ref => $line_item->{'pkgnum'},
1085 pkgpart => $line_item->{'pkgpart'},
1086 #quantity => 1, # not really correct
1087 section => $previous_section, # which might be $default_section
1088 description => &$escape_function($line_item->{'description'}),
1089 ext_description => [ map { &$escape_function($_) }
1090 @{ $line_item->{'ext_description'} || [] }
1092 amount => $money_char . $line_item->{'amount'},
1093 product_code => $line_item->{'pkgpart'} || 'N/A',
1096 push @detail_items, $detail;
1097 push @buf, [ $detail->{'description'},
1098 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1104 if ( @pr_cust_bill && $self->enable_previous ) {
1105 push @buf, ['','-----------'];
1106 push @buf, [ $self->mt('Total Previous Balance'),
1107 $money_char. sprintf("%10.2f", $pr_total) ];
1111 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
1112 warn "$me adding DID summary\n"
1115 my ($didsummary,$minutes) = $self->_did_summary;
1116 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
1118 { 'description' => $didsummary_desc,
1119 'ext_description' => [ $didsummary, $minutes ],
1123 foreach my $section (@sections, @$late_sections) {
1125 # begin some normalization
1126 $section->{'subtotal'} = $section->{'amount'}
1128 && !exists($section->{subtotal})
1129 && exists($section->{amount});
1131 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
1132 if ( $invoice_data{finance_section} &&
1133 $section->{'description'} eq $invoice_data{finance_section} );
1135 $section->{'subtotal'} = $other_money_char.
1136 sprintf('%.2f', $section->{'subtotal'})
1139 # continue some normalization
1140 $section->{'amount'} = $section->{'subtotal'}
1144 if ( $section->{'description'} ) {
1145 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
1150 warn "$me setting options\n"
1154 $options{'section'} = $section if $multisection;
1155 $options{'format'} = $format;
1156 $options{'escape_function'} = $escape_function;
1157 $options{'no_usage'} = 1 unless $unsquelched;
1158 $options{'unsquelched'} = $unsquelched;
1159 $options{'summary_page'} = $summarypage;
1160 $options{'skip_usage'} =
1161 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
1162 $options{'preref_callback'} = $params{'preref_callback'};
1164 warn "$me searching for line items\n"
1167 foreach my $line_item ( $self->_items_pkg(%options),
1168 $self->_items_fee(%options) ) {
1170 warn "$me adding line item ".
1171 join(', ', map "$_=>".$line_item->{$_}, keys %$line_item). "\n"
1174 $line_item->{'ref'} = $line_item->{'pkgnum'};
1175 $line_item->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; # mt()?
1176 $line_item->{'section'} = $section;
1177 $line_item->{'description'} = &$escape_function($line_item->{'description'});
1178 $line_item->{'amount'} = $money_char.$line_item->{'amount'};
1180 if ( length($line_item->{'unit_amount'}) ) {
1181 $line_item->{'unit_amount'} = $money_char.$line_item->{'unit_amount'};
1183 $line_item->{'ext_description'} ||= [];
1185 push @detail_items, $line_item;
1186 push @buf, ( [ $line_item->{'description'},
1187 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1189 map { [ " ". $_, '' ] } @{$line_item->{'ext_description'}},
1193 if ( $section->{'description'} ) {
1194 push @buf, ( ['','-----------'],
1195 [ $section->{'description'}. ' sub-total',
1196 $section->{'subtotal'} # already formatted this
1205 $invoice_data{current_less_finance} =
1206 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1208 # if there's anything in the Previous Charges section, prepend it to the list
1209 if ( $pr_total and $previous_section ne $default_section ) {
1210 unshift @sections, $previous_section;
1211 # but not @summary_subtotals
1214 warn "$me adding taxes\n"
1217 my @items_tax = $self->_items_tax;
1218 foreach my $tax ( @items_tax ) {
1220 $taxtotal += $tax->{'amount'};
1222 my $description = &$escape_function( $tax->{'description'} );
1223 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1225 if ( $multisection ) {
1227 push @detail_items, {
1228 ext_description => [],
1231 description => $description,
1232 amount => $money_char. $amount,
1234 section => $tax_section,
1239 push @total_items, {
1240 'total_item' => $description,
1241 'total_amount' => $other_money_char. $amount,
1246 push @buf,[ $description,
1247 $money_char. $amount,
1254 $total->{'total_item'} = $self->mt('Sub-total');
1255 $total->{'total_amount'} =
1256 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1258 if ( $multisection ) {
1259 $tax_section->{'subtotal'} = $other_money_char.
1260 sprintf('%.2f', $taxtotal);
1261 $tax_section->{'pretotal'} = 'New charges sub-total '.
1262 $total->{'total_amount'};
1264 push @sections, $tax_section;
1265 push @summary_subtotals, $tax_section;
1268 unshift @total_items, $total;
1271 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1273 push @buf,['','-----------'];
1274 push @buf,[$self->mt(
1275 (!$self->enable_previous)
1277 : 'Total New Charges'
1279 $money_char. sprintf("%10.2f",$self->charged) ];
1287 my %embolden_functions = (
1288 'latex' => sub { return '\textbf{'. shift(). '}' },
1289 'html' => sub { return '<b>'. shift(). '</b>' },
1290 'template' => sub { shift },
1292 my $embolden_function = $embolden_functions{$format};
1294 if ( $self->can('_items_total') ) { # quotations
1296 $self->_items_total(\@total_items);
1298 foreach ( @total_items ) {
1299 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1300 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1301 $_->{'total_amount'}
1305 } else { #normal invoice case
1307 # calculate total, possibly including total owed on previous
1311 $item = $conf->config('previous_balance-exclude_from_total')
1312 || 'Total New Charges'
1313 if $conf->exists('previous_balance-exclude_from_total');
1314 my $amount = $self->charged;
1315 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1316 $amount += $pr_total;
1319 $total->{'total_item'} = &$embolden_function($self->mt($item));
1320 $total->{'total_amount'} =
1321 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1322 if ( $multisection ) {
1323 if ( $adjust_section->{'sort_weight'} ) {
1324 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1325 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1327 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1328 $other_money_char. sprintf('%.2f', $self->charged );
1331 push @total_items, $total;
1333 push @buf,['','-----------'];
1336 sprintf( '%10.2f', $amount )
1340 # if we're showing previous invoices, also show previous
1341 # credits and payments
1342 if ( $self->enable_previous
1343 and $self->can('_items_credits')
1344 and $self->can('_items_payments') )
1346 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1349 my $credittotal = 0;
1350 foreach my $credit (
1351 $self->_items_credits( 'template' => $template, 'trim_len' => 60 )
1355 $total->{'total_item'} = &$escape_function($credit->{'description'});
1356 $credittotal += $credit->{'amount'};
1357 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1358 $adjusttotal += $credit->{'amount'};
1359 if ( $multisection ) {
1360 push @detail_items, {
1361 ext_description => [],
1364 description => &$escape_function($credit->{'description'}),
1365 amount => $money_char . $credit->{'amount'},
1367 section => $adjust_section,
1370 push @total_items, $total;
1374 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1377 foreach my $credit (
1378 $self->_items_credits( 'template' => $template, 'trim_len'=>32 )
1380 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1384 my $paymenttotal = 0;
1385 foreach my $payment (
1386 $self->_items_payments( 'template' => $template )
1389 $total->{'total_item'} = &$escape_function($payment->{'description'});
1390 $paymenttotal += $payment->{'amount'};
1391 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1392 $adjusttotal += $payment->{'amount'};
1393 if ( $multisection ) {
1394 push @detail_items, {
1395 ext_description => [],
1398 description => &$escape_function($payment->{'description'}),
1399 amount => $money_char . $payment->{'amount'},
1401 section => $adjust_section,
1404 push @total_items, $total;
1406 push @buf, [ $payment->{'description'},
1407 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1410 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1412 if ( $multisection ) {
1413 $adjust_section->{'subtotal'} = $other_money_char.
1414 sprintf('%.2f', $adjusttotal);
1415 push @sections, $adjust_section
1416 unless $adjust_section->{sort_weight};
1417 # do not summarize; adjustments there are shown according to
1421 # create Balance Due message
1424 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1425 $total->{'total_amount'} =
1426 &$embolden_function(
1427 $other_money_char. sprintf('%.2f', #why? $summarypage
1428 # ? $self->charged +
1429 # $self->billing_balance
1431 $self->owed + $pr_total
1434 if ( $multisection && !$adjust_section->{sort_weight} ) {
1435 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1436 $total->{'total_amount'};
1438 push @total_items, $total;
1440 push @buf,['','-----------'];
1441 push @buf,[$self->balance_due_msg, $money_char.
1442 sprintf("%10.2f", $balance_due ) ];
1445 if ( $conf->exists('previous_balance-show_credit')
1446 and $cust_main->balance < 0 ) {
1447 my $credit_total = {
1448 'total_item' => &$embolden_function($self->credit_balance_msg),
1449 'total_amount' => &$embolden_function(
1450 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1453 if ( $multisection ) {
1454 $adjust_section->{'posttotal'} .= $newline_token .
1455 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1458 push @total_items, $credit_total;
1460 push @buf,['','-----------'];
1461 push @buf,[$self->credit_balance_msg, $money_char.
1462 sprintf("%10.2f", -$cust_main->balance ) ];
1466 } #end of default total adding ! can('_items_total')
1468 if ( $multisection ) {
1469 if ( $conf->exists('svc_phone_sections')
1470 && $self->can('_items_svc_phone_sections')
1474 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1475 $total->{'total_amount'} =
1476 &$embolden_function(
1477 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1479 my $last_section = pop @sections;
1480 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1481 $total->{'total_amount'};
1482 push @sections, $last_section;
1484 push @sections, @$late_sections
1488 # make a discounts-available section, even without multisection
1489 if ( $conf->exists('discount-show_available')
1490 and my @discounts_avail = $self->_items_discounts_avail ) {
1491 my $discount_section = {
1492 'description' => $self->mt('Discounts Available'),
1497 push @sections, $discount_section; # do not summarize
1498 push @detail_items, map { +{
1499 'ref' => '', #should this be something else?
1500 'section' => $discount_section,
1501 'description' => &$escape_function( $_->{description} ),
1502 'amount' => $money_char . &$escape_function( $_->{amount} ),
1503 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1504 } } @discounts_avail;
1507 # not adding any more sections after this
1508 $invoice_data{summary_subtotals} = \@summary_subtotals;
1511 if ( $conf->exists('usage_class_summary')
1512 and $self->can('_items_usage_class_summary') ) {
1513 my @usage_subtotals = $self->_items_usage_class_summary(escape => $escape_function);
1514 if ( @usage_subtotals ) {
1515 unshift @sections, $usage_subtotals[0]->{section}; # do not summarize
1516 unshift @detail_items, @usage_subtotals;
1520 # invoice history "section" (not really a section)
1521 # not to be included in any subtotals, completely independent of
1523 if ( $conf->exists('previous_invoice_history') ) {
1526 foreach my $cust_bill ( $cust_main->cust_bill ) {
1527 # XXX hardcoded format, and currently only 'charged'; add other fields
1528 # if they become necessary
1529 my $date = $self->time2str_local('%b %Y', $cust_bill->_date);
1530 $history{$date} ||= 0;
1531 $history{$date} += $cust_bill->charged;
1532 # just so we have a numeric sort key
1533 $monthorder{$date} ||= $cust_bill->_date;
1535 my @sorted_months = sort { $monthorder{$a} <=> $monthorder{$b} }
1537 my @sorted_amounts = map { sprintf('%.2f', $history{$_}) } @sorted_months;
1538 $invoice_data{monthly_history} = [ \@sorted_months, \@sorted_amounts ];
1541 # service locations: another option for template customization
1543 foreach my $item (@detail_items) {
1544 if ( $item->{locationnum} ) {
1545 $location_info{ $item->{locationnum} } ||= {
1546 FS::cust_location->by_key( $item->{locationnum} )->location_hash
1550 $invoice_data{location_info} = \%location_info;
1552 # debugging hook: call this with 'diag' => 1 to just get a hash of
1553 # the invoice variables
1554 return \%invoice_data if ( $params{'diag'} );
1556 # All sections and items are built; now fill in templates.
1557 my @includelist = ();
1558 push @includelist, 'summary' if $summarypage;
1559 foreach my $include ( @includelist ) {
1561 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1564 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1566 @inc_src = $conf->config($inc_file, $agentnum);
1570 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1572 my $convert_map = $convert_maps{$format}{$include};
1574 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1575 s/--\@\]/$delimiters{$format}[1]/g;
1578 &$convert_map( $conf->config($inc_file, $agentnum) );
1582 my $inc_tt = new Text::Template (
1584 SOURCE => [ map "$_\n", @inc_src ],
1585 DELIMITERS => $delimiters{$format},
1586 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1588 unless ( $inc_tt->compile() ) {
1589 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1590 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1594 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1596 $invoice_data{$include} =~ s/\n+$//
1597 if ($format eq 'latex');
1602 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1603 /invoice_lines\((\d*)\)/;
1604 $invoice_lines += $1 || scalar(@buf);
1607 die "no invoice_lines() functions in template?"
1608 if ( $format eq 'template' && !$wasfunc );
1610 if ($format eq 'template') {
1612 if ( $invoice_lines ) {
1613 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1614 $invoice_data{'total_pages'}++
1615 if scalar(@buf) % $invoice_lines;
1618 #setup subroutine for the template
1619 $invoice_data{invoice_lines} = sub {
1620 my $lines = shift || scalar(@buf);
1632 push @collect, split("\n",
1633 $text_template->fill_in( HASH => \%invoice_data )
1635 $invoice_data{'page'}++;
1637 map "$_\n", @collect;
1639 } else { # this is where we actually create the invoice
1641 warn "filling in template for invoice ". $self->invnum. "\n"
1643 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1646 $text_template->fill_in(HASH => \%invoice_data);
1650 sub notice_name { '('.shift->table.')'; }
1652 sub template_conf { 'invoice_'; }
1654 # helper routine for generating date ranges
1655 sub _prior_month30s {
1658 [ 1, 2592000 ], # 0-30 days ago
1659 [ 2592000, 5184000 ], # 30-60 days ago
1660 [ 5184000, 7776000 ], # 60-90 days ago
1661 [ 7776000, 0 ], # 90+ days ago
1664 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1665 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1670 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1672 Returns an postscript invoice, as a scalar.
1674 Options can be passed as a hashref (recommended) or as a list of time, template
1675 and then any key/value pairs for any other options.
1677 I<time> an optional value used to control the printing of overdue messages. The
1678 default is now. It isn't the date of the invoice; that's the `_date' field.
1679 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1680 L<Time::Local> and L<Date::Parse> for conversion functions.
1682 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1689 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1690 my $ps = generate_ps($file);
1692 unlink($barcodefile) if $barcodefile;
1697 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1699 Returns an PDF invoice, as a scalar.
1701 Options can be passed as a hashref (recommended) or as a list of time, template
1702 and then any key/value pairs for any other options.
1704 I<time> an optional value used to control the printing of overdue messages. The
1705 default is now. It isn't the date of the invoice; that's the `_date' field.
1706 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1707 L<Time::Local> and L<Date::Parse> for conversion functions.
1709 I<template>, if specified, is the name of a suffix for alternate invoices.
1711 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1718 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1719 my $pdf = generate_pdf($file);
1721 unlink($barcodefile) if $barcodefile;
1726 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1728 Returns an HTML invoice, as a scalar.
1730 I<time> an optional value used to control the printing of overdue messages. The
1731 default is now. It isn't the date of the invoice; that's the `_date' field.
1732 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1733 L<Time::Local> and L<Date::Parse> for conversion functions.
1735 I<template>, if specified, is the name of a suffix for alternate invoices.
1737 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1739 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1740 when emailing the invoice as part of a multipart/related MIME email.
1748 %params = %{ shift() };
1752 $params{'format'} = 'html';
1754 $self->print_generic( %params );
1757 # quick subroutine for print_latex
1759 # There are ten characters that LaTeX treats as special characters, which
1760 # means that they do not simply typeset themselves:
1761 # # $ % & ~ _ ^ \ { }
1763 # TeX ignores blanks following an escaped character; if you want a blank (as
1764 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1768 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1769 $value =~ s/([<>])/\$$1\$/g;
1775 encode_entities($value);
1779 sub _html_escape_nbsp {
1780 my $value = _html_escape(shift);
1781 $value =~ s/ +/ /g;
1785 #utility methods for print_*
1787 sub _translate_old_latex_format {
1788 warn "_translate_old_latex_format called\n"
1795 if ( $line =~ /^%%Detail\s*$/ ) {
1797 push @template, q![@--!,
1798 q! foreach my $_tr_line (@detail_items) {!,
1799 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1800 q! $_tr_line->{'description'} .= !,
1801 q! "\\tabularnewline\n~~".!,
1802 q! join( "\\tabularnewline\n~~",!,
1803 q! @{$_tr_line->{'ext_description'}}!,
1807 while ( ( my $line_item_line = shift )
1808 !~ /^%%EndDetail\s*$/ ) {
1809 $line_item_line =~ s/'/\\'/g; # nice LTS
1810 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1811 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1812 push @template, " \$OUT .= '$line_item_line';";
1815 push @template, '}',
1818 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1820 push @template, '[@--',
1821 ' foreach my $_tr_line (@total_items) {';
1823 while ( ( my $total_item_line = shift )
1824 !~ /^%%EndTotalDetails\s*$/ ) {
1825 $total_item_line =~ s/'/\\'/g; # nice LTS
1826 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1827 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1828 push @template, " \$OUT .= '$total_item_line';";
1831 push @template, '}',
1835 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1836 push @template, $line;
1842 warn "$_\n" foreach @template;
1850 my $conf = $self->conf;
1852 #check for an invoice-specific override
1853 return $self->invoice_terms if $self->invoice_terms;
1855 #check for a customer- specific override
1856 my $cust_main = $self->cust_main;
1857 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1861 $agentnum = $cust_main->agentnum;
1862 } elsif ( my $prospect_main = $self->prospect_main ) {
1863 $agentnum = $prospect_main->agentnum;
1866 #use configured default
1867 $conf->config('invoice_default_terms', $agentnum) || '';
1873 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1874 $duedate = $self->_date() + ( $1 * 86400 );
1881 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1884 sub balance_due_msg {
1886 my $msg = $self->mt('Balance Due');
1887 return $msg unless $self->terms;
1888 if ( $self->due_date ) {
1889 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1890 $self->due_date2str('short');
1891 } elsif ( $self->terms ) {
1892 $msg .= ' - '. $self->terms;
1897 sub balance_due_date {
1899 my $conf = $self->conf;
1901 my $terms = $self->terms;
1902 if ( $terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1903 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1908 sub credit_balance_msg {
1910 $self->mt('Credit Balance Remaining')
1915 Returns a string with the date, for example: "3/20/2008", localized for the
1916 customer. Use _date_pretty_unlocalized for non-end-customer display use.
1922 $self->time2str_local('short', $self->_date);
1925 =item _date_pretty_unlocalized
1927 Returns a string with the date, for example: "3/20/2008", in the format
1928 configured for the back-office. Use _date_pretty for end-customer display use.
1932 sub _date_pretty_unlocalized {
1934 time2str($date_format, $self->_date);
1937 =item _items_sections OPTIONS
1939 Generate section information for all items appearing on this invoice.
1940 This will only be called for multi-section invoices.
1942 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1943 related display records (L<FS::cust_bill_pkg_display>) and organize
1944 them into two groups ("early" and "late" according to whether they come
1945 before or after the total), then into sections. A subtotal is calculated
1948 Section descriptions are returned in sort weight order. Each consists
1949 of a hash containing:
1951 description: the package category name, escaped
1952 subtotal: the total charges in that section
1953 tax_section: a flag indicating that the section contains only tax charges
1954 summarized: same as tax_section, for some reason
1955 sort_weight: the package category's sort weight
1957 If 'condense' is set on the display record, it also contains everything
1958 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1959 coderefs to generate parts of the invoice. This is not advised.
1961 The method returns two arrayrefs, one of "early" sections and one of "late"
1964 OPTIONS may include:
1966 by_location: a flag to divide the invoice into sections by location.
1967 Each section hash will have a 'location' element containing a hashref of
1968 the location fields (see L<FS::cust_location>). The section description
1969 will be the location label, but the template can use any of the location
1970 fields to create a suitable label.
1972 by_category: a flag to divide the invoice into sections using display
1973 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1974 behavior. Each section hash will have a 'category' element containing
1975 the section name from the display record (which probably equals the
1976 category name of the package, but may not in some cases).
1978 summary: a flag indicating that this is a summary-format invoice.
1979 Turning this on has the following effects:
1980 - Ignores display items with the 'summary' flag.
1981 - Places all sections in the "early" group even if they have post_total.
1982 - Creates sections for all non-disabled package categories, even if they
1983 have no charges on this invoice, as well as a section with no name.
1985 escape: an escape function to use for section titles.
1987 extra_sections: an arrayref of additional sections to return after the
1988 sorted list. If there are any of these, section subtotals exclude
1991 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1992 passed through to C<_condense_section()>.
1996 use vars qw(%pkg_category_cache);
1997 sub _items_sections {
2001 my $escape = $opt{escape};
2002 my @extra_sections = @{ $opt{extra_sections} || [] };
2004 # $subtotal{$locationnum}{$categoryname} = amount.
2005 # if we're not using by_location, $locationnum is undef.
2006 # if we're not using by_category, you guessed it, $categoryname is undef.
2007 # if we're not using either one, we shouldn't be here in the first place...
2009 my %late_subtotal = ();
2012 # About tax items + multisection invoices:
2013 # If either invoice_*summary option is enabled, AND there is a
2014 # package category with the name of the tax, then there will be
2015 # a display record assigning the tax item to that category.
2017 # However, the taxes are always placed in the "Taxes, Surcharges,
2018 # and Fees" section regardless of that. The only effect of the
2019 # display record is to create a subtotal for the summary page.
2022 my $pkg_hash = $self->cust_pkg_hash;
2024 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2027 my $usage = $cust_bill_pkg->usage;
2030 if ( $opt{by_location} ) {
2031 if ( $cust_bill_pkg->pkgnum ) {
2032 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2037 $locationnum = undef;
2040 # as in _items_cust_pkg, if a line item has no display records,
2041 # cust_bill_pkg_display() returns a default record for it
2043 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2044 next if ( $display->summary && $opt{summary} );
2046 my $section = $display->section;
2047 my $type = $display->type;
2048 # Set $section = undef if we're sectioning by location and this
2049 # line item _has_ a location (i.e. isn't a fee).
2050 $section = undef if $locationnum;
2052 # set this flag if the section is not tax-only
2053 $not_tax{$locationnum}{$section} = 1
2054 if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
2056 # there's actually a very important piece of logic buried in here:
2057 # incrementing $late_subtotal{$section} CREATES
2058 # $late_subtotal{$section}. keys(%late_subtotal) is later used
2059 # to define the list of late sections, and likewise keys(%subtotal).
2060 # When _items_cust_bill_pkg is called to generate line items for
2061 # real, it will be called with 'section' => $section for each
2063 if ( $display->post_total && !$opt{summary} ) {
2064 if (! $type || $type eq 'S') {
2065 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2066 if $cust_bill_pkg->setup != 0
2067 || $cust_bill_pkg->setup_show_zero;
2071 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2072 if $cust_bill_pkg->recur != 0
2073 || $cust_bill_pkg->recur_show_zero;
2076 if ($type && $type eq 'R') {
2077 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2078 if $cust_bill_pkg->recur != 0
2079 || $cust_bill_pkg->recur_show_zero;
2082 if ($type && $type eq 'U') {
2083 $late_subtotal{$locationnum}{$section} += $usage
2084 unless scalar(@extra_sections);
2087 } else { # it's a pre-total (normal) section
2089 # skip tax items unless they're explicitly included in a section
2090 next if $cust_bill_pkg->pkgnum == 0 and
2091 ! $cust_bill_pkg->feepart and
2094 if ( $type eq 'S' ) {
2095 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2096 if $cust_bill_pkg->setup != 0
2097 || $cust_bill_pkg->setup_show_zero;
2098 } elsif ( $type eq 'R' ) {
2099 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2100 if $cust_bill_pkg->recur != 0
2101 || $cust_bill_pkg->recur_show_zero;
2102 } elsif ( $type eq 'U' ) {
2103 $subtotal{$locationnum}{$section} += $usage
2104 unless scalar(@extra_sections);
2105 } elsif ( !$type ) {
2106 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2107 + $cust_bill_pkg->recur;
2116 %pkg_category_cache = ();
2118 # summary invoices need subtotals for all non-disabled package categories,
2119 # even if they're zero
2120 # but currently assume that there are no location sections, or at least
2121 # that the summary page doesn't care about them
2122 if ( $opt{summary} ) {
2123 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2124 $subtotal{''}{$category->categoryname} ||= 0;
2126 $subtotal{''}{''} ||= 0;
2130 foreach my $post_total (0,1) {
2132 my $s = $post_total ? \%late_subtotal : \%subtotal;
2133 foreach my $locationnum (keys %$s) {
2134 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2136 'subtotal' => $s->{$locationnum}{$sectionname},
2137 'post_total' => $post_total,
2140 if ( $locationnum ) {
2141 $section->{'locationnum'} = $locationnum;
2142 my $location = FS::cust_location->by_key($locationnum);
2143 $section->{'description'} = &{ $escape }($location->location_label);
2144 # Better ideas? This will roughly group them by proximity,
2145 # which alpha sorting on any of the address fields won't.
2146 # Sorting by locationnum is meaningless.
2147 # We have to sort on _something_ or the order may change
2148 # randomly from one invoice to the next, which will confuse
2150 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2152 $section->{'location'} = {
2153 label_prefix => &{ $escape }($location->label_prefix),
2154 map { $_ => &{ $escape }($location->get($_)) }
2158 $section->{'category'} = $sectionname;
2159 $section->{'description'} = &{ $escape }($sectionname);
2160 if ( _pkg_category($sectionname) ) {
2161 $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2162 if ( _pkg_category($sectionname)->condense ) {
2163 $section = { %$section, $self->_condense_section($opt{format}) };
2167 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2168 # then it's a tax-only section
2169 $section->{'summarized'} = 'Y';
2170 $section->{'tax_section'} = 'Y';
2172 push @these, $section;
2173 } # foreach $sectionname
2174 } #foreach $locationnum
2175 push @these, @extra_sections if $post_total == 0;
2176 # need an alpha sort for location sections, because postal codes can
2178 $sections[ $post_total ] = [ sort {
2179 $opt{'by_location'} ?
2180 ($a->{sort_weight} cmp $b->{sort_weight}) :
2181 ($a->{sort_weight} <=> $b->{sort_weight})
2183 } #foreach $post_total
2185 return @sections; # early, late
2188 #helper subs for above
2192 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2196 my $categoryname = shift;
2197 $pkg_category_cache{$categoryname} ||=
2198 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2201 my %condensed_format = (
2202 'label' => [ qw( Description Qty Amount ) ],
2204 sub { shift->{description} },
2205 sub { shift->{quantity} },
2206 sub { my($href, %opt) = @_;
2207 ($opt{dollar} || ''). $href->{amount};
2210 'align' => [ qw( l r r ) ],
2211 'span' => [ qw( 5 1 1 ) ], # unitprices?
2212 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2215 sub _condense_section {
2216 my ( $self, $format ) = ( shift, shift );
2218 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2219 qw( description_generator
2222 total_line_generator
2227 sub _condensed_generator_defaults {
2228 my ( $self, $format ) = ( shift, shift );
2229 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2238 sub _condensed_header_generator {
2239 my ( $self, $format ) = ( shift, shift );
2241 my ( $f, $prefix, $suffix, $separator, $column ) =
2242 _condensed_generator_defaults($format);
2244 if ($format eq 'latex') {
2245 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2246 $suffix = "\\\\\n\\hline";
2249 sub { my ($d,$a,$s,$w) = @_;
2250 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2252 } elsif ( $format eq 'html' ) {
2253 $prefix = '<th></th>';
2257 sub { my ($d,$a,$s,$w) = @_;
2258 return qq!<th align="$html_align{$a}">$d</th>!;
2266 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2268 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2271 $prefix. join($separator, @result). $suffix;
2276 sub _condensed_description_generator {
2277 my ( $self, $format ) = ( shift, shift );
2279 my ( $f, $prefix, $suffix, $separator, $column ) =
2280 _condensed_generator_defaults($format);
2282 my $money_char = '$';
2283 if ($format eq 'latex') {
2284 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2286 $separator = " & \n";
2288 sub { my ($d,$a,$s,$w) = @_;
2289 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2291 $money_char = '\\dollar';
2292 }elsif ( $format eq 'html' ) {
2293 $prefix = '"><td align="center"></td>';
2297 sub { my ($d,$a,$s,$w) = @_;
2298 return qq!<td align="$html_align{$a}">$d</td>!;
2300 #$money_char = $conf->config('money_char') || '$';
2301 $money_char = ''; # this is madness
2309 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2311 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2313 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2314 map { $f->{$_}->[$i] } qw(align span width)
2318 $prefix. join( $separator, @result ). $suffix;
2323 sub _condensed_total_generator {
2324 my ( $self, $format ) = ( shift, shift );
2326 my ( $f, $prefix, $suffix, $separator, $column ) =
2327 _condensed_generator_defaults($format);
2330 if ($format eq 'latex') {
2333 $separator = " & \n";
2335 sub { my ($d,$a,$s,$w) = @_;
2336 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2338 }elsif ( $format eq 'html' ) {
2342 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2344 sub { my ($d,$a,$s,$w) = @_;
2345 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2354 # my $r = &{$f->{fields}->[$i]}(@args);
2355 # $r .= ' Total' unless $i;
2357 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2359 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2360 map { $f->{$_}->[$i] } qw(align span width)
2364 $prefix. join( $separator, @result ). $suffix;
2369 =item total_line_generator FORMAT
2371 Returns a coderef used for generation of invoice total line items for this
2372 usage_class. FORMAT is either html or latex
2376 # should not be used: will have issues with hash element names (description vs
2377 # total_item and amount vs total_amount -- another array of functions?
2379 sub _condensed_total_line_generator {
2380 my ( $self, $format ) = ( shift, shift );
2382 my ( $f, $prefix, $suffix, $separator, $column ) =
2383 _condensed_generator_defaults($format);
2386 if ($format eq 'latex') {
2389 $separator = " & \n";
2391 sub { my ($d,$a,$s,$w) = @_;
2392 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2394 }elsif ( $format eq 'html' ) {
2398 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2400 sub { my ($d,$a,$s,$w) = @_;
2401 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2410 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2412 &{$column}( &{$f->{fields}->[$i]}(@args),
2413 map { $f->{$_}->[$i] } qw(align span width)
2417 $prefix. join( $separator, @result ). $suffix;
2422 =item _items_pkg [ OPTIONS ]
2424 Return line item hashes for each package item on this invoice. Nearly
2427 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2429 The only OPTIONS accepted is 'section', which may point to a hashref
2430 with a key named 'condensed', which may have a true value. If it
2431 does, this method tries to merge identical items into items with
2432 'quantity' equal to the number of items (not the sum of their
2433 separate quantities, for some reason).
2439 # The order of these is important. Bundled line items will be merged into
2440 # the most recent non-hidden item, so it needs to be the one with:
2442 # - the same start date
2443 # - no pkgpart_override
2445 # So: sort by pkgnum,
2447 # then sort the base line item before any overrides
2448 # then sort hidden before non-hidden add-ons
2449 # then sort by override pkgpart (for consistency)
2450 sort { $a->pkgnum <=> $b->pkgnum or
2451 $a->sdate <=> $b->sdate or
2452 ($a->pkgpart_override ? 0 : -1) or
2453 ($b->pkgpart_override ? 0 : 1) or
2454 $b->hidden cmp $a->hidden or
2455 $a->pkgpart_override <=> $b->pkgpart_override
2457 # and of course exclude taxes and fees
2458 grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2464 my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2466 foreach my $cust_bill_pkg (@cust_bill_pkg) {
2467 # cache this, so we don't look it up again in every section
2468 my $part_fee = $cust_bill_pkg->get('part_fee')
2469 || $cust_bill_pkg->part_fee;
2470 $cust_bill_pkg->set('part_fee', $part_fee);
2472 #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2473 warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2476 if ( exists($options{section}) and exists($options{section}{category}) )
2478 my $categoryname = $options{section}{category};
2479 # then filter for items that have that section
2480 if ( $part_fee->categoryname ne $categoryname ) {
2481 warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2484 } # otherwise include them all in the main section
2485 # XXX what to do when sectioning by location?
2488 my %base_invnums; # invnum => invoice date
2489 foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2490 if ($_->base_invnum) {
2491 my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2492 my $base_date = $self->time2str_local('short', $base_bill->_date)
2494 $base_invnums{$_->base_invnum} = $base_date || '';
2497 foreach (sort keys(%base_invnums)) {
2498 next if $_ == $self->invnum;
2500 $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2503 { feepart => $cust_bill_pkg->feepart,
2504 amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2505 description => $part_fee->itemdesc_locale($self->cust_main->locale),
2506 ext_description => \@ext_desc
2517 warn "$me _items_pkg searching for all package line items\n"
2520 my @cust_bill_pkg = $self->_items_nontax;
2522 warn "$me _items_pkg filtering line items\n"
2524 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2526 if ($options{section} && $options{section}->{condensed}) {
2528 warn "$me _items_pkg condensing section\n"
2532 local $Storable::canonical = 1;
2533 foreach ( @items ) {
2535 delete $item->{ref};
2536 delete $item->{ext_description};
2537 my $key = freeze($item);
2538 $itemshash{$key} ||= 0;
2539 $itemshash{$key} ++; # += $item->{quantity};
2541 @items = sort { $a->{description} cmp $b->{description} }
2542 map { my $i = thaw($_);
2543 $i->{quantity} = $itemshash{$_};
2545 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2551 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2558 return 0 unless $a->itemdesc cmp $b->itemdesc;
2559 return -1 if $b->itemdesc eq 'Tax';
2560 return 1 if $a->itemdesc eq 'Tax';
2561 return -1 if $b->itemdesc eq 'Other surcharges';
2562 return 1 if $a->itemdesc eq 'Other surcharges';
2563 $a->itemdesc cmp $b->itemdesc;
2568 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
2569 $self->cust_bill_pkg;
2570 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2572 if ( $self->conf->exists('always_show_tax') ) {
2573 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2574 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2576 { 'description' => $itemdesc,
2583 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2585 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2586 list of hashrefs describing the line items they generate on the invoice.
2588 OPTIONS may include:
2590 format: the invoice format.
2592 escape_function: the function used to escape strings.
2594 DEPRECATED? (expensive, mostly unused?)
2595 format_function: the function used to format CDRs.
2597 section: a hashref containing 'category' and/or 'locationnum'; if this
2598 is present, only returns line items that belong to that category and/or
2599 location (whichever is defined).
2601 multisection: a flag indicating that this is a multisection invoice,
2602 which does something complicated.
2604 preref_callback: coderef run for each line item, code should return HTML to be
2605 displayed before that line item (quotations only)
2607 Returns a list of hashrefs, each of which may contain:
2609 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2610 ext_description, which is an arrayref of detail lines to show below
2615 sub _items_cust_bill_pkg {
2617 my $conf = $self->conf;
2618 my $cust_bill_pkgs = shift;
2621 my $format = $opt{format} || '';
2622 my $escape_function = $opt{escape_function} || sub { shift };
2623 my $format_function = $opt{format_function} || '';
2624 my $no_usage = $opt{no_usage} || '';
2625 my $unsquelched = $opt{unsquelched} || ''; #unused
2626 my ($section, $locationnum, $category);
2627 if ( $opt{section} ) {
2628 $category = $opt{section}->{category};
2629 $locationnum = $opt{section}->{locationnum};
2631 my $summary_page = $opt{summary_page} || ''; #unused
2632 my $multisection = defined($category) || defined($locationnum);
2633 my $discount_show_always = 0;
2635 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2637 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2638 # and location labels
2640 my @b = (); # accumulator for the line item hashes that we'll return
2641 my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
2642 # the 'current' line item hashes for setup, recur, usage, discount
2643 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2645 # if the current line item is waiting to go out, and the one we're about
2646 # to start is not bundled, then push out the current one and start a new
2648 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
2649 if ( $_ && !$cust_bill_pkg->hidden ) {
2650 $_->{amount} = sprintf( "%.2f", $_->{amount} );
2651 $_->{amount} =~ s/^\-0\.00$/0.00/;
2652 if (exists($_->{unit_amount})) {
2653 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
2656 if $_->{amount} != 0
2657 || $discount_show_always
2658 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2659 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2665 if ( $locationnum ) {
2666 # this is a location section; skip packages that aren't at this
2668 next if $cust_bill_pkg->pkgnum == 0; # skips fees...
2669 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2673 # Consider display records for this item to determine if it belongs
2674 # in this section. Note that if there are no display records, there
2675 # will be a default pseudo-record that includes all charge types
2676 # and has no section name.
2677 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2678 ? $cust_bill_pkg->cust_bill_pkg_display
2679 : ( $cust_bill_pkg );
2681 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2682 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2685 if ( defined($category) ) {
2686 # then this is a package category section; process all display records
2687 # that belong to this section.
2688 @cust_bill_pkg_display = grep { $_->section eq $category }
2689 @cust_bill_pkg_display;
2691 # otherwise, process all display records that aren't usage summaries
2692 # (I don't think there should be usage summaries if you aren't using
2693 # category sections, but this is the historical behavior)
2694 @cust_bill_pkg_display = grep { !$_->summary }
2695 @cust_bill_pkg_display;
2698 my $classname = ''; # package class name, will fill in later
2700 foreach my $display (@cust_bill_pkg_display) {
2702 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2703 $display->billpkgdisplaynum. "\n"
2706 my $type = $display->type;
2708 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2709 $desc = substr($desc, 0, $maxlength). '...'
2710 if $format eq 'latex' && length($desc) > $maxlength;
2712 my %details_opt = ( 'format' => $format,
2713 'escape_function' => $escape_function,
2714 'format_function' => $format_function,
2715 'no_usage' => $opt{'no_usage'},
2718 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2720 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2722 # quotation_pkgs are never fees, so don't worry about the case where
2723 # part_pkg is undefined
2725 # and I guess they're never bundled either?
2726 if ( $cust_bill_pkg->setup != 0 ) {
2727 my $description = $desc;
2728 $description .= ' Setup'
2729 if $cust_bill_pkg->recur != 0
2730 || $discount_show_always
2731 || $cust_bill_pkg->recur_show_zero;
2733 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2734 'description' => $description,
2735 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2736 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2737 'quantity' => $cust_bill_pkg->quantity,
2738 'preref_html' => ( $opt{preref_callback}
2739 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2744 if ( $cust_bill_pkg->recur != 0 ) {
2746 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2747 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2748 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2749 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2750 'quantity' => $cust_bill_pkg->quantity,
2751 'preref_html' => ( $opt{preref_callback}
2752 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2758 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2759 # a "normal" package line item (not a quotation, not a fee, not a tax)
2761 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2764 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2765 my $part_pkg = $cust_pkg->part_pkg;
2767 # which pkgpart to show for display purposes?
2768 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2770 # start/end dates for invoice formats that do nonstandard
2772 my %item_dates = ();
2773 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2774 unless $part_pkg->option('disable_line_item_date_ranges',1);
2776 # not normally used, but pass this to the template anyway
2777 $classname = $part_pkg->classname;
2779 if ( (!$type || $type eq 'S')
2780 && ( $cust_bill_pkg->setup != 0
2781 || $cust_bill_pkg->setup_show_zero
2786 warn "$me _items_cust_bill_pkg adding setup\n"
2789 my $description = $desc;
2790 $description .= ' Setup'
2791 if $cust_bill_pkg->recur != 0
2792 || $discount_show_always
2793 || $cust_bill_pkg->recur_show_zero;
2795 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2797 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2798 # triggered by the recurring charge freq
2800 && $cust_bill_pkg->recur == 0
2801 && ! $cust_bill_pkg->recur_show_zero;
2806 # always pass the svc_label through to the template, even if
2807 # not displaying it as an ext_description
2808 my @svc_labels = map &{$escape_function}($_),
2809 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2811 $svc_label = $svc_labels[0];
2813 unless ( $cust_pkg->part_pkg->hide_svc_detail
2814 || $cust_bill_pkg->hidden )
2817 push @d, @svc_labels
2818 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2819 my $lnum = $cust_main ? $cust_main->ship_locationnum
2820 : $self->prospect_main->locationnum;
2821 # show the location label if it's not the customer's default
2822 # location, and we're not grouping items by location already
2823 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2824 my $loc = $cust_pkg->location_label;
2825 $loc = substr($loc, 0, $maxlength). '...'
2826 if $format eq 'latex' && length($loc) > $maxlength;
2827 push @d, &{$escape_function}($loc);
2830 } #unless hiding service details
2832 push @d, $cust_bill_pkg->details(%details_opt)
2833 if $cust_bill_pkg->recur == 0;
2835 if ( $cust_bill_pkg->hidden ) {
2836 $s->{amount} += $cust_bill_pkg->setup;
2837 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2838 push @{ $s->{ext_description} }, @d;
2842 description => $description,
2843 pkgpart => $pkgpart,
2844 pkgnum => $cust_bill_pkg->pkgnum,
2845 amount => $cust_bill_pkg->setup,
2846 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2847 unit_amount => $cust_bill_pkg->unitsetup,
2848 quantity => $cust_bill_pkg->quantity,
2849 ext_description => \@d,
2850 svc_label => ($svc_label || ''),
2851 locationnum => $cust_pkg->locationnum, # sure, why not?
2857 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2859 $cust_bill_pkg->recur != 0
2860 || $cust_bill_pkg->setup == 0
2861 || $discount_show_always
2862 || $cust_bill_pkg->recur_show_zero
2867 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2870 my $is_summary = $display->summary;
2871 my $description = $desc;
2872 if ( $type eq 'U' and defined($r) ) {
2873 # don't just show the same description as the recur line
2874 $description = $self->mt('Usage charges');
2877 my $part_pkg = $cust_pkg->part_pkg;
2879 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2883 my @seconds = (); # for display of usage info
2886 #at least until cust_bill_pkg has "past" ranges in addition to
2887 #the "future" sdate/edate ones... see #3032
2888 my @dates = ( $self->_date );
2889 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2890 push @dates, $prev->sdate if $prev;
2891 push @dates, undef if !$prev;
2893 my @svc_labels = map &{$escape_function}($_),
2894 $cust_pkg->h_labels_short(@dates, 'I');
2895 $svc_label = $svc_labels[0];
2897 # show service labels, unless...
2898 # the package is set not to display them
2899 unless ( $part_pkg->hide_svc_detail
2900 # or this is a tax-like line item
2901 || $cust_bill_pkg->itemdesc
2902 # or this is a hidden (bundled) line item
2903 || $cust_bill_pkg->hidden
2904 # or this is a usage summary line
2905 || $is_summary && $type && $type eq 'U'
2906 # or this is a usage line and there's a recurring line
2907 # for the package in the same section (which will
2908 # have service labels already)
2909 || ($type eq 'U' and defined($r))
2913 warn "$me _items_cust_bill_pkg adding service details\n"
2916 push @d, @svc_labels
2917 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2918 warn "$me _items_cust_bill_pkg done adding service details\n"
2921 my $lnum = $cust_main ? $cust_main->ship_locationnum
2922 : $self->prospect_main->locationnum;
2923 # show the location label if it's not the customer's default
2924 # location, and we're not grouping items by location already
2925 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2926 my $loc = $cust_pkg->location_label;
2927 $loc = substr($loc, 0, $maxlength). '...'
2928 if $format eq 'latex' && length($loc) > $maxlength;
2929 push @d, &{$escape_function}($loc);
2932 # Display of seconds_since_sqlradacct:
2933 # On the invoice, when processing @detail_items, look for a field
2934 # named 'seconds'. This will contain total seconds for each
2935 # service, in the same order as @ext_description. For services
2936 # that don't support this it will show undef.
2937 if ( $conf->exists('svc_acct-usage_seconds')
2938 and ! $cust_bill_pkg->pkgpart_override ) {
2939 foreach my $cust_svc (
2940 $cust_pkg->h_cust_svc(@dates, 'I')
2943 # eval because not having any part_export_usage exports
2944 # is a fatal error, last_bill/_date because that's how
2945 # sqlradius_hour billing does it
2947 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2949 push @seconds, $sec;
2951 } #if svc_acct-usage_seconds
2953 } # if we are showing service labels
2955 unless ( $is_summary ) {
2956 warn "$me _items_cust_bill_pkg adding details\n"
2959 #instead of omitting details entirely in this case (unwanted side
2960 # effects), just omit CDRs
2961 $details_opt{'no_usage'} = 1
2962 if $type && $type eq 'R';
2964 push @d, $cust_bill_pkg->details(%details_opt);
2967 warn "$me _items_cust_bill_pkg calculating amount\n"
2972 $amount = $cust_bill_pkg->recur;
2973 } elsif ($type eq 'R') {
2974 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2975 } elsif ($type eq 'U') {
2976 $amount = $cust_bill_pkg->usage;
2979 if ( !$type || $type eq 'R' ) {
2981 warn "$me _items_cust_bill_pkg adding recur\n"
2985 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2988 if ( $cust_bill_pkg->hidden ) {
2989 $r->{amount} += $amount;
2990 $r->{unit_amount} += $unit_amount;
2991 push @{ $r->{ext_description} }, @d;
2994 description => $description,
2995 pkgpart => $pkgpart,
2996 pkgnum => $cust_bill_pkg->pkgnum,
2998 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2999 unit_amount => $unit_amount,
3000 quantity => $cust_bill_pkg->quantity,
3002 ext_description => \@d,
3003 svc_label => ($svc_label || ''),
3004 locationnum => $cust_pkg->locationnum,
3006 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3009 } else { # $type eq 'U'
3011 warn "$me _items_cust_bill_pkg adding usage\n"
3014 if ( $cust_bill_pkg->hidden and defined($u) ) {
3015 # if this is a hidden package and there's already a usage
3016 # line for the bundle, add this package's total amount and
3017 # usage details to it
3018 $u->{amount} += $amount;
3019 push @{ $u->{ext_description} }, @d;
3020 } elsif ( $amount ) {
3021 # create a new usage line
3023 description => $description,
3024 pkgpart => $pkgpart,
3025 pkgnum => $cust_bill_pkg->pkgnum,
3028 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3030 ext_description => \@d,
3031 locationnum => $cust_pkg->locationnum,
3033 } # else this has no usage, so don't create a usage section
3036 } # recurring or usage with recurring charge
3038 # decide whether to show active discounts here
3040 # case 1: we are showing a single line for the package
3042 # case 2: we are showing a setup line for a package that has
3043 # no base recurring fee
3044 or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3045 # case 3: we are showing a recur line for a package that has
3046 # a base recurring fee
3047 or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3050 # the line item hashref for the line that will show the original
3052 # (use the recur or single line for the package, unless we're
3053 # showing a setup line for a package with no recurring fee)
3054 my $active_line = $r;
3055 if ( $type eq 'S' ) {
3059 my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
3060 # special case: if there are old "discount details" on this line
3061 # item, don't show discount line items
3062 if ( FS::cust_bill_pkg_detail->count(
3063 "detail LIKE 'Includes discount%' AND billpkgnum = " .
3064 $cust_bill_pkg->billpkgnum
3069 warn "$me _items_cust_bill_pkg including discounts for ".
3070 $cust_bill_pkg->billpkgnum."\n"
3072 my $discount_amount = sum( map {$_->amount} @discounts );
3073 # if multiple discounts apply to the same package, how to display
3074 # them? ext_description lines, apparently
3076 # # discount amounts are negative
3077 if ( $d and $cust_bill_pkg->hidden ) {
3078 $d->{amount} -= $discount_amount;
3083 description => $self->mt('Discount'),
3084 amount => -1 * $discount_amount,
3085 ext_description => \@ext,
3087 foreach my $cust_bill_pkg_discount (@discounts) {
3088 my $def = $cust_bill_pkg_discount->cust_pkg_discount->discount;
3089 push @ext, &{$escape_function}( $def->description );
3093 # update the active line (before the discount) to show the
3094 # original price (whether this is a hidden line or not)
3095 $active_line->{amount} += $discount_amount;
3097 } # if there are any discounts
3098 } # if this is an appropriate place to show discounts
3100 } else { # taxes and fees
3102 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3105 # items of this kind should normally not have sdate/edate.
3107 'description' => $desc,
3108 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
3109 + $cust_bill_pkg->recur)
3112 } # if quotation / package line item / other line item
3114 } # foreach $display
3116 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
3117 && $conf->exists('discount-show-always'));
3121 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3123 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3124 if exists($_->{amount});
3125 $_->{amount} =~ s/^\-0\.00$/0.00/;
3126 if (exists($_->{unit_amount})) {
3127 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3131 if $_->{amount} != 0
3132 || $discount_show_always
3133 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3134 || ( $_->{_is_setup} && $_->{setup_show_zero} )
3138 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3145 =item _items_discounts_avail
3147 Returns an array of line item hashrefs representing available term discounts
3148 for this invoice. This makes the same assumptions that apply to term
3149 discounts in general: that the package is billed monthly, at a flat rate,
3150 with no usage charges. A prorated first month will be handled, as will
3151 a setup fee if the discount is allowed to apply to setup fees.
3155 sub _items_discounts_avail {
3158 #maybe move this method from cust_bill when quotations support discount_plans
3159 return () unless $self->can('discount_plans');
3160 my %plans = $self->discount_plans;
3162 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3163 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3167 my $plan = $plans{$months};
3169 my $term_total = sprintf('%.2f', $plan->discounted_total);
3170 my $percent = sprintf('%.0f',
3171 100 * (1 - $term_total / $plan->base_total) );
3172 my $permonth = sprintf('%.2f', $term_total / $months);
3173 my $detail = $self->mt('discount on item'). ' '.
3174 join(', ', map { "#$_" } $plan->pkgnums)
3177 # discounts for non-integer months don't work anyway
3178 $months = sprintf("%d", $months);
3181 description => $self->mt('Save [_1]% by paying for [_2] months',
3183 amount => $self->mt('[_1] ([_2] per month)',
3184 $term_total, $money_char.$permonth),
3185 ext_description => ($detail || ''),
3188 sort { $b <=> $a } keys %plans;