1 package FS::Template_Mixin;
4 use vars qw( $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use List::Util qw(sum);
11 use Text::Template 1.20;
17 use FS::Record qw( qsearch qsearchs );
18 use FS::Misc qw( generate_ps generate_pdf );
24 $me = '[FS::Template_Mixin]';
25 FS::UID->install_callback( sub {
26 my $conf = new FS::Conf; #global
27 $money_char = $conf->config('money_char') || '$';
28 $date_format = $conf->config('date_format') || '%x'; #/YY
29 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
30 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
33 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
35 Returns an text invoice, as a list of lines.
37 Options can be passed as a hashref (recommended) or as a list of time, template
38 and then any key/value pairs for any other options.
40 I<time>, if specified, is used to control the printing of overdue messages. The
41 default is now. It isn't the date of the invoice; that's the `_date' field.
42 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
43 L<Time::Local> and L<Date::Parse> for conversion functions.
45 I<template>, if specified, is the name of a suffix for alternate invoices.
47 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
53 my( $today, $template, %opt );
56 $today = delete($opt{'time'}) || '';
57 $template = delete($opt{template}) || '';
59 ( $today, $template, %opt ) = @_;
62 my %params = ( 'format' => 'template' );
63 $params{'time'} = $today if $today;
64 $params{'template'} = $template if $template;
65 $params{$_} = $opt{$_}
66 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
68 $self->print_generic( %params );
71 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
73 Internal method - returns a filename of a filled-in LaTeX template for this
74 invoice (Note: add ".tex" to get the actual filename), and a filename of
75 an associated logo (with the .eps extension included).
77 See print_ps and print_pdf for methods that return PostScript and PDF output.
79 Options can be passed as a hashref (recommended) or as a list of time, template
80 and then any key/value pairs for any other options.
82 I<time>, if specified, is used to control the printing of overdue messages. The
83 default is now. It isn't the date of the invoice; that's the `_date' field.
84 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
85 L<Time::Local> and L<Date::Parse> for conversion functions.
87 I<template>, if specified, is the name of a suffix for alternate invoices.
89 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
95 my $conf = $self->conf;
96 my( $today, $template, %opt );
99 $today = delete($opt{'time'}) || '';
100 $template = delete($opt{template}) || '';
102 ( $today, $template, %opt ) = @_;
105 my %params = ( 'format' => 'latex' );
106 $params{'time'} = $today if $today;
107 $params{'template'} = $template if $template;
108 $params{$_} = $opt{$_}
109 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number );
111 $template ||= $self->_agent_template
112 if $self->can('_agent_template');
114 my $pkey = $self->primary_key;
115 my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
117 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
118 my $lh = new File::Temp(
119 TEMPLATE => $tmp_template,
123 ) or die "can't open temp file: $!\n";
125 my $agentnum = $self->agentnum;
127 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
128 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
129 or die "can't write temp file: $!\n";
131 print $lh $conf->config_binary('logo.eps', $agentnum)
132 or die "can't write temp file: $!\n";
135 $params{'logo_file'} = $lh->filename;
137 if( $conf->exists('invoice-barcode')
138 && $self->can('invoice_barcode')
139 && $self->invnum ) { # don't try to barcode statements
140 my $png_file = $self->invoice_barcode($dir);
141 my $eps_file = $png_file;
142 $eps_file =~ s/\.png$/.eps/g;
143 $png_file =~ /(barcode.*png)/;
145 $eps_file =~ /(barcode.*eps)/;
148 my $curr_dir = cwd();
150 # after painfuly long experimentation, it was determined that sam2p won't
151 # accept : and other chars in the path, no matter how hard I tried to
152 # escape them, hence the chdir (and chdir back, just to be safe)
153 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
154 or die "sam2p failed: $!\n";
158 $params{'barcode_file'} = $eps_file;
161 my @filled_in = $self->print_generic( %params );
163 my $fh = new File::Temp( TEMPLATE => $tmp_template,
167 ) or die "can't open temp file: $!\n";
168 binmode($fh, ':utf8'); # language support
169 print $fh join('', @filled_in );
172 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
173 return ($1, $params{'logo_file'}, $params{'barcode_file'});
179 my $cust_main = $self->cust_main;
180 $cust_main ? $cust_main->agentnum : $self->prospect_main->agentnum;
183 =item print_generic OPTION => VALUE ...
185 Internal method - returns a filled-in template for this invoice as a scalar.
187 See print_ps and print_pdf for methods that return PostScript and PDF output.
189 Non optional options include
190 format - latex, html, template
192 Optional options include
194 template - a value used as a suffix for a configuration template
196 time - a value used to control the printing of overdue messages. The
197 default is now. It isn't the date of the invoice; that's the `_date' field.
198 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
199 L<Time::Local> and L<Date::Parse> for conversion functions.
203 unsquelch_cdr - overrides any per customer cdr squelching when true
205 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
207 locale - override customer's locale
211 #what's with all the sprintf('%10.2f')'s in here? will it cause any
212 # (alignment in text invoice?) problems to change them all to '%.2f' ?
213 # yes: fixed width/plain text printing will be borked
215 my( $self, %params ) = @_;
216 my $conf = $self->conf;
217 my $today = $params{today} ? $params{today} : time;
218 warn "$me print_generic called on $self with suffix $params{template}\n"
221 my $format = $params{format};
222 die "Unknown format: $format"
223 unless $format =~ /^(latex|html|template)$/;
225 my $cust_main = $self->cust_main || $self->prospect_main;
226 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
227 unless $cust_main->payname
228 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
230 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
231 'html' => [ '<%=', '%>' ],
232 'template' => [ '{', '}' ],
235 warn "$me print_generic creating template\n"
239 my $template = $params{template} ? $params{template} : $self->_agent_template;
240 my $templatefile = $self->template_conf. $format;
241 $templatefile .= "_$template"
242 if length($template) && $conf->exists($templatefile."_$template");
243 my @invoice_template = map "$_\n", $conf->config($templatefile)
244 or die "cannot load config data $templatefile";
247 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
248 #change this to a die when the old code is removed
249 warn "old-style invoice template $templatefile; ".
250 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
252 @invoice_template = _translate_old_latex_format(@invoice_template);
255 warn "$me print_generic creating T:T object\n"
258 my $text_template = new Text::Template(
260 SOURCE => \@invoice_template,
261 DELIMITERS => $delimiters{$format},
264 warn "$me print_generic compiling T:T object\n"
267 $text_template->compile()
268 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
271 # additional substitution could possibly cause breakage in existing templates
274 'notes' => sub { map "$_", @_ },
275 'footer' => sub { map "$_", @_ },
276 'smallfooter' => sub { map "$_", @_ },
277 'returnaddress' => sub { map "$_", @_ },
278 'coupon' => sub { map "$_", @_ },
279 'summary' => sub { map "$_", @_ },
285 s/%%(.*)$/<!-- $1 -->/g;
286 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
287 s/\\begin\{enumerate\}/<ol>/g;
289 s/\\end\{enumerate\}/<\/ol>/g;
290 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
299 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
301 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
307 s/\\hyphenation\{[\w\s\-]+}//;
312 'coupon' => sub { "" },
313 'summary' => sub { "" },
320 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
321 s/\\begin\{enumerate\}//g;
323 s/\\end\{enumerate\}//g;
324 s/\\textbf\{(.*)\}/$1/g;
331 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
333 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
338 s/\\\\\*?\s*$/\n/; # dubious
339 s/\\hyphenation\{[\w\s\-]+}//;
343 'coupon' => sub { "" },
344 'summary' => sub { "" },
349 # hashes for differing output formats
350 my %nbsps = ( 'latex' => '~',
351 'html' => '', # '&nbps;' would be nice
352 'template' => '', # not used
354 my $nbsp = $nbsps{$format};
356 my %escape_functions = ( 'latex' => \&_latex_escape,
357 'html' => \&_html_escape_nbsp,#\&encode_entities,
358 'template' => sub { shift },
360 my $escape_function = $escape_functions{$format};
361 my $escape_function_nonbsp = ($format eq 'html')
362 ? \&_html_escape : $escape_function;
364 my %date_formats = ( 'latex' => $date_format_long,
365 'html' => $date_format_long,
368 $date_formats{'html'} =~ s/ / /g;
370 my $date_format = $date_formats{$format};
372 my %newline_tokens = ( 'latex' => '\\\\',
376 my $newline_token = $newline_tokens{$format};
378 warn "$me generating template variables\n"
381 # generate template variables
384 defined( $conf->config_orbase( "invoice_${format}returnaddress",
388 && length( $conf->config_orbase( "invoice_${format}returnaddress",
394 $returnaddress = join("\n",
395 $conf->config_orbase("invoice_${format}returnaddress", $template)
399 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
401 my $convert_map = $convert_maps{$format}{'returnaddress'};
404 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
409 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
411 my $convert_map = $convert_maps{$format}{'returnaddress'};
412 $returnaddress = join( "\n", &$convert_map(
413 map { s/( {2,})/'~' x length($1)/eg;
417 ( $conf->config('company_name', $cust_main->agentnum),
418 $conf->config('company_address', $cust_main->agentnum),
425 my $warning = "Couldn't find a return address; ".
426 "do you need to set the company_address configuration value?";
428 $returnaddress = $nbsp;
429 #$returnaddress = $warning;
433 warn "$me generating invoice data\n"
436 my $agentnum = $cust_main->agentnum;
441 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
442 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
443 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
444 'returnaddress' => $returnaddress,
445 'agent' => &$escape_function($cust_main->agent->agent),
447 #invoice/quotation info
448 'no_number' => $params{'no_number'},
449 'invnum' => ( $params{'no_number'} ? '' : $self->invnum ),
450 'quotationnum' => $self->quotationnum,
451 'no_date' => $params{'no_date'},
452 '_date' => ( $params{'no_date'} ? '' : $self->_date ),
453 'date' => ( $params{'no_date'}
455 : time2str($date_format, $self->_date)
457 'today' => time2str($date_format_long, $today),
458 'terms' => $self->terms,
459 'template' => $template, #params{'template'},
460 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
461 'current_charges' => sprintf("%.2f", $self->charged),
462 'duedate' => $self->due_date2str($rdate_format), #date_format?
465 'custnum' => $cust_main->display_custnum,
466 'prospectnum' => $cust_main->prospectnum,
467 'agent_custid' => &$escape_function($cust_main->agent_custid),
468 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
469 payname company address1 address2 city state zip fax
473 'ship_enable' => $conf->exists('invoice-ship_address'),
474 'unitprices' => $conf->exists('invoice-unitprice'),
475 'smallernotes' => $conf->exists('invoice-smallernotes'),
476 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
477 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
479 #layout info -- would be fancy to calc some of this and bury the template
481 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
482 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
483 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
484 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
485 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
486 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
487 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
488 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
489 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
490 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
492 # better hang on to conf_dir for a while (for old templates)
493 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
495 #these are only used when doing paged plaintext
502 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
503 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
504 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
505 # eval to avoid death for unimplemented languages
506 my $dh = eval { Date::Language->new($info{'name'}) } ||
507 Date::Language->new(); # fall back to English
508 # prototype here to silence warnings
509 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
510 # eventually use this date handle everywhere in here, too
512 my $min_sdate = 999999999999;
514 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
515 next unless $cust_bill_pkg->pkgnum > 0;
516 $min_sdate = $cust_bill_pkg->sdate
517 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
518 $max_edate = $cust_bill_pkg->edate
519 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
522 $invoice_data{'bill_period'} = '';
523 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
524 . " to " . time2str('%e %h', $max_edate)
525 if ($max_edate != 0 && $min_sdate != 999999999999);
527 $invoice_data{finance_section} = '';
528 if ( $conf->config('finance_pkgclass') ) {
530 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
531 $invoice_data{finance_section} = $pkg_class->categoryname;
533 $invoice_data{finance_amount} = '0.00';
534 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
536 my $countrydefault = $conf->config('countrydefault') || 'US';
537 foreach ( qw( address1 address2 city state zip country fax) ){
538 my $method = 'ship_'.$_;
539 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
541 foreach ( qw( contact company ) ) { #compatibility
542 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
544 $invoice_data{'ship_country'} = ''
545 if ( $invoice_data{'ship_country'} eq $countrydefault );
547 $invoice_data{'cid'} = $params{'cid'}
550 if ( $cust_main->country eq $countrydefault ) {
551 $invoice_data{'country'} = '';
553 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
557 $invoice_data{'address'} = \@address;
560 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
561 ? " (P.O. #". $cust_main->payinfo. ")"
565 push @address, $cust_main->company
566 if $cust_main->company;
567 push @address, $cust_main->address1;
568 push @address, $cust_main->address2
569 if $cust_main->address2;
571 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
572 push @address, $invoice_data{'country'}
573 if $invoice_data{'country'};
575 while (scalar(@address) < 5);
577 $invoice_data{'logo_file'} = $params{'logo_file'}
578 if $params{'logo_file'};
579 $invoice_data{'barcode_file'} = $params{'barcode_file'}
580 if $params{'barcode_file'};
581 $invoice_data{'barcode_img'} = $params{'barcode_img'}
582 if $params{'barcode_img'};
583 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
584 if $params{'barcode_cid'};
586 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
587 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
588 #my $balance_due = $self->owed + $pr_total - $cr_total;
589 my $balance_due = $self->owed + $pr_total;
591 #these are used on the summary page only
593 # the customer's current balance as shown on the invoice before this one
594 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
596 # the change in balance from that invoice to this one
597 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
599 # the sum of amount owed on all previous invoices
600 # ($pr_total is used elsewhere but not as $previous_balance)
601 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
603 # the sum of amount owed on all invoices
604 # (this is used in the summary & on the payment coupon)
605 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
607 # info from customer's last invoice before this one, for some
609 $invoice_data{'last_bill'} = {};
611 # returns the last unpaid bill, not the last bill
612 #my $last_bill = $pr_cust_bill[-1];
614 if ( $self->custnum && $self->invnum ) {
616 # THIS returns the customer's last bill before this one
617 my $last_bill = qsearchs({
618 'table' => 'cust_bill',
619 'hashref' => { 'custnum' => $self->custnum,
620 'invnum' => { op => '<', value => $self->invnum },
622 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
625 $invoice_data{'last_bill'} = {
626 '_date' => $last_bill->_date, #unformatted
627 # all we need for now
629 my (@payments, @credits);
630 # for formats that itemize previous payments
631 foreach my $cust_pay ( qsearch('cust_pay', {
632 'custnum' => $self->custnum,
633 '_date' => { op => '>=',
634 value => $last_bill->_date }
637 next if $cust_pay->_date > $self->_date;
639 '_date' => $cust_pay->_date,
640 'date' => time2str($date_format, $cust_pay->_date),
641 'payinfo' => $cust_pay->payby_payinfo_pretty,
642 'amount' => sprintf('%.2f', $cust_pay->paid),
644 # not concerned about applications
646 foreach my $cust_credit ( qsearch('cust_credit', {
647 'custnum' => $self->custnum,
648 '_date' => { op => '>=',
649 value => $last_bill->_date }
652 next if $cust_credit->_date > $self->_date;
654 '_date' => $cust_credit->_date,
655 'date' => time2str($date_format, $cust_credit->_date),
656 'creditreason'=> $cust_credit->reason,
657 'amount' => sprintf('%.2f', $cust_credit->amount),
660 $invoice_data{'previous_payments'} = \@payments;
661 $invoice_data{'previous_credits'} = \@credits;
666 my $summarypage = '';
667 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
670 $invoice_data{'summarypage'} = $summarypage;
672 warn "$me substituting variables in notes, footer, smallfooter\n"
675 my $tc = $self->template_conf;
676 my @include = ( [ $tc, 'notes' ],
677 [ 'invoice_', 'footer' ],
678 [ 'invoice_', 'smallfooter', ],
680 push @include, [ $tc, 'coupon', ]
681 unless $params{'no_coupon'};
683 foreach my $i (@include) {
685 my($base, $include) = @$i;
687 my $inc_file = $conf->key_orbase("$base$format$include", $template);
690 if ( $conf->exists($inc_file, $agentnum)
691 && length( $conf->config($inc_file, $agentnum) ) ) {
693 @inc_src = $conf->config($inc_file, $agentnum);
697 $inc_file = $conf->key_orbase("${base}latex$include", $template);
699 my $convert_map = $convert_maps{$format}{$include};
701 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
702 s/--\@\]/$delimiters{$format}[1]/g;
705 &$convert_map( $conf->config($inc_file, $agentnum) );
709 my $inc_tt = new Text::Template (
711 SOURCE => [ map "$_\n", @inc_src ],
712 DELIMITERS => $delimiters{$format},
713 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
715 unless ( $inc_tt->compile() ) {
716 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
717 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
721 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
723 $invoice_data{$include} =~ s/\n+$//
724 if ($format eq 'latex');
727 # let invoices use either of these as needed
728 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
729 ? $cust_main->payinfo : '';
730 $invoice_data{'po_line'} =
731 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
732 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
735 my %money_chars = ( 'latex' => '',
736 'html' => $conf->config('money_char') || '$',
739 my $money_char = $money_chars{$format};
741 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
742 'html' => $conf->config('money_char') || '$',
745 my $other_money_char = $other_money_chars{$format};
746 $invoice_data{'dollar'} = $other_money_char;
748 my %minus_signs = ( 'latex' => '$-$',
750 'template' => '- ' );
751 my $minus = $minus_signs{$format};
753 my @detail_items = ();
754 my @total_items = ();
758 $invoice_data{'detail_items'} = \@detail_items;
759 $invoice_data{'total_items'} = \@total_items;
760 $invoice_data{'buf'} = \@buf;
761 $invoice_data{'sections'} = \@sections;
763 warn "$me generating sections\n"
766 # Previous Charges section
767 # subtotal is the first return value from $self->previous
768 my $previous_section = { 'description' => $self->mt('Previous Charges'),
769 'subtotal' => $other_money_char.
770 sprintf('%.2f', $pr_total),
771 'summarized' => '', #why? $summarypage ? 'Y' : '',
773 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
774 join(' / ', map { $cust_main->balance_date_range(@$_) }
775 $self->_prior_month30s
777 if $conf->exists('invoice_include_aging');
780 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
781 'subtotal' => $taxtotal, # adjusted below
783 my $tax_weight = _pkg_category($tax_section->{description})
784 ? _pkg_category($tax_section->{description})->weight
786 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
787 $tax_section->{'sort_weight'} = $tax_weight;
791 my $adjust_section = {
792 'description' => $self->mt('Credits, Payments, and Adjustments'),
793 'adjust_section' => 1,
794 'subtotal' => 0, # adjusted below
796 my $adjust_weight = _pkg_category($adjust_section->{description})
797 ? _pkg_category($adjust_section->{description})->weight
799 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
800 $adjust_section->{'sort_weight'} = $adjust_weight;
802 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
803 my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
804 $conf->exists($tc.'sections_by_location', $cust_main->agentnum);
805 $invoice_data{'multisection'} = $multisection;
807 my $extra_sections = [];
808 my $extra_lines = ();
810 my $default_section = { 'description' => '',
815 if ( $multisection ) {
816 ($extra_sections, $extra_lines) =
817 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
818 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
819 && $self->can('_items_extra_usage_sections');
821 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
823 push @detail_items, @$extra_lines if $extra_lines;
825 # the code is written so that both methods can be used together, but
826 # we haven't yet changed the template to take advantage of that, so for
827 # now, treat them as mutually exclusive.
828 my %section_method = ( by_category => 1 );
829 if ( $conf->exists($tc.'sections_by_location') ) {
830 %section_method = ( by_location => 1 );
833 $self->_items_sections( 'summary' => $summarypage,
834 'escape' => $escape_function_nonbsp,
835 'extra_sections' => $extra_sections,
839 push @sections, @$early;
840 $late_sections = $late;
842 if ( $conf->exists('svc_phone_sections')
843 && $self->can('_items_svc_phone_sections')
846 my ($phone_sections, $phone_lines) =
847 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
848 push @{$late_sections}, @$phone_sections;
849 push @detail_items, @$phone_lines;
851 if ( $conf->exists('voip-cust_accountcode_cdr')
852 && $cust_main->accountcode_cdr
853 && $self->can('_items_accountcode_cdr')
856 my ($accountcode_section, $accountcode_lines) =
857 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
858 if ( scalar(@$accountcode_lines) ) {
859 push @{$late_sections}, $accountcode_section;
860 push @detail_items, @$accountcode_lines;
863 } else {# not multisection
864 # make a default section
865 push @sections, $default_section;
866 # and calculate the finance charge total, since it won't get done otherwise.
867 # XXX possibly other totals?
868 # XXX possibly finance_pkgclass should not be used in this manner?
869 if ( $conf->exists('finance_pkgclass') ) {
871 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
872 if ( grep { $_->section eq $invoice_data{finance_section} }
873 $cust_bill_pkg->cust_bill_pkg_display ) {
874 # I think these are always setup fees, but just to be sure...
875 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
878 $invoice_data{finance_amount} =
879 sprintf('%.2f', sum( @finance_charges ) || 0);
883 # previous invoice balances in the Previous Charges section if there
884 # is one, otherwise in the main detail section
885 if ( $self->can('_items_previous') &&
886 $self->enable_previous &&
887 ! $conf->exists('previous_balance-summary_only') ) {
889 warn "$me adding previous balances\n"
892 foreach my $line_item ( $self->_items_previous ) {
895 ext_description => [],
897 $detail->{'ref'} = $line_item->{'pkgnum'};
898 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
899 $detail->{'quantity'} = 1;
900 $detail->{'section'} = $multisection ? $previous_section
902 $detail->{'description'} = &$escape_function($line_item->{'description'});
903 if ( exists $line_item->{'ext_description'} ) {
904 @{$detail->{'ext_description'}} = map {
905 &$escape_function($_);
906 } @{$line_item->{'ext_description'}};
908 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
909 $line_item->{'amount'};
910 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
912 push @detail_items, $detail;
913 push @buf, [ $detail->{'description'},
914 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
920 if ( @pr_cust_bill && $self->enable_previous ) {
921 push @buf, ['','-----------'];
922 push @buf, [ $self->mt('Total Previous Balance'),
923 $money_char. sprintf("%10.2f", $pr_total) ];
927 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
928 warn "$me adding DID summary\n"
931 my ($didsummary,$minutes) = $self->_did_summary;
932 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
934 { 'description' => $didsummary_desc,
935 'ext_description' => [ $didsummary, $minutes ],
939 foreach my $section (@sections, @$late_sections) {
941 warn "$me adding section \n". Dumper($section)
944 # begin some normalization
945 $section->{'subtotal'} = $section->{'amount'}
947 && !exists($section->{subtotal})
948 && exists($section->{amount});
950 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
951 if ( $invoice_data{finance_section} &&
952 $section->{'description'} eq $invoice_data{finance_section} );
954 $section->{'subtotal'} = $other_money_char.
955 sprintf('%.2f', $section->{'subtotal'})
958 # continue some normalization
959 $section->{'amount'} = $section->{'subtotal'}
963 if ( $section->{'description'} ) {
964 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
969 warn "$me setting options\n"
973 $options{'section'} = $section if $multisection;
974 $options{'format'} = $format;
975 $options{'escape_function'} = $escape_function;
976 $options{'no_usage'} = 1 unless $unsquelched;
977 $options{'unsquelched'} = $unsquelched;
978 $options{'summary_page'} = $summarypage;
979 $options{'skip_usage'} =
980 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
982 warn "$me searching for line items\n"
985 foreach my $line_item ( $self->_items_pkg(%options) ) {
987 warn "$me adding line item $line_item\n"
991 ext_description => [],
993 $detail->{'ref'} = $line_item->{'pkgnum'};
994 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
995 $detail->{'quantity'} = $line_item->{'quantity'};
996 $detail->{'section'} = $section;
997 $detail->{'description'} = &$escape_function($line_item->{'description'});
998 if ( exists $line_item->{'ext_description'} ) {
999 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
1001 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
1002 $line_item->{'amount'};
1003 if ( exists $line_item->{'unit_amount'} ) {
1004 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
1005 $line_item->{'unit_amount'};
1007 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1009 $detail->{'sdate'} = $line_item->{'sdate'};
1010 $detail->{'edate'} = $line_item->{'edate'};
1011 $detail->{'seconds'} = $line_item->{'seconds'};
1012 $detail->{'svc_label'} = $line_item->{'svc_label'};
1014 push @detail_items, $detail;
1015 push @buf, ( [ $detail->{'description'},
1016 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1018 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1022 if ( $section->{'description'} ) {
1023 push @buf, ( ['','-----------'],
1024 [ $section->{'description'}. ' sub-total',
1025 $section->{'subtotal'} # already formatted this
1034 $invoice_data{current_less_finance} =
1035 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1037 # create a major section for previous balance if we have major sections,
1038 # or if previous_section is in summary form
1039 if ( ( $multisection && $self->enable_previous )
1040 || $conf->exists('previous_balance-summary_only') )
1042 unshift @sections, $previous_section if $pr_total;
1045 warn "$me adding taxes\n"
1048 my @items_tax = $self->_items_tax;
1049 foreach my $tax ( @items_tax ) {
1051 $taxtotal += $tax->{'amount'};
1053 my $description = &$escape_function( $tax->{'description'} );
1054 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1056 if ( $multisection ) {
1058 my $money = $old_latex ? '' : $money_char;
1059 push @detail_items, {
1060 ext_description => [],
1063 description => $description,
1064 amount => $money. $amount,
1066 section => $tax_section,
1071 push @total_items, {
1072 'total_item' => $description,
1073 'total_amount' => $other_money_char. $amount,
1078 push @buf,[ $description,
1079 $money_char. $amount,
1086 $total->{'total_item'} = $self->mt('Sub-total');
1087 $total->{'total_amount'} =
1088 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1090 if ( $multisection ) {
1091 $tax_section->{'subtotal'} = $other_money_char.
1092 sprintf('%.2f', $taxtotal);
1093 $tax_section->{'pretotal'} = 'New charges sub-total '.
1094 $total->{'total_amount'};
1095 push @sections, $tax_section if $taxtotal;
1097 unshift @total_items, $total;
1100 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1102 push @buf,['','-----------'];
1103 push @buf,[$self->mt(
1104 (!$self->enable_previous)
1106 : 'Total New Charges'
1108 $money_char. sprintf("%10.2f",$self->charged) ];
1116 my %embolden_functions = (
1117 'latex' => sub { return '\textbf{'. shift(). '}' },
1118 'html' => sub { return '<b>'. shift(). '</b>' },
1119 'template' => sub { shift },
1121 my $embolden_function = $embolden_functions{$format};
1123 if ( $self->can('_items_total') ) { # quotations
1125 $self->_items_total(\@total_items);
1127 foreach ( @total_items ) {
1128 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1129 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1130 $_->{'total_amount'}
1134 } else { #normal invoice case
1136 # calculate total, possibly including total owed on previous
1140 $item = $conf->config('previous_balance-exclude_from_total')
1141 || 'Total New Charges'
1142 if $conf->exists('previous_balance-exclude_from_total');
1143 my $amount = $self->charged;
1144 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1145 $amount += $pr_total;
1148 $total->{'total_item'} = &$embolden_function($self->mt($item));
1149 $total->{'total_amount'} =
1150 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1151 if ( $multisection ) {
1152 if ( $adjust_section->{'sort_weight'} ) {
1153 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1154 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1156 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1157 $other_money_char. sprintf('%.2f', $self->charged );
1160 push @total_items, $total;
1162 push @buf,['','-----------'];
1165 sprintf( '%10.2f', $amount )
1169 # if we're showing previous invoices, also show previous
1170 # credits and payments
1171 if ( $self->enable_previous
1172 and $self->can('_items_credits')
1173 and $self->can('_items_payments') )
1175 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1178 my $credittotal = 0;
1179 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1182 $total->{'total_item'} = &$escape_function($credit->{'description'});
1183 $credittotal += $credit->{'amount'};
1184 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1185 $adjusttotal += $credit->{'amount'};
1186 if ( $multisection ) {
1187 my $money = $old_latex ? '' : $money_char;
1188 push @detail_items, {
1189 ext_description => [],
1192 description => &$escape_function($credit->{'description'}),
1193 amount => $money. $credit->{'amount'},
1195 section => $adjust_section,
1198 push @total_items, $total;
1202 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1205 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1206 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1210 my $paymenttotal = 0;
1211 foreach my $payment ( $self->_items_payments ) {
1213 $total->{'total_item'} = &$escape_function($payment->{'description'});
1214 $paymenttotal += $payment->{'amount'};
1215 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1216 $adjusttotal += $payment->{'amount'};
1217 if ( $multisection ) {
1218 my $money = $old_latex ? '' : $money_char;
1219 push @detail_items, {
1220 ext_description => [],
1223 description => &$escape_function($payment->{'description'}),
1224 amount => $money. $payment->{'amount'},
1226 section => $adjust_section,
1229 push @total_items, $total;
1231 push @buf, [ $payment->{'description'},
1232 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1235 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1237 if ( $multisection ) {
1238 $adjust_section->{'subtotal'} = $other_money_char.
1239 sprintf('%.2f', $adjusttotal);
1240 push @sections, $adjust_section
1241 unless $adjust_section->{sort_weight};
1244 # create Balance Due message
1247 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1248 $total->{'total_amount'} =
1249 &$embolden_function(
1250 $other_money_char. sprintf('%.2f', #why? $summarypage
1251 # ? $self->charged +
1252 # $self->billing_balance
1254 $self->owed + $pr_total
1257 if ( $multisection && !$adjust_section->{sort_weight} ) {
1258 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1259 $total->{'total_amount'};
1261 push @total_items, $total;
1263 push @buf,['','-----------'];
1264 push @buf,[$self->balance_due_msg, $money_char.
1265 sprintf("%10.2f", $balance_due ) ];
1268 if ( $conf->exists('previous_balance-show_credit')
1269 and $cust_main->balance < 0 ) {
1270 my $credit_total = {
1271 'total_item' => &$embolden_function($self->credit_balance_msg),
1272 'total_amount' => &$embolden_function(
1273 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1276 if ( $multisection ) {
1277 $adjust_section->{'posttotal'} .= $newline_token .
1278 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1281 push @total_items, $credit_total;
1283 push @buf,['','-----------'];
1284 push @buf,[$self->credit_balance_msg, $money_char.
1285 sprintf("%10.2f", -$cust_main->balance ) ];
1289 } #end of default total adding ! can('_items_total')
1291 if ( $multisection ) {
1292 if ( $conf->exists('svc_phone_sections')
1293 && $self->can('_items_svc_phone_sections')
1297 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1298 $total->{'total_amount'} =
1299 &$embolden_function(
1300 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1302 my $last_section = pop @sections;
1303 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1304 $total->{'total_amount'};
1305 push @sections, $last_section;
1307 push @sections, @$late_sections
1311 # make a discounts-available section, even without multisection
1312 if ( $conf->exists('discount-show_available')
1313 and my @discounts_avail = $self->_items_discounts_avail ) {
1314 my $discount_section = {
1315 'description' => $self->mt('Discounts Available'),
1320 push @sections, $discount_section;
1321 push @detail_items, map { +{
1322 'ref' => '', #should this be something else?
1323 'section' => $discount_section,
1324 'description' => &$escape_function( $_->{description} ),
1325 'amount' => $money_char . &$escape_function( $_->{amount} ),
1326 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1327 } } @discounts_avail;
1330 # debugging hook: call this with 'diag' => 1 to just get a hash of
1331 # the invoice variables
1332 return \%invoice_data if ( $params{'diag'} );
1334 # All sections and items are built; now fill in templates.
1335 my @includelist = ();
1336 push @includelist, 'summary' if $summarypage;
1337 foreach my $include ( @includelist ) {
1339 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1342 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1344 @inc_src = $conf->config($inc_file, $agentnum);
1348 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1350 my $convert_map = $convert_maps{$format}{$include};
1352 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1353 s/--\@\]/$delimiters{$format}[1]/g;
1356 &$convert_map( $conf->config($inc_file, $agentnum) );
1360 my $inc_tt = new Text::Template (
1362 SOURCE => [ map "$_\n", @inc_src ],
1363 DELIMITERS => $delimiters{$format},
1364 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1366 unless ( $inc_tt->compile() ) {
1367 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1368 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1372 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1374 $invoice_data{$include} =~ s/\n+$//
1375 if ($format eq 'latex');
1380 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1381 /invoice_lines\((\d*)\)/;
1382 $invoice_lines += $1 || scalar(@buf);
1385 die "no invoice_lines() functions in template?"
1386 if ( $format eq 'template' && !$wasfunc );
1388 if ($format eq 'template') {
1390 if ( $invoice_lines ) {
1391 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1392 $invoice_data{'total_pages'}++
1393 if scalar(@buf) % $invoice_lines;
1396 #setup subroutine for the template
1397 $invoice_data{invoice_lines} = sub {
1398 my $lines = shift || scalar(@buf);
1410 push @collect, split("\n",
1411 $text_template->fill_in( HASH => \%invoice_data )
1413 $invoice_data{'page'}++;
1415 map "$_\n", @collect;
1417 } else { # this is where we actually create the invoice
1419 warn "filling in template for invoice ". $self->invnum. "\n"
1421 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1424 $text_template->fill_in(HASH => \%invoice_data);
1428 sub notice_name { '('.shift->table.')'; }
1430 sub template_conf { 'invoice_'; }
1432 # helper routine for generating date ranges
1433 sub _prior_month30s {
1436 [ 1, 2592000 ], # 0-30 days ago
1437 [ 2592000, 5184000 ], # 30-60 days ago
1438 [ 5184000, 7776000 ], # 60-90 days ago
1439 [ 7776000, 0 ], # 90+ days ago
1442 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1443 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1448 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1450 Returns an postscript invoice, as a scalar.
1452 Options can be passed as a hashref (recommended) or as a list of time, template
1453 and then any key/value pairs for any other options.
1455 I<time> an optional value used to control the printing of overdue messages. The
1456 default is now. It isn't the date of the invoice; that's the `_date' field.
1457 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1458 L<Time::Local> and L<Date::Parse> for conversion functions.
1460 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1467 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1468 my $ps = generate_ps($file);
1470 unlink($barcodefile) if $barcodefile;
1475 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1477 Returns an PDF invoice, as a scalar.
1479 Options can be passed as a hashref (recommended) or as a list of time, template
1480 and then any key/value pairs for any other options.
1482 I<time> an optional value used to control the printing of overdue messages. The
1483 default is now. It isn't the date of the invoice; that's the `_date' field.
1484 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1485 L<Time::Local> and L<Date::Parse> for conversion functions.
1487 I<template>, if specified, is the name of a suffix for alternate invoices.
1489 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1496 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1497 my $pdf = generate_pdf($file);
1499 unlink($barcodefile) if $barcodefile;
1504 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1506 Returns an HTML invoice, as a scalar.
1508 I<time> an optional value used to control the printing of overdue messages. The
1509 default is now. It isn't the date of the invoice; that's the `_date' field.
1510 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1511 L<Time::Local> and L<Date::Parse> for conversion functions.
1513 I<template>, if specified, is the name of a suffix for alternate invoices.
1515 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1517 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1518 when emailing the invoice as part of a multipart/related MIME email.
1526 %params = %{ shift() };
1528 $params{'time'} = shift;
1529 $params{'template'} = shift;
1530 $params{'cid'} = shift;
1533 $params{'format'} = 'html';
1535 $self->print_generic( %params );
1538 # quick subroutine for print_latex
1540 # There are ten characters that LaTeX treats as special characters, which
1541 # means that they do not simply typeset themselves:
1542 # # $ % & ~ _ ^ \ { }
1544 # TeX ignores blanks following an escaped character; if you want a blank (as
1545 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1549 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1550 $value =~ s/([<>])/\$$1\$/g;
1556 encode_entities($value);
1560 sub _html_escape_nbsp {
1561 my $value = _html_escape(shift);
1562 $value =~ s/ +/ /g;
1566 #utility methods for print_*
1568 sub _translate_old_latex_format {
1569 warn "_translate_old_latex_format called\n"
1576 if ( $line =~ /^%%Detail\s*$/ ) {
1578 push @template, q![@--!,
1579 q! foreach my $_tr_line (@detail_items) {!,
1580 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1581 q! $_tr_line->{'description'} .= !,
1582 q! "\\tabularnewline\n~~".!,
1583 q! join( "\\tabularnewline\n~~",!,
1584 q! @{$_tr_line->{'ext_description'}}!,
1588 while ( ( my $line_item_line = shift )
1589 !~ /^%%EndDetail\s*$/ ) {
1590 $line_item_line =~ s/'/\\'/g; # nice LTS
1591 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1592 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1593 push @template, " \$OUT .= '$line_item_line';";
1596 push @template, '}',
1599 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1601 push @template, '[@--',
1602 ' foreach my $_tr_line (@total_items) {';
1604 while ( ( my $total_item_line = shift )
1605 !~ /^%%EndTotalDetails\s*$/ ) {
1606 $total_item_line =~ s/'/\\'/g; # nice LTS
1607 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1608 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1609 push @template, " \$OUT .= '$total_item_line';";
1612 push @template, '}',
1616 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1617 push @template, $line;
1623 warn "$_\n" foreach @template;
1631 my $conf = $self->conf;
1633 #check for an invoice-specific override
1634 return $self->invoice_terms if $self->invoice_terms;
1636 #check for a customer- specific override
1637 my $cust_main = $self->cust_main;
1638 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1640 #use configured default
1641 $conf->config('invoice_default_terms') || '';
1647 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1648 $duedate = $self->_date() + ( $1 * 86400 );
1655 $self->due_date ? time2str(shift, $self->due_date) : '';
1658 sub balance_due_msg {
1660 my $msg = $self->mt('Balance Due');
1661 return $msg unless $self->terms;
1662 if ( $self->due_date ) {
1663 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1664 $self->due_date2str($date_format);
1665 } elsif ( $self->terms ) {
1666 $msg .= ' - '. $self->terms;
1671 sub balance_due_date {
1673 my $conf = $self->conf;
1675 if ( $conf->exists('invoice_default_terms')
1676 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1677 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1682 sub credit_balance_msg {
1684 $self->mt('Credit Balance Remaining')
1689 Returns a string with the date, for example: "3/20/2008"
1695 time2str($date_format, $self->_date);
1698 =item _items_sections OPTIONS
1700 Generate section information for all items appearing on this invoice.
1701 This will only be called for multi-section invoices.
1703 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1704 related display records (L<FS::cust_bill_pkg_display>) and organize
1705 them into two groups ("early" and "late" according to whether they come
1706 before or after the total), then into sections. A subtotal is calculated
1709 Section descriptions are returned in sort weight order. Each consists
1710 of a hash containing:
1712 description: the package category name, escaped
1713 subtotal: the total charges in that section
1714 tax_section: a flag indicating that the section contains only tax charges
1715 summarized: same as tax_section, for some reason
1716 sort_weight: the package category's sort weight
1718 If 'condense' is set on the display record, it also contains everything
1719 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1720 coderefs to generate parts of the invoice. This is not advised.
1722 The method returns two arrayrefs, one of "early" sections and one of "late"
1725 OPTIONS may include:
1727 by_location: a flag to divide the invoice into sections by location.
1728 Each section hash will have a 'location' element containing a hashref of
1729 the location fields (see L<FS::cust_location>). The section description
1730 will be the location label, but the template can use any of the location
1731 fields to create a suitable label.
1733 by_category: a flag to divide the invoice into sections using display
1734 records (see L<FS::cust_bill_pkg_display>). This is the "traditional"
1735 behavior. Each section hash will have a 'category' element containing
1736 the section name from the display record (which probably equals the
1737 category name of the package, but may not in some cases).
1739 summary: a flag indicating that this is a summary-format invoice.
1740 Turning this on has the following effects:
1741 - Ignores display items with the 'summary' flag.
1742 - Places all sections in the "early" group even if they have post_total.
1743 - Creates sections for all non-disabled package categories, even if they
1744 have no charges on this invoice, as well as a section with no name.
1746 escape: an escape function to use for section titles.
1748 extra_sections: an arrayref of additional sections to return after the
1749 sorted list. If there are any of these, section subtotals exclude
1752 format: 'latex', 'html', or 'template' (i.e. text). Not used, but
1753 passed through to C<_condense_section()>.
1757 use vars qw(%pkg_category_cache);
1758 sub _items_sections {
1762 my $escape = $opt{escape};
1763 my @extra_sections = @{ $opt{extra_sections} || [] };
1765 # $subtotal{$locationnum}{$categoryname} = amount.
1766 # if we're not using by_location, $locationnum is undef.
1767 # if we're not using by_category, you guessed it, $categoryname is undef.
1768 # if we're not using either one, we shouldn't be here in the first place...
1770 my %late_subtotal = ();
1773 # About tax items + multisection invoices:
1774 # If either invoice_*summary option is enabled, AND there is a
1775 # package category with the name of the tax, then there will be
1776 # a display record assigning the tax item to that category.
1778 # However, the taxes are always placed in the "Taxes, Surcharges,
1779 # and Fees" section regardless of that. The only effect of the
1780 # display record is to create a subtotal for the summary page.
1783 my $pkg_hash = $self->cust_pkg_hash;
1785 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1788 my $usage = $cust_bill_pkg->usage;
1791 if ( $opt{by_location} ) {
1792 if ( $cust_bill_pkg->pkgnum ) {
1793 $locationnum = $pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum;
1798 $locationnum = undef;
1801 # as in _items_cust_pkg, if a line item has no display records,
1802 # cust_bill_pkg_display() returns a default record for it
1804 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1805 next if ( $display->summary && $opt{summary} );
1807 my $section = $display->section;
1808 my $type = $display->type;
1809 $section = undef unless $opt{by_category};
1811 $not_tax{$locationnum}{$section} = 1
1812 unless $cust_bill_pkg->pkgnum == 0;
1814 # there's actually a very important piece of logic buried in here:
1815 # incrementing $late_subtotal{$section} CREATES
1816 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1817 # to define the list of late sections, and likewise keys(%subtotal).
1818 # When _items_cust_bill_pkg is called to generate line items for
1819 # real, it will be called with 'section' => $section for each
1821 if ( $display->post_total && !$opt{summary} ) {
1822 if (! $type || $type eq 'S') {
1823 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1824 if $cust_bill_pkg->setup != 0
1825 || $cust_bill_pkg->setup_show_zero;
1829 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1830 if $cust_bill_pkg->recur != 0
1831 || $cust_bill_pkg->recur_show_zero;
1834 if ($type && $type eq 'R') {
1835 $late_subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1836 if $cust_bill_pkg->recur != 0
1837 || $cust_bill_pkg->recur_show_zero;
1840 if ($type && $type eq 'U') {
1841 $late_subtotal{$locationnum}{$section} += $usage
1842 unless scalar(@extra_sections);
1845 } else { # it's a pre-total (normal) section
1847 # skip tax items unless they're explicitly included in a section
1848 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1850 if (! $type || $type eq 'S') {
1851 $subtotal{$locationnum}{$section} += $cust_bill_pkg->setup
1852 if $cust_bill_pkg->setup != 0
1853 || $cust_bill_pkg->setup_show_zero;
1857 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur
1858 if $cust_bill_pkg->recur != 0
1859 || $cust_bill_pkg->recur_show_zero;
1862 if ($type && $type eq 'R') {
1863 $subtotal{$locationnum}{$section} += $cust_bill_pkg->recur - $usage
1864 if $cust_bill_pkg->recur != 0
1865 || $cust_bill_pkg->recur_show_zero;
1868 if ($type && $type eq 'U') {
1869 $subtotal{$locationnum}{$section} += $usage
1870 unless scalar(@extra_sections);
1879 %pkg_category_cache = ();
1881 # summary invoices need subtotals for all non-disabled package categories,
1882 # even if they're zero
1883 # but currently assume that there are no location sections, or at least
1884 # that the summary page doesn't care about them
1885 if ( $opt{summary} ) {
1886 foreach my $category (qsearch('pkg_category', {disabled => ''})) {
1887 $subtotal{''}{$category->categoryname} ||= 0;
1889 $subtotal{''}{''} ||= 0;
1893 foreach my $post_total (0,1) {
1895 my $s = $post_total ? \%late_subtotal : \%subtotal;
1896 foreach my $locationnum (keys %$s) {
1897 foreach my $sectionname (keys %{ $s->{$locationnum} }) {
1899 'subtotal' => $s->{$locationnum}{$sectionname},
1900 'post_total' => $post_total,
1903 if ( $locationnum ) {
1904 $section->{'locationnum'} = $locationnum;
1905 my $location = FS::cust_location->by_key($locationnum);
1906 $section->{'description'} = &{ $escape }($location->location_label);
1907 # Better ideas? This will roughly group them by proximity,
1908 # which alpha sorting on any of the address fields won't.
1909 # Sorting by locationnum is meaningless.
1910 # We have to sort on _something_ or the order may change
1911 # randomly from one invoice to the next, which will confuse
1913 $section->{'sort_weight'} = sprintf('%012s',$location->zip) .
1915 $section->{'location'} = {
1916 map { $_ => &{ $escape }($location->get($_)) }
1920 $section->{'category'} = $sectionname;
1921 $section->{'description'} = &{ $escape }($sectionname);
1922 if ( _pkg_category($_) ) {
1923 $section->{'sort_weight'} = _pkg_category($_)->weight;
1924 if ( _pkg_category($_)->condense ) {
1925 $section = { %$section, $self->_condense_section($opt{format}) };
1929 if ( !$post_total and !$not_tax{$locationnum}{$sectionname} ) {
1930 # then it's a tax-only section
1931 $section->{'summarized'} = 'Y';
1932 $section->{'tax_section'} = 'Y';
1934 push @these, $section;
1935 } # foreach $sectionname
1936 } #foreach $locationnum
1937 push @these, @extra_sections if $post_total == 0;
1938 # need an alpha sort for location sections, because postal codes can
1940 $sections[ $post_total ] = [ sort {
1941 $opt{'by_location'} ?
1942 ($a->{sort_weight} cmp $b->{sort_weight}) :
1943 ($a->{sort_weight} <=> $b->{sort_weight})
1945 } #foreach $post_total
1947 return @sections; # early, late
1950 #helper subs for above
1954 $self->{cust_pkg} ||= { map { $_->pkgnum => $_ } $self->cust_pkg };
1958 my $categoryname = shift;
1959 $pkg_category_cache{$categoryname} ||=
1960 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1963 my %condensed_format = (
1964 'label' => [ qw( Description Qty Amount ) ],
1966 sub { shift->{description} },
1967 sub { shift->{quantity} },
1968 sub { my($href, %opt) = @_;
1969 ($opt{dollar} || ''). $href->{amount};
1972 'align' => [ qw( l r r ) ],
1973 'span' => [ qw( 5 1 1 ) ], # unitprices?
1974 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1977 sub _condense_section {
1978 my ( $self, $format ) = ( shift, shift );
1980 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1981 qw( description_generator
1984 total_line_generator
1989 sub _condensed_generator_defaults {
1990 my ( $self, $format ) = ( shift, shift );
1991 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
2000 sub _condensed_header_generator {
2001 my ( $self, $format ) = ( shift, shift );
2003 my ( $f, $prefix, $suffix, $separator, $column ) =
2004 _condensed_generator_defaults($format);
2006 if ($format eq 'latex') {
2007 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
2008 $suffix = "\\\\\n\\hline";
2011 sub { my ($d,$a,$s,$w) = @_;
2012 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2014 } elsif ( $format eq 'html' ) {
2015 $prefix = '<th></th>';
2019 sub { my ($d,$a,$s,$w) = @_;
2020 return qq!<th align="$html_align{$a}">$d</th>!;
2028 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2030 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
2033 $prefix. join($separator, @result). $suffix;
2038 sub _condensed_description_generator {
2039 my ( $self, $format ) = ( shift, shift );
2041 my ( $f, $prefix, $suffix, $separator, $column ) =
2042 _condensed_generator_defaults($format);
2044 my $money_char = '$';
2045 if ($format eq 'latex') {
2046 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
2048 $separator = " & \n";
2050 sub { my ($d,$a,$s,$w) = @_;
2051 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
2053 $money_char = '\\dollar';
2054 }elsif ( $format eq 'html' ) {
2055 $prefix = '"><td align="center"></td>';
2059 sub { my ($d,$a,$s,$w) = @_;
2060 return qq!<td align="$html_align{$a}">$d</td>!;
2062 #$money_char = $conf->config('money_char') || '$';
2063 $money_char = ''; # this is madness
2071 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2073 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
2075 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
2076 map { $f->{$_}->[$i] } qw(align span width)
2080 $prefix. join( $separator, @result ). $suffix;
2085 sub _condensed_total_generator {
2086 my ( $self, $format ) = ( shift, shift );
2088 my ( $f, $prefix, $suffix, $separator, $column ) =
2089 _condensed_generator_defaults($format);
2092 if ($format eq 'latex') {
2095 $separator = " & \n";
2097 sub { my ($d,$a,$s,$w) = @_;
2098 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2100 }elsif ( $format eq 'html' ) {
2104 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2106 sub { my ($d,$a,$s,$w) = @_;
2107 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2116 # my $r = &{$f->{fields}->[$i]}(@args);
2117 # $r .= ' Total' unless $i;
2119 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2121 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2122 map { $f->{$_}->[$i] } qw(align span width)
2126 $prefix. join( $separator, @result ). $suffix;
2131 =item total_line_generator FORMAT
2133 Returns a coderef used for generation of invoice total line items for this
2134 usage_class. FORMAT is either html or latex
2138 # should not be used: will have issues with hash element names (description vs
2139 # total_item and amount vs total_amount -- another array of functions?
2141 sub _condensed_total_line_generator {
2142 my ( $self, $format ) = ( shift, shift );
2144 my ( $f, $prefix, $suffix, $separator, $column ) =
2145 _condensed_generator_defaults($format);
2148 if ($format eq 'latex') {
2151 $separator = " & \n";
2153 sub { my ($d,$a,$s,$w) = @_;
2154 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2156 }elsif ( $format eq 'html' ) {
2160 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2162 sub { my ($d,$a,$s,$w) = @_;
2163 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2172 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2174 &{$column}( &{$f->{fields}->[$i]}(@args),
2175 map { $f->{$_}->[$i] } qw(align span width)
2179 $prefix. join( $separator, @result ). $suffix;
2184 =item _items_pkg [ OPTIONS ]
2186 Return line item hashes for each package item on this invoice. Nearly
2189 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2191 The only OPTIONS accepted is 'section', which may point to a hashref
2192 with a key named 'condensed', which may have a true value. If it
2193 does, this method tries to merge identical items into items with
2194 'quantity' equal to the number of items (not the sum of their
2195 separate quantities, for some reason).
2201 grep { $_->pkgnum } $self->cust_bill_pkg;
2208 warn "$me _items_pkg searching for all package line items\n"
2211 my @cust_bill_pkg = $self->_items_nontax;
2213 warn "$me _items_pkg filtering line items\n"
2215 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2217 if ($options{section} && $options{section}->{condensed}) {
2219 warn "$me _items_pkg condensing section\n"
2223 local $Storable::canonical = 1;
2224 foreach ( @items ) {
2226 delete $item->{ref};
2227 delete $item->{ext_description};
2228 my $key = freeze($item);
2229 $itemshash{$key} ||= 0;
2230 $itemshash{$key} ++; # += $item->{quantity};
2232 @items = sort { $a->{description} cmp $b->{description} }
2233 map { my $i = thaw($_);
2234 $i->{quantity} = $itemshash{$_};
2236 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2242 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2249 return 0 unless $a->itemdesc cmp $b->itemdesc;
2250 return -1 if $b->itemdesc eq 'Tax';
2251 return 1 if $a->itemdesc eq 'Tax';
2252 return -1 if $b->itemdesc eq 'Other surcharges';
2253 return 1 if $a->itemdesc eq 'Other surcharges';
2254 $a->itemdesc cmp $b->itemdesc;
2259 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2260 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2262 if ( $self->conf->exists('always_show_tax') ) {
2263 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2264 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2266 { 'description' => $itemdesc,
2273 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2275 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2276 list of hashrefs describing the line items they generate on the invoice.
2278 OPTIONS may include:
2280 format: the invoice format.
2282 escape_function: the function used to escape strings.
2284 DEPRECATED? (expensive, mostly unused?)
2285 format_function: the function used to format CDRs.
2287 section: a hashref containing 'category' and/or 'locationnum'; if this
2288 is present, only returns line items that belong to that category and/or
2289 location (whichever is defined).
2291 multisection: a flag indicating that this is a multisection invoice,
2292 which does something complicated.
2294 Returns a list of hashrefs, each of which may contain:
2296 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2297 ext_description, which is an arrayref of detail lines to show below
2302 sub _items_cust_bill_pkg {
2304 my $conf = $self->conf;
2305 my $cust_bill_pkgs = shift;
2308 my $format = $opt{format} || '';
2309 my $escape_function = $opt{escape_function} || sub { shift };
2310 my $format_function = $opt{format_function} || '';
2311 my $no_usage = $opt{no_usage} || '';
2312 my $unsquelched = $opt{unsquelched} || ''; #unused
2313 my ($section, $locationnum, $category);
2314 if ( $opt{section} ) {
2315 $category = $opt{section}->{category};
2316 $locationnum = $opt{section}->{locationnum};
2318 my $summary_page = $opt{summary_page} || ''; #unused
2319 my $multisection = defined($category) || defined($locationnum);
2320 my $discount_show_always = 0;
2322 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2324 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2325 # and location labels
2328 my ($s, $r, $u) = ( undef, undef, undef );
2329 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2332 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2333 if ( $_ && !$cust_bill_pkg->hidden ) {
2334 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2335 $_->{amount} =~ s/^\-0\.00$/0.00/;
2336 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2338 if $_->{amount} != 0
2339 || $discount_show_always
2340 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2341 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2347 if ( $locationnum ) {
2348 # this is a location section; skip packages that aren't at this
2350 next if $cust_bill_pkg->pkgnum == 0;
2351 next if $self->cust_pkg_hash->{ $cust_bill_pkg->pkgnum }->locationnum
2355 # Consider display records for this item to determine if it belongs
2356 # in this section. Note that if there are no display records, there
2357 # will be a default pseudo-record that includes all charge types
2358 # and has no section name.
2359 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2360 ? $cust_bill_pkg->cust_bill_pkg_display
2361 : ( $cust_bill_pkg );
2363 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2364 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2367 if ( defined($category) ) {
2368 # then this is a package category section; process all display records
2369 # that belong to this section.
2370 @cust_bill_pkg_display = grep { $_->section eq $category }
2371 @cust_bill_pkg_display;
2373 # otherwise, process all display records that aren't usage summaries
2374 # (I don't think there should be usage summaries if you aren't using
2375 # category sections, but this is the historical behavior)
2376 @cust_bill_pkg_display = grep { !$_->summary }
2377 @cust_bill_pkg_display;
2379 foreach my $display (@cust_bill_pkg_display) {
2381 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2382 $display->billpkgdisplaynum. "\n"
2385 my $type = $display->type;
2387 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2388 $desc = substr($desc, 0, $maxlength). '...'
2389 if $format eq 'latex' && length($desc) > $maxlength;
2391 my %details_opt = ( 'format' => $format,
2392 'escape_function' => $escape_function,
2393 'format_function' => $format_function,
2394 'no_usage' => $opt{'no_usage'},
2397 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2399 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2402 if ( $cust_bill_pkg->setup != 0 ) {
2403 my $description = $desc;
2404 $description .= ' Setup'
2405 if $cust_bill_pkg->recur != 0
2406 || $discount_show_always
2407 || $cust_bill_pkg->recur_show_zero;
2409 'description' => $description,
2410 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2413 if ( $cust_bill_pkg->recur != 0 ) {
2415 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2416 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2420 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2422 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2425 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2427 # which pkgpart to show for display purposes?
2428 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2430 # start/end dates for invoice formats that do nonstandard
2432 my %item_dates = ();
2433 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2434 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2436 if ( (!$type || $type eq 'S')
2437 && ( $cust_bill_pkg->setup != 0
2438 || $cust_bill_pkg->setup_show_zero
2443 warn "$me _items_cust_bill_pkg adding setup\n"
2446 my $description = $desc;
2447 $description .= ' Setup'
2448 if $cust_bill_pkg->recur != 0
2449 || $discount_show_always
2450 || $cust_bill_pkg->recur_show_zero;
2454 unless ( $cust_pkg->part_pkg->hide_svc_detail
2455 || $cust_bill_pkg->hidden )
2458 my @svc_labels = map &{$escape_function}($_),
2459 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2460 push @d, @svc_labels
2461 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2462 $svc_label = $svc_labels[0];
2464 my $lnum = $cust_main ? $cust_main->ship_locationnum
2465 : $self->prospect_main->locationnum;
2466 # show the location label if it's not the customer's default
2467 # location, and we're not grouping items by location already
2468 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2469 my $loc = $cust_pkg->location_label;
2470 $loc = substr($loc, 0, $maxlength). '...'
2471 if $format eq 'latex' && length($loc) > $maxlength;
2472 push @d, &{$escape_function}($loc);
2475 } #unless hiding service details
2477 push @d, $cust_bill_pkg->details(%details_opt)
2478 if $cust_bill_pkg->recur == 0;
2480 if ( $cust_bill_pkg->hidden ) {
2481 $s->{amount} += $cust_bill_pkg->setup;
2482 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2483 push @{ $s->{ext_description} }, @d;
2487 description => $description,
2488 pkgpart => $pkgpart,
2489 pkgnum => $cust_bill_pkg->pkgnum,
2490 amount => $cust_bill_pkg->setup,
2491 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2492 unit_amount => $cust_bill_pkg->unitsetup,
2493 quantity => $cust_bill_pkg->quantity,
2494 ext_description => \@d,
2495 svc_label => ($svc_label || ''),
2501 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2503 $cust_bill_pkg->recur != 0
2504 || $cust_bill_pkg->setup == 0
2505 || $discount_show_always
2506 || $cust_bill_pkg->recur_show_zero
2511 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2514 my $is_summary = $display->summary;
2515 my $description = ($is_summary && $type && $type eq 'U')
2516 ? "Usage charges" : $desc;
2518 my $part_pkg = $cust_pkg->part_pkg;
2520 #pry be a bit more efficient to look some of this conf stuff up
2523 $conf->exists('disable_line_item_date_ranges')
2524 || $part_pkg->option('disable_line_item_date_ranges',1)
2525 || ! $cust_bill_pkg->sdate
2526 || ! $cust_bill_pkg->edate
2529 my $date_style = '';
2530 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2533 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2534 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2537 if ( defined($date_style) && $date_style eq 'month_of' ) {
2538 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2539 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2540 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2543 $desc .= ' ' unless $desc =~ /\s$/;
2544 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2546 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2547 " - ". time2str($date_format, $cust_bill_pkg->edate);
2549 $description .= " ($time_period)";
2553 my @seconds = (); # for display of usage info
2556 #at least until cust_bill_pkg has "past" ranges in addition to
2557 #the "future" sdate/edate ones... see #3032
2558 my @dates = ( $self->_date );
2559 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2560 push @dates, $prev->sdate if $prev;
2561 push @dates, undef if !$prev;
2563 unless ( $part_pkg->hide_svc_detail
2564 || $cust_bill_pkg->itemdesc
2565 || $cust_bill_pkg->hidden
2566 || $is_summary && $type && $type eq 'U'
2570 warn "$me _items_cust_bill_pkg adding service details\n"
2573 my @svc_labels = map &{$escape_function}($_),
2574 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2575 push @d, @svc_labels
2576 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2577 $svc_label = $svc_labels[0];
2579 warn "$me _items_cust_bill_pkg done adding service details\n"
2582 my $lnum = $cust_main ? $cust_main->ship_locationnum
2583 : $self->prospect_main->locationnum;
2584 # show the location label if it's not the customer's default
2585 # location, and we're not grouping items by location already
2586 if ( $cust_pkg->locationnum != $lnum and !defined($locationnum) ) {
2587 my $loc = $cust_pkg->location_label;
2588 $loc = substr($loc, 0, $maxlength). '...'
2589 if $format eq 'latex' && length($loc) > $maxlength;
2590 push @d, &{$escape_function}($loc);
2593 # Display of seconds_since_sqlradacct:
2594 # On the invoice, when processing @detail_items, look for a field
2595 # named 'seconds'. This will contain total seconds for each
2596 # service, in the same order as @ext_description. For services
2597 # that don't support this it will show undef.
2598 if ( $conf->exists('svc_acct-usage_seconds')
2599 and ! $cust_bill_pkg->pkgpart_override ) {
2600 foreach my $cust_svc (
2601 $cust_pkg->h_cust_svc(@dates, 'I')
2604 # eval because not having any part_export_usage exports
2605 # is a fatal error, last_bill/_date because that's how
2606 # sqlradius_hour billing does it
2608 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2610 push @seconds, $sec;
2612 } #if svc_acct-usage_seconds
2616 unless ( $is_summary ) {
2617 warn "$me _items_cust_bill_pkg adding details\n"
2620 #instead of omitting details entirely in this case (unwanted side
2621 # effects), just omit CDRs
2622 $details_opt{'no_usage'} = 1
2623 if $type && $type eq 'R';
2625 push @d, $cust_bill_pkg->details(%details_opt);
2628 warn "$me _items_cust_bill_pkg calculating amount\n"
2633 $amount = $cust_bill_pkg->recur;
2634 } elsif ($type eq 'R') {
2635 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2636 } elsif ($type eq 'U') {
2637 $amount = $cust_bill_pkg->usage;
2641 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2644 if ( !$type || $type eq 'R' ) {
2646 warn "$me _items_cust_bill_pkg adding recur\n"
2649 if ( $cust_bill_pkg->hidden ) {
2650 $r->{amount} += $amount;
2651 $r->{unit_amount} += $unit_amount;
2652 push @{ $r->{ext_description} }, @d;
2655 description => $description,
2656 pkgpart => $pkgpart,
2657 pkgnum => $cust_bill_pkg->pkgnum,
2659 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2660 unit_amount => $unit_amount,
2661 quantity => $cust_bill_pkg->quantity,
2663 ext_description => \@d,
2664 svc_label => ($svc_label || ''),
2666 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2669 } else { # $type eq 'U'
2671 warn "$me _items_cust_bill_pkg adding usage\n"
2674 if ( $cust_bill_pkg->hidden ) {
2675 $u->{amount} += $amount;
2676 $u->{unit_amount} += $unit_amount,
2677 push @{ $u->{ext_description} }, @d;
2680 description => $description,
2681 pkgpart => $pkgpart,
2682 pkgnum => $cust_bill_pkg->pkgnum,
2684 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2685 unit_amount => $unit_amount,
2686 quantity => $cust_bill_pkg->quantity,
2688 ext_description => \@d,
2693 } # recurring or usage with recurring charge
2695 } else { #pkgnum tax or one-shot line item (??)
2697 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2700 if ( $cust_bill_pkg->setup != 0 ) {
2702 'description' => $desc,
2703 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2706 if ( $cust_bill_pkg->recur != 0 ) {
2708 'description' => "$desc (".
2709 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2710 time2str($date_format, $cust_bill_pkg->edate). ')',
2711 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2719 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2720 && $conf->exists('discount-show-always'));
2724 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2726 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2727 $_->{amount} =~ s/^\-0\.00$/0.00/;
2728 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2730 if $_->{amount} != 0
2731 || $discount_show_always
2732 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2733 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2737 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2744 =item _items_discounts_avail
2746 Returns an array of line item hashrefs representing available term discounts
2747 for this invoice. This makes the same assumptions that apply to term
2748 discounts in general: that the package is billed monthly, at a flat rate,
2749 with no usage charges. A prorated first month will be handled, as will
2750 a setup fee if the discount is allowed to apply to setup fees.
2754 sub _items_discounts_avail {
2757 #maybe move this method from cust_bill when quotations support discount_plans
2758 return () unless $self->can('discount_plans');
2759 my %plans = $self->discount_plans;
2761 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2762 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2766 my $plan = $plans{$months};
2768 my $term_total = sprintf('%.2f', $plan->discounted_total);
2769 my $percent = sprintf('%.0f',
2770 100 * (1 - $term_total / $plan->base_total) );
2771 my $permonth = sprintf('%.2f', $term_total / $months);
2772 my $detail = $self->mt('discount on item'). ' '.
2773 join(', ', map { "#$_" } $plan->pkgnums)
2776 # discounts for non-integer months don't work anyway
2777 $months = sprintf("%d", $months);
2780 description => $self->mt('Save [_1]% by paying for [_2] months',
2782 amount => $self->mt('[_1] ([_2] per month)',
2783 $term_total, $money_char.$permonth),
2784 ext_description => ($detail || ''),
2787 sort { $b <=> $a } keys %plans;