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;
1859 #use configured default
1860 $conf->config('invoice_default_terms') || '';
1866 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1867 $duedate = $self->_date() + ( $1 * 86400 );
1874 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
1877 sub balance_due_msg {
1879 my $msg = $self->mt('Balance Due');
1880 return $msg unless $self->terms;
1881 if ( $self->due_date ) {
1882 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1883 $self->due_date2str('short');
1884 } elsif ( $self->terms ) {
1885 $msg .= ' - '. $self->terms;
1890 sub balance_due_date {
1892 my $conf = $self->conf;
1894 if ( $conf->exists('invoice_default_terms')
1895 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1896 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
1901 sub credit_balance_msg {
1903 $self->mt('Credit Balance Remaining')
1908 Returns a string with the date, for example: "3/20/2008", localized for the
1909 customer. Use _date_pretty_unlocalized for non-end-customer display use.
1915 $self->time2str_local('short', $self->_date);
1918 =item _date_pretty_unlocalized
1920 Returns a string with the date, for example: "3/20/2008", in the format
1921 configured for the back-office. Use _date_pretty for end-customer display use.
1925 sub _date_pretty_unlocalized {
1927 time2str($date_format, $self->_date);
1930 =item _items_sections OPTIONS
1932 Generate section information for all items appearing on this invoice.
1933 This will only be called for multi-section invoices.
1935 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1936 related display records (L<FS::cust_bill_pkg_display>) and organize
1937 them into two groups ("early" and "late" according to whether they come
1938 before or after the total), then into sections. A subtotal is calculated
1941 Section descriptions are returned in sort weight order. Each consists
1942 of a hash containing:
1944 description: the package category name, escaped
1945 subtotal: the total charges in that section
1946 tax_section: a flag indicating that the section contains only tax charges
1947 summarized: same as tax_section, for some reason
1948 sort_weight: the package category's sort weight
1950 If 'condense' is set on the display record, it also contains everything
1951 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1952 coderefs to generate parts of the invoice. This is not advised.
1954 The method returns two arrayrefs, one of "early" sections and one of "late"
1957 OPTIONS may include:
1959 by_location: a flag to divide the invoice into sections by location.
1960 Each section hash will have a 'location' element containing a hashref of
1961 the location fields (see L<FS::cust_location>). The section description
1962 will be the location label, but the template can use any of the location
1963 fields to create a suitable label.
1965 by_category: a flag to divide the invoice into sections using display
1966 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1967 behavior. Each section hash will have a 'category' element containing
1968 the section name from the display record (which probably equals the
1969 category name of the package, but may not in some cases).
1971 summary: a flag indicating that this is a summary-format invoice.
1972 Turning this on has the following effects:
1973 - Ignores display items with the 'summary' flag.
1974 - Places all sections in the "early" group even if they have post_total.
1975 - Creates sections for all non-disabled package categories, even if they
1976 have no charges on this invoice, as well as a section with no name.
1978 escape: an escape function to use for section titles.
1980 extra_sections: an arrayref of additional sections to return after the
1981 sorted list. If there are any of these, section subtotals exclude
1984 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1985 passed through to C<_condense_section()>.
1989 use vars qw(%pkg_category_cache);
1990 sub _items_sections {
1994 my $escape = $opt{escape};
1995 my @extra_sections = @{ $opt{extra_sections} || [] };
1997 # $subtotal{$locationnum}{$categoryname} = amount.
1998 # if we're not using by_location, $locationnum is undef.
1999 # if we're not using by_category, you guessed it, $categoryname is undef.
2000 # if we're not using either one, we shouldn't be here in the first place...
2002 my %late_subtotal = ();
2005 # About tax items + multisection invoices:
2006 # If either invoice_*summary option is enabled, AND there is a
2007 # package category with the name of the tax, then there will be
2008 # a display record assigning the tax item to that category.
2010 # However, the taxes are always placed in the "Taxes, Surcharges,
2011 # and Fees" section regardless of that. The only effect of the
2012 # display record is to create a subtotal for the summary page.
2015 my $pkg_hash = $self->cust_pkg_hash;
2017 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2020 my $usage = $cust_bill_pkg->usage;
2023 if ( $opt{by_location} ) {
2024 if ( $cust_bill_pkg->pkgnum ) {
2025 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
2030 $locationnum = undef;
2033 # as in _items_cust_pkg, if a line item has no display records,
2034 # cust_bill_pkg_display() returns a default record for it
2036 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2037 next if ( $display->summary && $opt{summary} );
2039 my $section = $display->section;
2040 my $type = $display->type;
2041 # Set $section = undef if we're sectioning by location and this
2042 # line item _has_ a location (i.e. isn't a fee).
2043 $section = undef if $locationnum;
2045 # set this flag if the section is not tax-only
2046 $not_tax{$locationnum}{$section} = 1
2047 if $cust_bill_pkg->pkgnum or $cust_bill_pkg->feepart;
2049 # there's actually a very important piece of logic buried in here:
2050 # incrementing $late_subtotal{$section} CREATES
2051 # $late_subtotal{$section}. keys(%late_subtotal) is later used
2052 # to define the list of late sections, and likewise keys(%subtotal).
2053 # When _items_cust_bill_pkg is called to generate line items for
2054 # real, it will be called with 'section' => $section for each
2056 if ( $display->post_total && !$opt{summary} ) {
2057 if (! $type || $type eq 'S') {
2058 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2059 if $cust_bill_pkg->setup != 0
2060 || $cust_bill_pkg->setup_show_zero;
2064 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
2065 if $cust_bill_pkg->recur != 0
2066 || $cust_bill_pkg->recur_show_zero;
2069 if ($type && $type eq 'R') {
2070 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2071 if $cust_bill_pkg->recur != 0
2072 || $cust_bill_pkg->recur_show_zero;
2075 if ($type && $type eq 'U') {
2076 $late_subtotal{$locationnum}{$section} += $usage
2077 unless scalar(@extra_sections);
2080 } else { # it's a pre-total (normal) section
2082 # skip tax items unless they're explicitly included in a section
2083 next if $cust_bill_pkg->pkgnum == 0 and
2084 ! $cust_bill_pkg->feepart and
2087 if ( $type eq 'S' ) {
2088 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2089 if $cust_bill_pkg->setup != 0
2090 || $cust_bill_pkg->setup_show_zero;
2091 } elsif ( $type eq 'R' ) {
2092 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
2093 if $cust_bill_pkg->recur != 0
2094 || $cust_bill_pkg->recur_show_zero;
2095 } elsif ( $type eq 'U' ) {
2096 $subtotal{$locationnum}{$section} += $usage
2097 unless scalar(@extra_sections);
2098 } elsif ( !$type ) {
2099 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
2100 + $cust_bill_pkg->recur;
2109 %pkg_category_cache = ();
2111 # summary invoices need subtotals for all non-disabled package categories,
2112 # even if they're zero
2113 # but currently assume that there are no location sections, or at least
2114 # that the summary page doesn't care about them
2115 if ( $opt{summary} ) {
2116 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
2117 $subtotal{''}{$category->categoryname} ||= 0;
2119 $subtotal{''}{''} ||= 0;
2123 foreach my $post_total (0,1) {
2125 my $s = $post_total ? \%late_subtotal : \%subtotal;
2126 foreach my $locationnum (keys %$s) {
2127 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
2129 'subtotal' => $s->{$locationnum}{$sectionname},
2130 'post_total' => $post_total,
2133 if ( $locationnum ) {
2134 $section->{'locationnum'} = $locationnum;
2135 my $location = FS::cust_location->by_key($locationnum);
2136 $section->{'description'} = &{ $escape }($location->location_label);
2137 # Better ideas? This will roughly group them by proximity,
2138 # which alpha sorting on any of the address fields won't.
2139 # Sorting by locationnum is meaningless.
2140 # We have to sort on _something_ or the order may change
2141 # randomly from one invoice to the next, which will confuse
2143 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
2145 $section->{'location'} = {
2146 label_prefix => &{ $escape }($location->label_prefix),
2147 map { $_ => &{ $escape }($location->get($_)) }
2151 $section->{'category'} = $sectionname;
2152 $section->{'description'} = &{ $escape }($sectionname);
2153 if ( _pkg_category($sectionname) ) {
2154 $section->{'sort_weight'} = _pkg_category($sectionname)->weight;
2155 if ( _pkg_category($sectionname)->condense ) {
2156 $section = { %$section, $self->_condense_section($opt{format}) };
2160 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
2161 # then it's a tax-only section
2162 $section->{'summarized'} = 'Y';
2163 $section->{'tax_section'} = 'Y';
2165 push @these, $section;
2166 } # foreach $sectionname
2167 } #foreach $locationnum
2168 push @these, @extra_sections if $post_total == 0;
2169 # need an alpha sort for location sections, because postal codes can
2171 $sections[ $post_total ] = [ sort {
2172 $opt{'by_location'} ?
2173 ($a->{sort_weight} cmp $b->{sort_weight}) :
2174 ($a->{sort_weight} <=> $b->{sort_weight})
2176 } #foreach $post_total
2178 return @sections; # early, late
2181 #helper subs for above
2185 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
2189 my $categoryname = shift;
2190 $pkg_category_cache{$categoryname} ||=
2191 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
2194 my %condensed_format = (
2195 'label' => [ qw( Description Qty Amount ) ],
2197 sub { shift->{description} },
2198 sub { shift->{quantity} },
2199 sub { my($href, %opt) = @_;
2200 ($opt{dollar} || ''). $href->{amount};
2203 'align' => [ qw( l r r ) ],
2204 'span' => [ qw( 5 1 1 ) ], # unitprices?
2205 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
2208 sub _condense_section {
2209 my ( $self, $format ) = ( shift, shift );
2211 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
2212 qw( description_generator
2215 total_line_generator
2220 sub _condensed_generator_defaults {
2221 my ( $self, $format ) = ( shift, shift );
2222 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2231 sub _condensed_header_generator {
2232 my ( $self, $format ) = ( shift, shift );
2234 my ( $f, $prefix, $suffix, $separator, $column ) =
2235 _condensed_generator_defaults($format);
2237 if ($format eq 'latex') {
2238 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2239 $suffix = "\\\\\n\\hline";
2242 sub { my ($d,$a,$s,$w) = @_;
2243 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2245 } elsif ( $format eq 'html' ) {
2246 $prefix = '<th></th>';
2250 sub { my ($d,$a,$s,$w) = @_;
2251 return qq!<th align="$html_align{$a}">$d</th>!;
2259 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2261 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2264 $prefix. join($separator, @result). $suffix;
2269 sub _condensed_description_generator {
2270 my ( $self, $format ) = ( shift, shift );
2272 my ( $f, $prefix, $suffix, $separator, $column ) =
2273 _condensed_generator_defaults($format);
2275 my $money_char = '$';
2276 if ($format eq 'latex') {
2277 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2279 $separator = " & \n";
2281 sub { my ($d,$a,$s,$w) = @_;
2282 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2284 $money_char = '\\dollar';
2285 }elsif ( $format eq 'html' ) {
2286 $prefix = '"><td align="center"></td>';
2290 sub { my ($d,$a,$s,$w) = @_;
2291 return qq!<td align="$html_align{$a}">$d</td>!;
2293 #$money_char = $conf->config('money_char') || '$';
2294 $money_char = ''; # this is madness
2302 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2304 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2306 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2307 map { $f->{$_}->[$i] } qw(align span width)
2311 $prefix. join( $separator, @result ). $suffix;
2316 sub _condensed_total_generator {
2317 my ( $self, $format ) = ( shift, shift );
2319 my ( $f, $prefix, $suffix, $separator, $column ) =
2320 _condensed_generator_defaults($format);
2323 if ($format eq 'latex') {
2326 $separator = " & \n";
2328 sub { my ($d,$a,$s,$w) = @_;
2329 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2331 }elsif ( $format eq 'html' ) {
2335 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2337 sub { my ($d,$a,$s,$w) = @_;
2338 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2347 # my $r = &{$f->{fields}->[$i]}(@args);
2348 # $r .= ' Total' unless $i;
2350 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2352 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2353 map { $f->{$_}->[$i] } qw(align span width)
2357 $prefix. join( $separator, @result ). $suffix;
2362 =item total_line_generator FORMAT
2364 Returns a coderef used for generation of invoice total line items for this
2365 usage_class. FORMAT is either html or latex
2369 # should not be used: will have issues with hash element names (description vs
2370 # total_item and amount vs total_amount -- another array of functions?
2372 sub _condensed_total_line_generator {
2373 my ( $self, $format ) = ( shift, shift );
2375 my ( $f, $prefix, $suffix, $separator, $column ) =
2376 _condensed_generator_defaults($format);
2379 if ($format eq 'latex') {
2382 $separator = " & \n";
2384 sub { my ($d,$a,$s,$w) = @_;
2385 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2387 }elsif ( $format eq 'html' ) {
2391 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2393 sub { my ($d,$a,$s,$w) = @_;
2394 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2403 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2405 &{$column}( &{$f->{fields}->[$i]}(@args),
2406 map { $f->{$_}->[$i] } qw(align span width)
2410 $prefix. join( $separator, @result ). $suffix;
2415 =item _items_pkg [ OPTIONS ]
2417 Return line item hashes for each package item on this invoice. Nearly
2420 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2422 The only OPTIONS accepted is 'section', which may point to a hashref
2423 with a key named 'condensed', which may have a true value. If it
2424 does, this method tries to merge identical items into items with
2425 'quantity' equal to the number of items (not the sum of their
2426 separate quantities, for some reason).
2432 # The order of these is important. Bundled line items will be merged into
2433 # the most recent non-hidden item, so it needs to be the one with:
2435 # - the same start date
2436 # - no pkgpart_override
2438 # So: sort by pkgnum,
2440 # then sort the base line item before any overrides
2441 # then sort hidden before non-hidden add-ons
2442 # then sort by override pkgpart (for consistency)
2443 sort { $a->pkgnum <=> $b->pkgnum or
2444 $a->sdate <=> $b->sdate or
2445 ($a->pkgpart_override ? 0 : -1) or
2446 ($b->pkgpart_override ? 0 : 1) or
2447 $b->hidden cmp $a->hidden or
2448 $a->pkgpart_override <=> $b->pkgpart_override
2450 # and of course exclude taxes and fees
2451 grep { $_->pkgnum > 0 } $self->cust_bill_pkg;
2457 my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
2459 foreach my $cust_bill_pkg (@cust_bill_pkg) {
2460 # cache this, so we don't look it up again in every section
2461 my $part_fee = $cust_bill_pkg->get('part_fee')
2462 || $cust_bill_pkg->part_fee;
2463 $cust_bill_pkg->set('part_fee', $part_fee);
2465 #die "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n"; # might make more sense
2466 warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
2469 if ( exists($options{section}) and exists($options{section}{category}) )
2471 my $categoryname = $options{section}{category};
2472 # then filter for items that have that section
2473 if ( $part_fee->categoryname ne $categoryname ) {
2474 warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
2477 } # otherwise include them all in the main section
2478 # XXX what to do when sectioning by location?
2481 my %base_invnums; # invnum => invoice date
2482 foreach ($cust_bill_pkg->cust_bill_pkg_fee) {
2483 if ($_->base_invnum) {
2484 my $base_bill = FS::cust_bill->by_key($_->base_invnum);
2485 my $base_date = $self->time2str_local('short', $base_bill->_date)
2487 $base_invnums{$_->base_invnum} = $base_date || '';
2490 foreach (sort keys(%base_invnums)) {
2491 next if $_ == $self->invnum;
2493 $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
2496 { feepart => $cust_bill_pkg->feepart,
2497 amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2498 description => $part_fee->itemdesc_locale($self->cust_main->locale),
2499 ext_description => \@ext_desc
2510 warn "$me _items_pkg searching for all package line items\n"
2513 my @cust_bill_pkg = $self->_items_nontax;
2515 warn "$me _items_pkg filtering line items\n"
2517 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2519 if ($options{section} && $options{section}->{condensed}) {
2521 warn "$me _items_pkg condensing section\n"
2525 local $Storable::canonical = 1;
2526 foreach ( @items ) {
2528 delete $item->{ref};
2529 delete $item->{ext_description};
2530 my $key = freeze($item);
2531 $itemshash{$key} ||= 0;
2532 $itemshash{$key} ++; # += $item->{quantity};
2534 @items = sort { $a->{description} cmp $b->{description} }
2535 map { my $i = thaw($_);
2536 $i->{quantity} = $itemshash{$_};
2538 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2544 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2551 return 0 unless $a->itemdesc cmp $b->itemdesc;
2552 return -1 if $b->itemdesc eq 'Tax';
2553 return 1 if $a->itemdesc eq 'Tax';
2554 return -1 if $b->itemdesc eq 'Other surcharges';
2555 return 1 if $a->itemdesc eq 'Other surcharges';
2556 $a->itemdesc cmp $b->itemdesc;
2561 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum and ! $_->feepart }
2562 $self->cust_bill_pkg;
2563 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2565 if ( $self->conf->exists('always_show_tax') ) {
2566 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2567 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2569 { 'description' => $itemdesc,
2576 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2578 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2579 list of hashrefs describing the line items they generate on the invoice.
2581 OPTIONS may include:
2583 format: the invoice format.
2585 escape_function: the function used to escape strings.
2587 DEPRECATED? (expensive, mostly unused?)
2588 format_function: the function used to format CDRs.
2590 section: a hashref containing 'category' and/or 'locationnum'; if this
2591 is present, only returns line items that belong to that category and/or
2592 location (whichever is defined).
2594 multisection: a flag indicating that this is a multisection invoice,
2595 which does something complicated.
2597 preref_callback: coderef run for each line item, code should return HTML to be
2598 displayed before that line item (quotations only)
2600 Returns a list of hashrefs, each of which may contain:
2602 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2603 ext_description, which is an arrayref of detail lines to show below
2608 sub _items_cust_bill_pkg {
2610 my $conf = $self->conf;
2611 my $cust_bill_pkgs = shift;
2614 my $format = $opt{format} || '';
2615 my $escape_function = $opt{escape_function} || sub { shift };
2616 my $format_function = $opt{format_function} || '';
2617 my $no_usage = $opt{no_usage} || '';
2618 my $unsquelched = $opt{unsquelched} || ''; #unused
2619 my ($section, $locationnum, $category);
2620 if ( $opt{section} ) {
2621 $category = $opt{section}->{category};
2622 $locationnum = $opt{section}->{locationnum};
2624 my $summary_page = $opt{summary_page} || ''; #unused
2625 my $multisection = defined($category) || defined($locationnum);
2626 my $discount_show_always = 0;
2628 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2630 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2631 # and location labels
2633 my @b = (); # accumulator for the line item hashes that we'll return
2634 my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
2635 # the 'current' line item hashes for setup, recur, usage, discount
2636 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2638 # if the current line item is waiting to go out, and the one we're about
2639 # to start is not bundled, then push out the current one and start a new
2641 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
2642 if ( $_ && !$cust_bill_pkg->hidden ) {
2643 $_->{amount} = sprintf( "%.2f", $_->{amount} );
2644 $_->{amount} =~ s/^\-0\.00$/0.00/;
2645 if (exists($_->{unit_amount})) {
2646 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
2649 if $_->{amount} != 0
2650 || $discount_show_always
2651 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2652 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2658 if ( $locationnum ) {
2659 # this is a location section; skip packages that aren't at this
2661 next if $cust_bill_pkg->pkgnum == 0; # skips fees...
2662 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2666 # Consider display records for this item to determine if it belongs
2667 # in this section. Note that if there are no display records, there
2668 # will be a default pseudo-record that includes all charge types
2669 # and has no section name.
2670 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2671 ? $cust_bill_pkg->cust_bill_pkg_display
2672 : ( $cust_bill_pkg );
2674 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2675 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2678 if ( defined($category) ) {
2679 # then this is a package category section; process all display records
2680 # that belong to this section.
2681 @cust_bill_pkg_display = grep { $_->section eq $category }
2682 @cust_bill_pkg_display;
2684 # otherwise, process all display records that aren't usage summaries
2685 # (I don't think there should be usage summaries if you aren't using
2686 # category sections, but this is the historical behavior)
2687 @cust_bill_pkg_display = grep { !$_->summary }
2688 @cust_bill_pkg_display;
2691 my $classname = ''; # package class name, will fill in later
2693 foreach my $display (@cust_bill_pkg_display) {
2695 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2696 $display->billpkgdisplaynum. "\n"
2699 my $type = $display->type;
2701 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2702 $desc = substr($desc, 0, $maxlength). '...'
2703 if $format eq 'latex' && length($desc) > $maxlength;
2705 my %details_opt = ( 'format' => $format,
2706 'escape_function' => $escape_function,
2707 'format_function' => $format_function,
2708 'no_usage' => $opt{'no_usage'},
2711 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2713 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2715 # quotation_pkgs are never fees, so don't worry about the case where
2716 # part_pkg is undefined
2718 # and I guess they're never bundled either?
2719 if ( $cust_bill_pkg->setup != 0 ) {
2720 my $description = $desc;
2721 $description .= ' Setup'
2722 if $cust_bill_pkg->recur != 0
2723 || $discount_show_always
2724 || $cust_bill_pkg->recur_show_zero;
2726 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2727 'description' => $description,
2728 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2729 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2730 'quantity' => $cust_bill_pkg->quantity,
2731 'preref_html' => ( $opt{preref_callback}
2732 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2737 if ( $cust_bill_pkg->recur != 0 ) {
2739 'pkgnum' => $cust_bill_pkg->pkgpart, #so it displays in Ref
2740 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2741 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2742 'unit_amount' => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2743 'quantity' => $cust_bill_pkg->quantity,
2744 'preref_html' => ( $opt{preref_callback}
2745 ? &{ $opt{preref_callback} }( $cust_bill_pkg )
2751 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2752 # a "normal" package line item (not a quotation, not a fee, not a tax)
2754 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2757 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2758 my $part_pkg = $cust_pkg->part_pkg;
2760 # which pkgpart to show for display purposes?
2761 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2763 # start/end dates for invoice formats that do nonstandard
2765 my %item_dates = ();
2766 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2767 unless $part_pkg->option('disable_line_item_date_ranges',1);
2769 # not normally used, but pass this to the template anyway
2770 $classname = $part_pkg->classname;
2772 if ( (!$type || $type eq 'S')
2773 && ( $cust_bill_pkg->setup != 0
2774 || $cust_bill_pkg->setup_show_zero
2779 warn "$me _items_cust_bill_pkg adding setup\n"
2782 my $description = $desc;
2783 $description .= ' Setup'
2784 if $cust_bill_pkg->recur != 0
2785 || $discount_show_always
2786 || $cust_bill_pkg->recur_show_zero;
2788 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2790 if $part_pkg->is_prepaid #for prepaid, "display the validity period
2791 # triggered by the recurring charge freq
2793 && $cust_bill_pkg->recur == 0
2794 && ! $cust_bill_pkg->recur_show_zero;
2799 # always pass the svc_label through to the template, even if
2800 # not displaying it as an ext_description
2801 my @svc_labels = map &{$escape_function}($_),
2802 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2804 $svc_label = $svc_labels[0];
2806 unless ( $cust_pkg->part_pkg->hide_svc_detail
2807 || $cust_bill_pkg->hidden )
2810 push @d, @svc_labels
2811 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2812 my $lnum = $cust_main ? $cust_main->ship_locationnum
2813 : $self->prospect_main->locationnum;
2814 # show the location label if it's not the customer's default
2815 # location, and we're not grouping items by location already
2816 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2817 my $loc = $cust_pkg->location_label;
2818 $loc = substr($loc, 0, $maxlength). '...'
2819 if $format eq 'latex' && length($loc) > $maxlength;
2820 push @d, &{$escape_function}($loc);
2823 } #unless hiding service details
2825 push @d, $cust_bill_pkg->details(%details_opt)
2826 if $cust_bill_pkg->recur == 0;
2828 if ( $cust_bill_pkg->hidden ) {
2829 $s->{amount} += $cust_bill_pkg->setup;
2830 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2831 push @{ $s->{ext_description} }, @d;
2835 description => $description,
2836 pkgpart => $pkgpart,
2837 pkgnum => $cust_bill_pkg->pkgnum,
2838 amount => $cust_bill_pkg->setup,
2839 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2840 unit_amount => $cust_bill_pkg->unitsetup,
2841 quantity => $cust_bill_pkg->quantity,
2842 ext_description => \@d,
2843 svc_label => ($svc_label || ''),
2844 locationnum => $cust_pkg->locationnum, # sure, why not?
2850 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2852 $cust_bill_pkg->recur != 0
2853 || $cust_bill_pkg->setup == 0
2854 || $discount_show_always
2855 || $cust_bill_pkg->recur_show_zero
2860 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2863 my $is_summary = $display->summary;
2864 my $description = $desc;
2865 if ( $type eq 'U' and defined($r) ) {
2866 # don't just show the same description as the recur line
2867 $description = $self->mt('Usage charges');
2870 my $part_pkg = $cust_pkg->part_pkg;
2872 $description .= $cust_bill_pkg->time_period_pretty( $part_pkg,
2876 my @seconds = (); # for display of usage info
2879 #at least until cust_bill_pkg has "past" ranges in addition to
2880 #the "future" sdate/edate ones... see #3032
2881 my @dates = ( $self->_date );
2882 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2883 push @dates, $prev->sdate if $prev;
2884 push @dates, undef if !$prev;
2886 my @svc_labels = map &{$escape_function}($_),
2887 $cust_pkg->h_labels_short(@dates, 'I');
2888 $svc_label = $svc_labels[0];
2890 # show service labels, unless...
2891 # the package is set not to display them
2892 unless ( $part_pkg->hide_svc_detail
2893 # or this is a tax-like line item
2894 || $cust_bill_pkg->itemdesc
2895 # or this is a hidden (bundled) line item
2896 || $cust_bill_pkg->hidden
2897 # or this is a usage summary line
2898 || $is_summary && $type && $type eq 'U'
2899 # or this is a usage line and there's a recurring line
2900 # for the package in the same section (which will
2901 # have service labels already)
2902 || ($type eq 'U' and defined($r))
2906 warn "$me _items_cust_bill_pkg adding service details\n"
2909 push @d, @svc_labels
2910 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2911 warn "$me _items_cust_bill_pkg done adding service details\n"
2914 my $lnum = $cust_main ? $cust_main->ship_locationnum
2915 : $self->prospect_main->locationnum;
2916 # show the location label if it's not the customer's default
2917 # location, and we're not grouping items by location already
2918 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2919 my $loc = $cust_pkg->location_label;
2920 $loc = substr($loc, 0, $maxlength). '...'
2921 if $format eq 'latex' && length($loc) > $maxlength;
2922 push @d, &{$escape_function}($loc);
2925 # Display of seconds_since_sqlradacct:
2926 # On the invoice, when processing @detail_items, look for a field
2927 # named 'seconds'. This will contain total seconds for each
2928 # service, in the same order as @ext_description. For services
2929 # that don't support this it will show undef.
2930 if ( $conf->exists('svc_acct-usage_seconds')
2931 and ! $cust_bill_pkg->pkgpart_override ) {
2932 foreach my $cust_svc (
2933 $cust_pkg->h_cust_svc(@dates, 'I')
2936 # eval because not having any part_export_usage exports
2937 # is a fatal error, last_bill/_date because that's how
2938 # sqlradius_hour billing does it
2940 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2942 push @seconds, $sec;
2944 } #if svc_acct-usage_seconds
2946 } # if we are showing service labels
2948 unless ( $is_summary ) {
2949 warn "$me _items_cust_bill_pkg adding details\n"
2952 #instead of omitting details entirely in this case (unwanted side
2953 # effects), just omit CDRs
2954 $details_opt{'no_usage'} = 1
2955 if $type && $type eq 'R';
2957 push @d, $cust_bill_pkg->details(%details_opt);
2960 warn "$me _items_cust_bill_pkg calculating amount\n"
2965 $amount = $cust_bill_pkg->recur;
2966 } elsif ($type eq 'R') {
2967 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2968 } elsif ($type eq 'U') {
2969 $amount = $cust_bill_pkg->usage;
2972 if ( !$type || $type eq 'R' ) {
2974 warn "$me _items_cust_bill_pkg adding recur\n"
2978 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2981 if ( $cust_bill_pkg->hidden ) {
2982 $r->{amount} += $amount;
2983 $r->{unit_amount} += $unit_amount;
2984 push @{ $r->{ext_description} }, @d;
2987 description => $description,
2988 pkgpart => $pkgpart,
2989 pkgnum => $cust_bill_pkg->pkgnum,
2991 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2992 unit_amount => $unit_amount,
2993 quantity => $cust_bill_pkg->quantity,
2995 ext_description => \@d,
2996 svc_label => ($svc_label || ''),
2997 locationnum => $cust_pkg->locationnum,
2999 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
3002 } else { # $type eq 'U'
3004 warn "$me _items_cust_bill_pkg adding usage\n"
3007 if ( $cust_bill_pkg->hidden and defined($u) ) {
3008 # if this is a hidden package and there's already a usage
3009 # line for the bundle, add this package's total amount and
3010 # usage details to it
3011 $u->{amount} += $amount;
3012 push @{ $u->{ext_description} }, @d;
3013 } elsif ( $amount ) {
3014 # create a new usage line
3016 description => $description,
3017 pkgpart => $pkgpart,
3018 pkgnum => $cust_bill_pkg->pkgnum,
3021 recur_show_zero => $cust_bill_pkg->recur_show_zero,
3023 ext_description => \@d,
3024 locationnum => $cust_pkg->locationnum,
3026 } # else this has no usage, so don't create a usage section
3029 } # recurring or usage with recurring charge
3031 # decide whether to show active discounts here
3033 # case 1: we are showing a single line for the package
3035 # case 2: we are showing a setup line for a package that has
3036 # no base recurring fee
3037 or ( $type eq 'S' and $cust_bill_pkg->unitrecur == 0 )
3038 # case 3: we are showing a recur line for a package that has
3039 # a base recurring fee
3040 or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
3043 # the line item hashref for the line that will show the original
3045 # (use the recur or single line for the package, unless we're
3046 # showing a setup line for a package with no recurring fee)
3047 my $active_line = $r;
3048 if ( $type eq 'S' ) {
3052 my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
3053 # special case: if there are old "discount details" on this line
3054 # item, don't show discount line items
3055 if ( FS::cust_bill_pkg_detail->count(
3056 "detail LIKE 'Includes discount%' AND billpkgnum = " .
3057 $cust_bill_pkg->billpkgnum
3062 warn "$me _items_cust_bill_pkg including discounts for ".
3063 $cust_bill_pkg->billpkgnum."\n"
3065 my $discount_amount = sum( map {$_->amount} @discounts );
3066 # if multiple discounts apply to the same package, how to display
3067 # them? ext_description lines, apparently
3069 # # discount amounts are negative
3070 if ( $d and $cust_bill_pkg->hidden ) {
3071 $d->{amount} -= $discount_amount;
3076 description => $self->mt('Discount'),
3077 amount => -1 * $discount_amount,
3078 ext_description => \@ext,
3080 foreach my $cust_bill_pkg_discount (@discounts) {
3081 my $def = $cust_bill_pkg_discount->cust_pkg_discount->discount;
3082 push @ext, &{$escape_function}( $def->description );
3086 # update the active line (before the discount) to show the
3087 # original price (whether this is a hidden line or not)
3088 $active_line->{amount} += $discount_amount;
3090 } # if there are any discounts
3091 } # if this is an appropriate place to show discounts
3093 } else { # taxes and fees
3095 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
3098 # items of this kind should normally not have sdate/edate.
3100 'description' => $desc,
3101 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
3102 + $cust_bill_pkg->recur)
3105 } # if quotation / package line item / other line item
3107 } # foreach $display
3109 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
3110 && $conf->exists('discount-show-always'));
3114 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
3116 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3117 if exists($_->{amount});
3118 $_->{amount} =~ s/^\-0\.00$/0.00/;
3119 if (exists($_->{unit_amount})) {
3120 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} );
3124 if $_->{amount} != 0
3125 || $discount_show_always
3126 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
3127 || ( $_->{_is_setup} && $_->{setup_show_zero} )
3131 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
3138 =item _items_discounts_avail
3140 Returns an array of line item hashrefs representing available term discounts
3141 for this invoice. This makes the same assumptions that apply to term
3142 discounts in general: that the package is billed monthly, at a flat rate,
3143 with no usage charges. A prorated first month will be handled, as will
3144 a setup fee if the discount is allowed to apply to setup fees.
3148 sub _items_discounts_avail {
3151 #maybe move this method from cust_bill when quotations support discount_plans
3152 return () unless $self->can('discount_plans');
3153 my %plans = $self->discount_plans;
3155 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
3156 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
3160 my $plan = $plans{$months};
3162 my $term_total = sprintf('%.2f', $plan->discounted_total);
3163 my $percent = sprintf('%.0f',
3164 100 * (1 - $term_total / $plan->base_total) );
3165 my $permonth = sprintf('%.2f', $term_total / $months);
3166 my $detail = $self->mt('discount on item'). ' '.
3167 join(', ', map { "#$_" } $plan->pkgnums)
3170 # discounts for non-integer months don't work anyway
3171 $months = sprintf("%d", $months);
3174 description => $self->mt('Save [_1]% by paying for [_2] months',
3176 amount => $self->mt('[_1] ([_2] per month)',
3177 $term_total, $money_char.$permonth),
3178 ext_description => ($detail || ''),
3181 sort { $b <=> $a } keys %plans;