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'}
454 : time2str($date_format, $self->_date)
456 'today' => time2str($date_format_long, $today),
457 'terms' => $self->terms,
458 'template' => $template, #params{'template'},
459 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
460 'current_charges' => sprintf("%.2f", $self->charged),
461 'duedate' => $self->due_date2str($rdate_format), #date_format?
464 'custnum' => $cust_main->display_custnum,
465 'prospectnum' => $cust_main->prospectnum,
466 'agent_custid' => &$escape_function($cust_main->agent_custid),
467 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
468 payname company address1 address2 city state zip fax
472 'ship_enable' => $conf->exists('invoice-ship_address'),
473 'unitprices' => $conf->exists('invoice-unitprice'),
474 'smallernotes' => $conf->exists('invoice-smallernotes'),
475 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
476 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
478 #layout info -- would be fancy to calc some of this and bury the template
480 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
481 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
482 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
483 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
484 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
485 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
486 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
487 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
488 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
489 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
491 # better hang on to conf_dir for a while (for old templates)
492 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
494 #these are only used when doing paged plaintext
501 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
502 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
503 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
504 # eval to avoid death for unimplemented languages
505 my $dh = eval { Date::Language->new($info{'name'}) } ||
506 Date::Language->new(); # fall back to English
507 # prototype here to silence warnings
508 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
509 # eventually use this date handle everywhere in here, too
511 my $min_sdate = 999999999999;
513 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
514 next unless $cust_bill_pkg->pkgnum > 0;
515 $min_sdate = $cust_bill_pkg->sdate
516 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
517 $max_edate = $cust_bill_pkg->edate
518 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
521 $invoice_data{'bill_period'} = '';
522 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
523 . " to " . time2str('%e %h', $max_edate)
524 if ($max_edate != 0 && $min_sdate != 999999999999);
526 $invoice_data{finance_section} = '';
527 if ( $conf->config('finance_pkgclass') ) {
529 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
530 $invoice_data{finance_section} = $pkg_class->categoryname;
532 $invoice_data{finance_amount} = '0.00';
533 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
535 my $countrydefault = $conf->config('countrydefault') || 'US';
536 foreach ( qw( address1 address2 city state zip country fax) ){
537 my $method = 'ship_'.$_;
538 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
540 foreach ( qw( contact company ) ) { #compatibility
541 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
543 $invoice_data{'ship_country'} = ''
544 if ( $invoice_data{'ship_country'} eq $countrydefault );
546 $invoice_data{'cid'} = $params{'cid'}
549 if ( $cust_main->country eq $countrydefault ) {
550 $invoice_data{'country'} = '';
552 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
556 $invoice_data{'address'} = \@address;
559 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
560 ? " (P.O. #". $cust_main->payinfo. ")"
564 push @address, $cust_main->company
565 if $cust_main->company;
566 push @address, $cust_main->address1;
567 push @address, $cust_main->address2
568 if $cust_main->address2;
570 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
571 push @address, $invoice_data{'country'}
572 if $invoice_data{'country'};
574 while (scalar(@address) < 5);
576 $invoice_data{'logo_file'} = $params{'logo_file'}
577 if $params{'logo_file'};
578 $invoice_data{'barcode_file'} = $params{'barcode_file'}
579 if $params{'barcode_file'};
580 $invoice_data{'barcode_img'} = $params{'barcode_img'}
581 if $params{'barcode_img'};
582 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
583 if $params{'barcode_cid'};
585 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
586 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
587 #my $balance_due = $self->owed + $pr_total - $cr_total;
588 my $balance_due = $self->owed + $pr_total;
590 #these are used on the summary page only
592 # the customer's current balance as shown on the invoice before this one
593 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
595 # the change in balance from that invoice to this one
596 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
598 # the sum of amount owed on all previous invoices
599 # ($pr_total is used elsewhere but not as $previous_balance)
600 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
602 # the sum of amount owed on all invoices
603 # (this is used in the summary & on the payment coupon)
604 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
606 # info from customer's last invoice before this one, for some
608 $invoice_data{'last_bill'} = {};
610 # returns the last unpaid bill, not the last bill
611 #my $last_bill = $pr_cust_bill[-1];
613 if ( $self->custnum && $self->invnum ) {
615 # THIS returns the customer's last bill before this one
616 my $last_bill = qsearchs({
617 'table' => 'cust_bill',
618 'hashref' => { 'custnum' => $self->custnum,
619 'invnum' => { op => '<', value => $self->invnum },
621 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
624 $invoice_data{'last_bill'} = {
625 '_date' => $last_bill->_date, #unformatted
626 # all we need for now
628 my (@payments, @credits);
629 # for formats that itemize previous payments
630 foreach my $cust_pay ( qsearch('cust_pay', {
631 'custnum' => $self->custnum,
632 '_date' => { op => '>=',
633 value => $last_bill->_date }
636 next if $cust_pay->_date > $self->_date;
638 '_date' => $cust_pay->_date,
639 'date' => time2str($date_format, $cust_pay->_date),
640 'payinfo' => $cust_pay->payby_payinfo_pretty,
641 'amount' => sprintf('%.2f', $cust_pay->paid),
643 # not concerned about applications
645 foreach my $cust_credit ( qsearch('cust_credit', {
646 'custnum' => $self->custnum,
647 '_date' => { op => '>=',
648 value => $last_bill->_date }
651 next if $cust_credit->_date > $self->_date;
653 '_date' => $cust_credit->_date,
654 'date' => time2str($date_format, $cust_credit->_date),
655 'creditreason'=> $cust_credit->reason,
656 'amount' => sprintf('%.2f', $cust_credit->amount),
659 $invoice_data{'previous_payments'} = \@payments;
660 $invoice_data{'previous_credits'} = \@credits;
665 my $summarypage = '';
666 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
669 $invoice_data{'summarypage'} = $summarypage;
671 warn "$me substituting variables in notes, footer, smallfooter\n"
674 my $tc = $self->template_conf;
675 my @include = ( [ $tc, 'notes' ],
676 [ 'invoice_', 'footer' ],
677 [ 'invoice_', 'smallfooter', ],
679 push @include, [ $tc, 'coupon', ]
680 unless $params{'no_coupon'};
682 foreach my $i (@include) {
684 my($base, $include) = @$i;
686 my $inc_file = $conf->key_orbase("$base$format$include", $template);
689 if ( $conf->exists($inc_file, $agentnum)
690 && length( $conf->config($inc_file, $agentnum) ) ) {
692 @inc_src = $conf->config($inc_file, $agentnum);
696 $inc_file = $conf->key_orbase("${base}latex$include", $template);
698 my $convert_map = $convert_maps{$format}{$include};
700 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
701 s/--\@\]/$delimiters{$format}[1]/g;
704 &$convert_map( $conf->config($inc_file, $agentnum) );
708 my $inc_tt = new Text::Template (
710 SOURCE => [ map "$_\n", @inc_src ],
711 DELIMITERS => $delimiters{$format},
712 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
714 unless ( $inc_tt->compile() ) {
715 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
716 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
720 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
722 $invoice_data{$include} =~ s/\n+$//
723 if ($format eq 'latex');
726 # let invoices use either of these as needed
727 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
728 ? $cust_main->payinfo : '';
729 $invoice_data{'po_line'} =
730 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
731 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
734 my %money_chars = ( 'latex' => '',
735 'html' => $conf->config('money_char') || '$',
738 my $money_char = $money_chars{$format};
740 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
741 'html' => $conf->config('money_char') || '$',
744 my $other_money_char = $other_money_chars{$format};
745 $invoice_data{'dollar'} = $other_money_char;
747 my %minus_signs = ( 'latex' => '$-$',
749 'template' => '- ' );
750 my $minus = $minus_signs{$format};
752 my @detail_items = ();
753 my @total_items = ();
757 $invoice_data{'detail_items'} = \@detail_items;
758 $invoice_data{'total_items'} = \@total_items;
759 $invoice_data{'buf'} = \@buf;
760 $invoice_data{'sections'} = \@sections;
762 warn "$me generating sections\n"
765 # Previous Charges section
766 # subtotal is the first return value from $self->previous
767 my $previous_section = { 'description' => $self->mt('Previous Charges'),
768 'subtotal' => $other_money_char.
769 sprintf('%.2f', $pr_total),
770 'summarized' => '', #why? $summarypage ? 'Y' : '',
772 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
773 join(' / ', map { $cust_main->balance_date_range(@$_) }
774 $self->_prior_month30s
776 if $conf->exists('invoice_include_aging');
779 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
780 'subtotal' => $taxtotal, # adjusted below
782 my $tax_weight = _pkg_category($tax_section->{description})
783 ? _pkg_category($tax_section->{description})->weight
785 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
786 $tax_section->{'sort_weight'} = $tax_weight;
790 my $adjust_section = {
791 'description' => $self->mt('Credits, Payments, and Adjustments'),
792 'adjust_section' => 1,
793 'subtotal' => 0, # adjusted below
795 my $adjust_weight = _pkg_category($adjust_section->{description})
796 ? _pkg_category($adjust_section->{description})->weight
798 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
799 $adjust_section->{'sort_weight'} = $adjust_weight;
801 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
802 my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum);
803 $invoice_data{'multisection'} = $multisection;
804 my $late_sections = [];
805 my $extra_sections = [];
806 my $extra_lines = ();
808 my $default_section = { 'description' => '',
813 if ( $multisection ) {
814 ($extra_sections, $extra_lines) =
815 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
816 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
817 && $self->can('_items_extra_usage_sections');
819 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
821 push @detail_items, @$extra_lines if $extra_lines;
823 $self->_items_sections( $late_sections, # this could stand a refactor
825 $escape_function_nonbsp,
829 if ( $conf->exists('svc_phone_sections')
830 && $self->can('_items_svc_phone_sections')
833 my ($phone_sections, $phone_lines) =
834 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
835 push @{$late_sections}, @$phone_sections;
836 push @detail_items, @$phone_lines;
838 if ( $conf->exists('voip-cust_accountcode_cdr')
839 && $cust_main->accountcode_cdr
840 && $self->can('_items_accountcode_cdr')
843 my ($accountcode_section, $accountcode_lines) =
844 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
845 if ( scalar(@$accountcode_lines) ) {
846 push @{$late_sections}, $accountcode_section;
847 push @detail_items, @$accountcode_lines;
850 } else {# not multisection
851 # make a default section
852 push @sections, $default_section;
853 # and calculate the finance charge total, since it won't get done otherwise.
854 # XXX possibly other totals?
855 # XXX possibly finance_pkgclass should not be used in this manner?
856 if ( $conf->exists('finance_pkgclass') ) {
858 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
859 if ( grep { $_->section eq $invoice_data{finance_section} }
860 $cust_bill_pkg->cust_bill_pkg_display ) {
861 # I think these are always setup fees, but just to be sure...
862 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
865 $invoice_data{finance_amount} =
866 sprintf('%.2f', sum( @finance_charges ) || 0);
870 # previous invoice balances in the Previous Charges section if there
871 # is one, otherwise in the main detail section
872 if ( $self->can('_items_previous') &&
873 $self->enable_previous &&
874 ! $conf->exists('previous_balance-summary_only') ) {
876 warn "$me adding previous balances\n"
879 foreach my $line_item ( $self->_items_previous ) {
882 ext_description => [],
884 $detail->{'ref'} = $line_item->{'pkgnum'};
885 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
886 $detail->{'quantity'} = 1;
887 $detail->{'section'} = $multisection ? $previous_section
889 $detail->{'description'} = &$escape_function($line_item->{'description'});
890 if ( exists $line_item->{'ext_description'} ) {
891 @{$detail->{'ext_description'}} = map {
892 &$escape_function($_);
893 } @{$line_item->{'ext_description'}};
895 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
896 $line_item->{'amount'};
897 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
899 push @detail_items, $detail;
900 push @buf, [ $detail->{'description'},
901 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
907 if ( @pr_cust_bill && $self->enable_previous ) {
908 push @buf, ['','-----------'];
909 push @buf, [ $self->mt('Total Previous Balance'),
910 $money_char. sprintf("%10.2f", $pr_total) ];
914 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
915 warn "$me adding DID summary\n"
918 my ($didsummary,$minutes) = $self->_did_summary;
919 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
921 { 'description' => $didsummary_desc,
922 'ext_description' => [ $didsummary, $minutes ],
926 foreach my $section (@sections, @$late_sections) {
928 warn "$me adding section \n". Dumper($section)
931 # begin some normalization
932 $section->{'subtotal'} = $section->{'amount'}
934 && !exists($section->{subtotal})
935 && exists($section->{amount});
937 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
938 if ( $invoice_data{finance_section} &&
939 $section->{'description'} eq $invoice_data{finance_section} );
941 $section->{'subtotal'} = $other_money_char.
942 sprintf('%.2f', $section->{'subtotal'})
945 # continue some normalization
946 $section->{'amount'} = $section->{'subtotal'}
950 if ( $section->{'description'} ) {
951 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
956 warn "$me setting options\n"
960 $options{'section'} = $section if $multisection;
961 $options{'format'} = $format;
962 $options{'escape_function'} = $escape_function;
963 $options{'no_usage'} = 1 unless $unsquelched;
964 $options{'unsquelched'} = $unsquelched;
965 $options{'summary_page'} = $summarypage;
966 $options{'skip_usage'} =
967 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
968 $options{'multisection'} = $multisection;
970 warn "$me searching for line items\n"
973 foreach my $line_item ( $self->_items_pkg(%options) ) {
975 warn "$me adding line item $line_item\n"
979 ext_description => [],
981 $detail->{'ref'} = $line_item->{'pkgnum'};
982 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
983 $detail->{'quantity'} = $line_item->{'quantity'};
984 $detail->{'section'} = $section;
985 $detail->{'description'} = &$escape_function($line_item->{'description'});
986 if ( exists $line_item->{'ext_description'} ) {
987 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
989 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
990 $line_item->{'amount'};
991 if ( exists $line_item->{'unit_amount'} ) {
992 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
993 $line_item->{'unit_amount'};
995 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
997 $detail->{'sdate'} = $line_item->{'sdate'};
998 $detail->{'edate'} = $line_item->{'edate'};
999 $detail->{'seconds'} = $line_item->{'seconds'};
1000 $detail->{'svc_label'} = $line_item->{'svc_label'};
1002 push @detail_items, $detail;
1003 push @buf, ( [ $detail->{'description'},
1004 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1006 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1010 if ( $section->{'description'} ) {
1011 push @buf, ( ['','-----------'],
1012 [ $section->{'description'}. ' sub-total',
1013 $section->{'subtotal'} # already formatted this
1022 $invoice_data{current_less_finance} =
1023 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1025 # create a major section for previous balance if we have major sections,
1026 # or if previous_section is in summary form
1027 if ( ( $multisection && $self->enable_previous )
1028 || $conf->exists('previous_balance-summary_only') )
1030 unshift @sections, $previous_section if $pr_total;
1033 warn "$me adding taxes\n"
1036 my @items_tax = $self->_items_tax;
1037 foreach my $tax ( @items_tax ) {
1039 $taxtotal += $tax->{'amount'};
1041 my $description = &$escape_function( $tax->{'description'} );
1042 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1044 if ( $multisection ) {
1046 my $money = $old_latex ? '' : $money_char;
1047 push @detail_items, {
1048 ext_description => [],
1051 description => $description,
1052 amount => $money. $amount,
1054 section => $tax_section,
1059 push @total_items, {
1060 'total_item' => $description,
1061 'total_amount' => $other_money_char. $amount,
1066 push @buf,[ $description,
1067 $money_char. $amount,
1074 $total->{'total_item'} = $self->mt('Sub-total');
1075 $total->{'total_amount'} =
1076 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1078 if ( $multisection ) {
1079 $tax_section->{'subtotal'} = $other_money_char.
1080 sprintf('%.2f', $taxtotal);
1081 $tax_section->{'pretotal'} = 'New charges sub-total '.
1082 $total->{'total_amount'};
1083 push @sections, $tax_section if $taxtotal;
1085 unshift @total_items, $total;
1088 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1090 push @buf,['','-----------'];
1091 push @buf,[$self->mt(
1092 (!$self->enable_previous)
1094 : 'Total New Charges'
1096 $money_char. sprintf("%10.2f",$self->charged) ];
1104 my %embolden_functions = (
1105 'latex' => sub { return '\textbf{'. shift(). '}' },
1106 'html' => sub { return '<b>'. shift(). '</b>' },
1107 'template' => sub { shift },
1109 my $embolden_function = $embolden_functions{$format};
1111 if ( $self->can('_items_total') ) { # quotations
1113 $self->_items_total(\@total_items);
1115 foreach ( @total_items ) {
1116 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1117 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1118 $_->{'total_amount'}
1122 } else { #normal invoice case
1124 # calculate total, possibly including total owed on previous
1128 $item = $conf->config('previous_balance-exclude_from_total')
1129 || 'Total New Charges'
1130 if $conf->exists('previous_balance-exclude_from_total');
1131 my $amount = $self->charged;
1132 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1133 $amount += $pr_total;
1136 $total->{'total_item'} = &$embolden_function($self->mt($item));
1137 $total->{'total_amount'} =
1138 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1139 if ( $multisection ) {
1140 if ( $adjust_section->{'sort_weight'} ) {
1141 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1142 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1144 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1145 $other_money_char. sprintf('%.2f', $self->charged );
1148 push @total_items, $total;
1150 push @buf,['','-----------'];
1153 sprintf( '%10.2f', $amount )
1157 # if we're showing previous invoices, also show previous
1158 # credits and payments
1159 if ( $self->enable_previous
1160 and $self->can('_items_credits')
1161 and $self->can('_items_payments') )
1163 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1166 my $credittotal = 0;
1167 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1170 $total->{'total_item'} = &$escape_function($credit->{'description'});
1171 $credittotal += $credit->{'amount'};
1172 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1173 $adjusttotal += $credit->{'amount'};
1174 if ( $multisection ) {
1175 my $money = $old_latex ? '' : $money_char;
1176 push @detail_items, {
1177 ext_description => [],
1180 description => &$escape_function($credit->{'description'}),
1181 amount => $money. $credit->{'amount'},
1183 section => $adjust_section,
1186 push @total_items, $total;
1190 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1193 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1194 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1198 my $paymenttotal = 0;
1199 foreach my $payment ( $self->_items_payments ) {
1201 $total->{'total_item'} = &$escape_function($payment->{'description'});
1202 $paymenttotal += $payment->{'amount'};
1203 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1204 $adjusttotal += $payment->{'amount'};
1205 if ( $multisection ) {
1206 my $money = $old_latex ? '' : $money_char;
1207 push @detail_items, {
1208 ext_description => [],
1211 description => &$escape_function($payment->{'description'}),
1212 amount => $money. $payment->{'amount'},
1214 section => $adjust_section,
1217 push @total_items, $total;
1219 push @buf, [ $payment->{'description'},
1220 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1223 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1225 if ( $multisection ) {
1226 $adjust_section->{'subtotal'} = $other_money_char.
1227 sprintf('%.2f', $adjusttotal);
1228 push @sections, $adjust_section
1229 unless $adjust_section->{sort_weight};
1232 # create Balance Due message
1235 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1236 $total->{'total_amount'} =
1237 &$embolden_function(
1238 $other_money_char. sprintf('%.2f', #why? $summarypage
1239 # ? $self->charged +
1240 # $self->billing_balance
1242 $self->owed + $pr_total
1245 if ( $multisection && !$adjust_section->{sort_weight} ) {
1246 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1247 $total->{'total_amount'};
1249 push @total_items, $total;
1251 push @buf,['','-----------'];
1252 push @buf,[$self->balance_due_msg, $money_char.
1253 sprintf("%10.2f", $balance_due ) ];
1256 if ( $conf->exists('previous_balance-show_credit')
1257 and $cust_main->balance < 0 ) {
1258 my $credit_total = {
1259 'total_item' => &$embolden_function($self->credit_balance_msg),
1260 'total_amount' => &$embolden_function(
1261 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1264 if ( $multisection ) {
1265 $adjust_section->{'posttotal'} .= $newline_token .
1266 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1269 push @total_items, $credit_total;
1271 push @buf,['','-----------'];
1272 push @buf,[$self->credit_balance_msg, $money_char.
1273 sprintf("%10.2f", -$cust_main->balance ) ];
1277 } #end of default total adding ! can('_items_total')
1279 if ( $multisection ) {
1280 if ( $conf->exists('svc_phone_sections')
1281 && $self->can('_items_svc_phone_sections')
1285 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1286 $total->{'total_amount'} =
1287 &$embolden_function(
1288 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1290 my $last_section = pop @sections;
1291 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1292 $total->{'total_amount'};
1293 push @sections, $last_section;
1295 push @sections, @$late_sections
1299 # make a discounts-available section, even without multisection
1300 if ( $conf->exists('discount-show_available')
1301 and my @discounts_avail = $self->_items_discounts_avail ) {
1302 my $discount_section = {
1303 'description' => $self->mt('Discounts Available'),
1308 push @sections, $discount_section;
1309 push @detail_items, map { +{
1310 'ref' => '', #should this be something else?
1311 'section' => $discount_section,
1312 'description' => &$escape_function( $_->{description} ),
1313 'amount' => $money_char . &$escape_function( $_->{amount} ),
1314 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1315 } } @discounts_avail;
1318 # debugging hook: call this with 'diag' => 1 to just get a hash of
1319 # the invoice variables
1320 return \%invoice_data if ( $params{'diag'} );
1322 # All sections and items are built; now fill in templates.
1323 my @includelist = ();
1324 push @includelist, 'summary' if $summarypage;
1325 foreach my $include ( @includelist ) {
1327 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1330 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1332 @inc_src = $conf->config($inc_file, $agentnum);
1336 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1338 my $convert_map = $convert_maps{$format}{$include};
1340 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1341 s/--\@\]/$delimiters{$format}[1]/g;
1344 &$convert_map( $conf->config($inc_file, $agentnum) );
1348 my $inc_tt = new Text::Template (
1350 SOURCE => [ map "$_\n", @inc_src ],
1351 DELIMITERS => $delimiters{$format},
1352 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1354 unless ( $inc_tt->compile() ) {
1355 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1356 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1360 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1362 $invoice_data{$include} =~ s/\n+$//
1363 if ($format eq 'latex');
1368 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1369 /invoice_lines\((\d*)\)/;
1370 $invoice_lines += $1 || scalar(@buf);
1373 die "no invoice_lines() functions in template?"
1374 if ( $format eq 'template' && !$wasfunc );
1376 if ($format eq 'template') {
1378 if ( $invoice_lines ) {
1379 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1380 $invoice_data{'total_pages'}++
1381 if scalar(@buf) % $invoice_lines;
1384 #setup subroutine for the template
1385 $invoice_data{invoice_lines} = sub {
1386 my $lines = shift || scalar(@buf);
1398 push @collect, split("\n",
1399 $text_template->fill_in( HASH => \%invoice_data )
1401 $invoice_data{'page'}++;
1403 map "$_\n", @collect;
1405 } else { # this is where we actually create the invoice
1407 warn "filling in template for invoice ". $self->invnum. "\n"
1409 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1412 $text_template->fill_in(HASH => \%invoice_data);
1416 sub notice_name { '('.shift->table.')'; }
1418 sub template_conf { 'invoice_'; }
1420 # helper routine for generating date ranges
1421 sub _prior_month30s {
1424 [ 1, 2592000 ], # 0-30 days ago
1425 [ 2592000, 5184000 ], # 30-60 days ago
1426 [ 5184000, 7776000 ], # 60-90 days ago
1427 [ 7776000, 0 ], # 90+ days ago
1430 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1431 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1436 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1438 Returns an postscript invoice, as a scalar.
1440 Options can be passed as a hashref (recommended) or as a list of time, template
1441 and then any key/value pairs for any other options.
1443 I<time> an optional value used to control the printing of overdue messages. The
1444 default is now. It isn't the date of the invoice; that's the `_date' field.
1445 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1446 L<Time::Local> and L<Date::Parse> for conversion functions.
1448 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1455 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1456 my $ps = generate_ps($file);
1458 unlink($barcodefile) if $barcodefile;
1463 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1465 Returns an PDF invoice, as a scalar.
1467 Options can be passed as a hashref (recommended) or as a list of time, template
1468 and then any key/value pairs for any other options.
1470 I<time> an optional value used to control the printing of overdue messages. The
1471 default is now. It isn't the date of the invoice; that's the `_date' field.
1472 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1473 L<Time::Local> and L<Date::Parse> for conversion functions.
1475 I<template>, if specified, is the name of a suffix for alternate invoices.
1477 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1484 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1485 my $pdf = generate_pdf($file);
1487 unlink($barcodefile) if $barcodefile;
1492 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1494 Returns an HTML invoice, as a scalar.
1496 I<time> an optional value used to control the printing of overdue messages. The
1497 default is now. It isn't the date of the invoice; that's the `_date' field.
1498 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1499 L<Time::Local> and L<Date::Parse> for conversion functions.
1501 I<template>, if specified, is the name of a suffix for alternate invoices.
1503 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1505 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1506 when emailing the invoice as part of a multipart/related MIME email.
1514 %params = %{ shift() };
1516 $params{'time'} = shift;
1517 $params{'template'} = shift;
1518 $params{'cid'} = shift;
1521 $params{'format'} = 'html';
1523 $self->print_generic( %params );
1526 # quick subroutine for print_latex
1528 # There are ten characters that LaTeX treats as special characters, which
1529 # means that they do not simply typeset themselves:
1530 # # $ % & ~ _ ^ \ { }
1532 # TeX ignores blanks following an escaped character; if you want a blank (as
1533 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1537 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1538 $value =~ s/([<>])/\$$1\$/g;
1544 encode_entities($value);
1548 sub _html_escape_nbsp {
1549 my $value = _html_escape(shift);
1550 $value =~ s/ +/ /g;
1554 #utility methods for print_*
1556 sub _translate_old_latex_format {
1557 warn "_translate_old_latex_format called\n"
1564 if ( $line =~ /^%%Detail\s*$/ ) {
1566 push @template, q![@--!,
1567 q! foreach my $_tr_line (@detail_items) {!,
1568 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1569 q! $_tr_line->{'description'} .= !,
1570 q! "\\tabularnewline\n~~".!,
1571 q! join( "\\tabularnewline\n~~",!,
1572 q! @{$_tr_line->{'ext_description'}}!,
1576 while ( ( my $line_item_line = shift )
1577 !~ /^%%EndDetail\s*$/ ) {
1578 $line_item_line =~ s/'/\\'/g; # nice LTS
1579 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1580 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1581 push @template, " \$OUT .= '$line_item_line';";
1584 push @template, '}',
1587 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1589 push @template, '[@--',
1590 ' foreach my $_tr_line (@total_items) {';
1592 while ( ( my $total_item_line = shift )
1593 !~ /^%%EndTotalDetails\s*$/ ) {
1594 $total_item_line =~ s/'/\\'/g; # nice LTS
1595 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1596 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1597 push @template, " \$OUT .= '$total_item_line';";
1600 push @template, '}',
1604 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1605 push @template, $line;
1611 warn "$_\n" foreach @template;
1619 my $conf = $self->conf;
1621 #check for an invoice-specific override
1622 return $self->invoice_terms if $self->invoice_terms;
1624 #check for a customer- specific override
1625 my $cust_main = $self->cust_main;
1626 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1628 #use configured default
1629 $conf->config('invoice_default_terms') || '';
1635 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1636 $duedate = $self->_date() + ( $1 * 86400 );
1643 $self->due_date ? time2str(shift, $self->due_date) : '';
1646 sub balance_due_msg {
1648 my $msg = $self->mt('Balance Due');
1649 return $msg unless $self->terms;
1650 if ( $self->due_date ) {
1651 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1652 $self->due_date2str($date_format);
1653 } elsif ( $self->terms ) {
1654 $msg .= ' - '. $self->terms;
1659 sub balance_due_date {
1661 my $conf = $self->conf;
1663 if ( $conf->exists('invoice_default_terms')
1664 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1665 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1670 sub credit_balance_msg {
1672 $self->mt('Credit Balance Remaining')
1677 Returns a string with the date, for example: "3/20/2008"
1683 time2str($date_format, $self->_date);
1686 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1688 Generate section information for all items appearing on this invoice.
1689 This will only be called for multi-section invoices.
1691 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1692 related display records (L<FS::cust_bill_pkg_display>) and organize
1693 them into two groups ("early" and "late" according to whether they come
1694 before or after the total), then into sections. A subtotal is calculated
1697 Section descriptions are returned in sort weight order. Each consists
1698 of a hash containing:
1700 description: the package category name, escaped
1701 subtotal: the total charges in that section
1702 tax_section: a flag indicating that the section contains only tax charges
1703 summarized: same as tax_section, for some reason
1704 sort_weight: the package category's sort weight
1706 If 'condense' is set on the display record, it also contains everything
1707 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1708 coderefs to generate parts of the invoice. This is not advised.
1712 LATE: an arrayref to push the "late" section hashes onto. The "early"
1713 group is simply returned from the method.
1715 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1716 Turning this on has the following effects:
1717 - Ignores display items with the 'summary' flag.
1718 - Combines all items into the "early" group.
1719 - Creates sections for all non-disabled package categories, even if they
1720 have no charges on this invoice, as well as a section with no name.
1722 ESCAPE: an escape function to use for section titles.
1724 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1725 sorted list. If there are any of these, section subtotals exclude
1728 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1729 passed through to C<_condense_section()>.
1733 use vars qw(%pkg_category_cache);
1734 sub _items_sections {
1737 my $summarypage = shift;
1739 my $extra_sections = shift;
1743 my %late_subtotal = ();
1746 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1749 my $usage = $cust_bill_pkg->usage;
1751 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1752 next if ( $display->summary && $summarypage );
1754 my $section = $display->section;
1755 my $type = $display->type;
1757 $not_tax{$section} = 1
1758 unless $cust_bill_pkg->pkgnum == 0;
1760 # there's actually a very important piece of logic buried in here:
1761 # incrementing $late_subtotal{$section} CREATES
1762 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1763 # to define the list of late sections, and likewise keys(%subtotal).
1764 # When _items_cust_bill_pkg is called to generate line items for
1765 # real, it will be called with 'section' => $section for each
1767 if ( $display->post_total && !$summarypage ) {
1768 if (! $type || $type eq 'S') {
1769 $late_subtotal{$section} += $cust_bill_pkg->setup
1770 if $cust_bill_pkg->setup != 0
1771 || $cust_bill_pkg->setup_show_zero;
1775 $late_subtotal{$section} += $cust_bill_pkg->recur
1776 if $cust_bill_pkg->recur != 0
1777 || $cust_bill_pkg->recur_show_zero;
1780 if ($type && $type eq 'R') {
1781 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1782 if $cust_bill_pkg->recur != 0
1783 || $cust_bill_pkg->recur_show_zero;
1786 if ($type && $type eq 'U') {
1787 $late_subtotal{$section} += $usage
1788 unless scalar(@$extra_sections);
1793 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1795 if (! $type || $type eq 'S') {
1796 $subtotal{$section} += $cust_bill_pkg->setup
1797 if $cust_bill_pkg->setup != 0
1798 || $cust_bill_pkg->setup_show_zero;
1802 $subtotal{$section} += $cust_bill_pkg->recur
1803 if $cust_bill_pkg->recur != 0
1804 || $cust_bill_pkg->recur_show_zero;
1807 if ($type && $type eq 'R') {
1808 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1809 if $cust_bill_pkg->recur != 0
1810 || $cust_bill_pkg->recur_show_zero;
1813 if ($type && $type eq 'U') {
1814 $subtotal{$section} += $usage
1815 unless scalar(@$extra_sections);
1824 %pkg_category_cache = ();
1826 push @$late, map { { 'description' => &{$escape}($_),
1827 'subtotal' => $late_subtotal{$_},
1829 'sort_weight' => ( _pkg_category($_)
1830 ? _pkg_category($_)->weight
1833 ((_pkg_category($_) && _pkg_category($_)->condense)
1834 ? $self->_condense_section($format)
1838 sort _sectionsort keys %late_subtotal;
1841 if ( $summarypage ) {
1842 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1843 map { $_->categoryname } qsearch('pkg_category', {});
1844 push @sections, '' if exists($subtotal{''});
1846 @sections = keys %subtotal;
1849 my @early = map { { 'description' => &{$escape}($_),
1850 'subtotal' => $subtotal{$_},
1851 'summarized' => $not_tax{$_} ? '' : 'Y',
1852 'tax_section' => $not_tax{$_} ? '' : 'Y',
1853 'sort_weight' => ( _pkg_category($_)
1854 ? _pkg_category($_)->weight
1857 ((_pkg_category($_) && _pkg_category($_)->condense)
1858 ? $self->_condense_section($format)
1863 push @early, @$extra_sections if $extra_sections;
1865 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1869 #helper subs for above
1872 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1876 my $categoryname = shift;
1877 $pkg_category_cache{$categoryname} ||=
1878 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1881 my %condensed_format = (
1882 'label' => [ qw( Description Qty Amount ) ],
1884 sub { shift->{description} },
1885 sub { shift->{quantity} },
1886 sub { my($href, %opt) = @_;
1887 ($opt{dollar} || ''). $href->{amount};
1890 'align' => [ qw( l r r ) ],
1891 'span' => [ qw( 5 1 1 ) ], # unitprices?
1892 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1895 sub _condense_section {
1896 my ( $self, $format ) = ( shift, shift );
1898 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1899 qw( description_generator
1902 total_line_generator
1907 sub _condensed_generator_defaults {
1908 my ( $self, $format ) = ( shift, shift );
1909 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1918 sub _condensed_header_generator {
1919 my ( $self, $format ) = ( shift, shift );
1921 my ( $f, $prefix, $suffix, $separator, $column ) =
1922 _condensed_generator_defaults($format);
1924 if ($format eq 'latex') {
1925 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1926 $suffix = "\\\\\n\\hline";
1929 sub { my ($d,$a,$s,$w) = @_;
1930 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1932 } elsif ( $format eq 'html' ) {
1933 $prefix = '<th></th>';
1937 sub { my ($d,$a,$s,$w) = @_;
1938 return qq!<th align="$html_align{$a}">$d</th>!;
1946 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1948 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1951 $prefix. join($separator, @result). $suffix;
1956 sub _condensed_description_generator {
1957 my ( $self, $format ) = ( shift, shift );
1959 my ( $f, $prefix, $suffix, $separator, $column ) =
1960 _condensed_generator_defaults($format);
1962 my $money_char = '$';
1963 if ($format eq 'latex') {
1964 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1966 $separator = " & \n";
1968 sub { my ($d,$a,$s,$w) = @_;
1969 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1971 $money_char = '\\dollar';
1972 }elsif ( $format eq 'html' ) {
1973 $prefix = '"><td align="center"></td>';
1977 sub { my ($d,$a,$s,$w) = @_;
1978 return qq!<td align="$html_align{$a}">$d</td>!;
1980 #$money_char = $conf->config('money_char') || '$';
1981 $money_char = ''; # this is madness
1989 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1991 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1993 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1994 map { $f->{$_}->[$i] } qw(align span width)
1998 $prefix. join( $separator, @result ). $suffix;
2003 sub _condensed_total_generator {
2004 my ( $self, $format ) = ( shift, shift );
2006 my ( $f, $prefix, $suffix, $separator, $column ) =
2007 _condensed_generator_defaults($format);
2010 if ($format eq 'latex') {
2013 $separator = " & \n";
2015 sub { my ($d,$a,$s,$w) = @_;
2016 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2018 }elsif ( $format eq 'html' ) {
2022 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2024 sub { my ($d,$a,$s,$w) = @_;
2025 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2034 # my $r = &{$f->{fields}->[$i]}(@args);
2035 # $r .= ' Total' unless $i;
2037 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2039 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2040 map { $f->{$_}->[$i] } qw(align span width)
2044 $prefix. join( $separator, @result ). $suffix;
2049 =item total_line_generator FORMAT
2051 Returns a coderef used for generation of invoice total line items for this
2052 usage_class. FORMAT is either html or latex
2056 # should not be used: will have issues with hash element names (description vs
2057 # total_item and amount vs total_amount -- another array of functions?
2059 sub _condensed_total_line_generator {
2060 my ( $self, $format ) = ( shift, shift );
2062 my ( $f, $prefix, $suffix, $separator, $column ) =
2063 _condensed_generator_defaults($format);
2066 if ($format eq 'latex') {
2069 $separator = " & \n";
2071 sub { my ($d,$a,$s,$w) = @_;
2072 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2074 }elsif ( $format eq 'html' ) {
2078 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2080 sub { my ($d,$a,$s,$w) = @_;
2081 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2090 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2092 &{$column}( &{$f->{fields}->[$i]}(@args),
2093 map { $f->{$_}->[$i] } qw(align span width)
2097 $prefix. join( $separator, @result ). $suffix;
2102 # sub _items { # seems to be unused
2105 # #my @display = scalar(@_)
2107 # # : qw( _items_previous _items_pkg );
2108 # # #: qw( _items_pkg );
2109 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2110 # my @display = qw( _items_previous _items_pkg );
2113 # foreach my $display ( @display ) {
2114 # push @b, $self->$display(@_);
2119 =item _items_pkg [ OPTIONS ]
2121 Return line item hashes for each package item on this invoice. Nearly
2124 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2126 The only OPTIONS accepted is 'section', which may point to a hashref
2127 with a key named 'condensed', which may have a true value. If it
2128 does, this method tries to merge identical items into items with
2129 'quantity' equal to the number of items (not the sum of their
2130 separate quantities, for some reason).
2136 grep { $_->pkgnum } $self->cust_bill_pkg;
2143 warn "$me _items_pkg searching for all package line items\n"
2146 my @cust_bill_pkg = $self->_items_nontax;
2148 warn "$me _items_pkg filtering line items\n"
2150 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2152 if ($options{section} && $options{section}->{condensed}) {
2154 warn "$me _items_pkg condensing section\n"
2158 local $Storable::canonical = 1;
2159 foreach ( @items ) {
2161 delete $item->{ref};
2162 delete $item->{ext_description};
2163 my $key = freeze($item);
2164 $itemshash{$key} ||= 0;
2165 $itemshash{$key} ++; # += $item->{quantity};
2167 @items = sort { $a->{description} cmp $b->{description} }
2168 map { my $i = thaw($_);
2169 $i->{quantity} = $itemshash{$_};
2171 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2177 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2184 return 0 unless $a->itemdesc cmp $b->itemdesc;
2185 return -1 if $b->itemdesc eq 'Tax';
2186 return 1 if $a->itemdesc eq 'Tax';
2187 return -1 if $b->itemdesc eq 'Other surcharges';
2188 return 1 if $a->itemdesc eq 'Other surcharges';
2189 $a->itemdesc cmp $b->itemdesc;
2194 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2195 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2197 if ( $self->conf->exists('always_show_tax') ) {
2198 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2199 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2201 { 'description' => $itemdesc,
2208 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2210 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2211 list of hashrefs describing the line items they generate on the invoice.
2213 OPTIONS may include:
2215 format: the invoice format.
2217 escape_function: the function used to escape strings.
2219 DEPRECATED? (expensive, mostly unused?)
2220 format_function: the function used to format CDRs.
2222 section: a hashref containing 'description'; if this is present,
2223 cust_bill_pkg_display records not belonging to this section are
2226 multisection: a flag indicating that this is a multisection invoice,
2227 which does something complicated.
2229 Returns a list of hashrefs, each of which may contain:
2231 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2232 ext_description, which is an arrayref of detail lines to show below
2237 sub _items_cust_bill_pkg {
2239 my $conf = $self->conf;
2240 my $cust_bill_pkgs = shift;
2243 my $format = $opt{format} || '';
2244 my $escape_function = $opt{escape_function} || sub { shift };
2245 my $format_function = $opt{format_function} || '';
2246 my $no_usage = $opt{no_usage} || '';
2247 my $unsquelched = $opt{unsquelched} || ''; #unused
2248 my $section = $opt{section}->{description} if $opt{section};
2249 my $summary_page = $opt{summary_page} || ''; #unused
2250 my $multisection = $opt{multisection} || '';
2251 my $discount_show_always = 0;
2253 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2255 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2256 # and location labels
2259 my ($s, $r, $u) = ( undef, undef, undef );
2260 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2263 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2264 if ( $_ && !$cust_bill_pkg->hidden ) {
2265 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2266 $_->{amount} =~ s/^\-0\.00$/0.00/;
2267 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2269 if $_->{amount} != 0
2270 || $discount_show_always
2271 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2272 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2278 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2279 ? $cust_bill_pkg->cust_bill_pkg_display
2280 : ( $cust_bill_pkg );
2282 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2283 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2286 foreach my $display ( grep { defined($section)
2287 ? $_->section eq $section
2290 grep { !$_->summary || $multisection }
2291 @cust_bill_pkg_display
2295 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2296 $display->billpkgdisplaynum. "\n"
2299 my $type = $display->type;
2301 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2302 $desc = substr($desc, 0, $maxlength). '...'
2303 if $format eq 'latex' && length($desc) > $maxlength;
2305 my %details_opt = ( 'format' => $format,
2306 'escape_function' => $escape_function,
2307 'format_function' => $format_function,
2308 'no_usage' => $opt{'no_usage'},
2311 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2313 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2316 if ( $cust_bill_pkg->setup != 0 ) {
2317 my $description = $desc;
2318 $description .= ' Setup'
2319 if $cust_bill_pkg->recur != 0
2320 || $discount_show_always
2321 || $cust_bill_pkg->recur_show_zero;
2323 'description' => $description,
2324 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2327 if ( $cust_bill_pkg->recur != 0 ) {
2329 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2330 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2334 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2336 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2339 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2341 # which pkgpart to show for display purposes?
2342 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2344 # start/end dates for invoice formats that do nonstandard
2346 my %item_dates = ();
2347 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2348 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2350 if ( (!$type || $type eq 'S')
2351 && ( $cust_bill_pkg->setup != 0
2352 || $cust_bill_pkg->setup_show_zero
2357 warn "$me _items_cust_bill_pkg adding setup\n"
2360 my $description = $desc;
2361 $description .= ' Setup'
2362 if $cust_bill_pkg->recur != 0
2363 || $discount_show_always
2364 || $cust_bill_pkg->recur_show_zero;
2368 unless ( $cust_pkg->part_pkg->hide_svc_detail
2369 || $cust_bill_pkg->hidden )
2372 my @svc_labels = map &{$escape_function}($_),
2373 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2374 push @d, @svc_labels
2375 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2376 $svc_label = $svc_labels[0];
2378 my $lnum = $cust_main ? $cust_main->ship_locationnum
2379 : $self->prospect_main->locationnum;
2380 if ( ! $cust_pkg->locationnum or $cust_pkg->locationnum != $lnum ) {
2381 my $loc = $cust_pkg->location_label;
2382 $loc = substr($loc, 0, $maxlength). '...'
2383 if $format eq 'latex' && length($loc) > $maxlength;
2384 push @d, &{$escape_function}($loc);
2387 } #unless hiding service details
2389 push @d, $cust_bill_pkg->details(%details_opt)
2390 if $cust_bill_pkg->recur == 0;
2392 if ( $cust_bill_pkg->hidden ) {
2393 $s->{amount} += $cust_bill_pkg->setup;
2394 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2395 push @{ $s->{ext_description} }, @d;
2399 description => $description,
2400 pkgpart => $pkgpart,
2401 pkgnum => $cust_bill_pkg->pkgnum,
2402 amount => $cust_bill_pkg->setup,
2403 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2404 unit_amount => $cust_bill_pkg->unitsetup,
2405 quantity => $cust_bill_pkg->quantity,
2406 ext_description => \@d,
2407 svc_label => ($svc_label || ''),
2413 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2415 $cust_bill_pkg->recur != 0
2416 || $cust_bill_pkg->setup == 0
2417 || $discount_show_always
2418 || $cust_bill_pkg->recur_show_zero
2423 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2426 my $is_summary = $display->summary;
2427 my $description = ($is_summary && $type && $type eq 'U')
2428 ? "Usage charges" : $desc;
2430 my $part_pkg = $cust_pkg->part_pkg;
2432 #pry be a bit more efficient to look some of this conf stuff up
2435 $conf->exists('disable_line_item_date_ranges')
2436 || $part_pkg->option('disable_line_item_date_ranges',1)
2437 || ! $cust_bill_pkg->sdate
2438 || ! $cust_bill_pkg->edate
2441 my $date_style = '';
2442 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2445 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2446 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2449 if ( defined($date_style) && $date_style eq 'month_of' ) {
2450 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2451 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2452 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2455 $desc .= ' ' unless $desc =~ /\s$/;
2456 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2458 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2459 " - ". time2str($date_format, $cust_bill_pkg->edate);
2461 $description .= " ($time_period)";
2465 my @seconds = (); # for display of usage info
2468 #at least until cust_bill_pkg has "past" ranges in addition to
2469 #the "future" sdate/edate ones... see #3032
2470 my @dates = ( $self->_date );
2471 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2472 push @dates, $prev->sdate if $prev;
2473 push @dates, undef if !$prev;
2475 unless ( $part_pkg->hide_svc_detail
2476 || $cust_bill_pkg->itemdesc
2477 || $cust_bill_pkg->hidden
2478 || $is_summary && $type && $type eq 'U'
2482 warn "$me _items_cust_bill_pkg adding service details\n"
2485 my @svc_labels = map &{$escape_function}($_),
2486 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2487 push @d, @svc_labels
2488 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2489 $svc_label = $svc_labels[0];
2491 warn "$me _items_cust_bill_pkg done adding service details\n"
2494 my $lnum = $cust_main ? $cust_main->ship_locationnum
2495 : $self->prospect_main->locationnum;
2496 if ( $cust_pkg->locationnum != $lnum ) {
2497 my $loc = $cust_pkg->location_label;
2498 $loc = substr($loc, 0, $maxlength). '...'
2499 if $format eq 'latex' && length($loc) > $maxlength;
2500 push @d, &{$escape_function}($loc);
2503 # Display of seconds_since_sqlradacct:
2504 # On the invoice, when processing @detail_items, look for a field
2505 # named 'seconds'. This will contain total seconds for each
2506 # service, in the same order as @ext_description. For services
2507 # that don't support this it will show undef.
2508 if ( $conf->exists('svc_acct-usage_seconds')
2509 and ! $cust_bill_pkg->pkgpart_override ) {
2510 foreach my $cust_svc (
2511 $cust_pkg->h_cust_svc(@dates, 'I')
2514 # eval because not having any part_export_usage exports
2515 # is a fatal error, last_bill/_date because that's how
2516 # sqlradius_hour billing does it
2518 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2520 push @seconds, $sec;
2522 } #if svc_acct-usage_seconds
2526 unless ( $is_summary ) {
2527 warn "$me _items_cust_bill_pkg adding details\n"
2530 #instead of omitting details entirely in this case (unwanted side
2531 # effects), just omit CDRs
2532 $details_opt{'no_usage'} = 1
2533 if $type && $type eq 'R';
2535 push @d, $cust_bill_pkg->details(%details_opt);
2538 warn "$me _items_cust_bill_pkg calculating amount\n"
2543 $amount = $cust_bill_pkg->recur;
2544 } elsif ($type eq 'R') {
2545 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2546 } elsif ($type eq 'U') {
2547 $amount = $cust_bill_pkg->usage;
2551 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2554 if ( !$type || $type eq 'R' ) {
2556 warn "$me _items_cust_bill_pkg adding recur\n"
2559 if ( $cust_bill_pkg->hidden ) {
2560 $r->{amount} += $amount;
2561 $r->{unit_amount} += $unit_amount;
2562 push @{ $r->{ext_description} }, @d;
2565 description => $description,
2566 pkgpart => $pkgpart,
2567 pkgnum => $cust_bill_pkg->pkgnum,
2569 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2570 unit_amount => $unit_amount,
2571 quantity => $cust_bill_pkg->quantity,
2573 ext_description => \@d,
2574 svc_label => ($svc_label || ''),
2576 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2579 } else { # $type eq 'U'
2581 warn "$me _items_cust_bill_pkg adding usage\n"
2584 if ( $cust_bill_pkg->hidden ) {
2585 $u->{amount} += $amount;
2586 $u->{unit_amount} += $unit_amount,
2587 push @{ $u->{ext_description} }, @d;
2590 description => $description,
2591 pkgpart => $pkgpart,
2592 pkgnum => $cust_bill_pkg->pkgnum,
2594 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2595 unit_amount => $unit_amount,
2596 quantity => $cust_bill_pkg->quantity,
2598 ext_description => \@d,
2603 } # recurring or usage with recurring charge
2605 } else { #pkgnum tax or one-shot line item (??)
2607 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2610 if ( $cust_bill_pkg->setup != 0 ) {
2612 'description' => $desc,
2613 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2616 if ( $cust_bill_pkg->recur != 0 ) {
2618 'description' => "$desc (".
2619 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2620 time2str($date_format, $cust_bill_pkg->edate). ')',
2621 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2629 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2630 && $conf->exists('discount-show-always'));
2634 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2636 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2637 $_->{amount} =~ s/^\-0\.00$/0.00/;
2638 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2640 if $_->{amount} != 0
2641 || $discount_show_always
2642 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2643 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2647 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2654 =item _items_discounts_avail
2656 Returns an array of line item hashrefs representing available term discounts
2657 for this invoice. This makes the same assumptions that apply to term
2658 discounts in general: that the package is billed monthly, at a flat rate,
2659 with no usage charges. A prorated first month will be handled, as will
2660 a setup fee if the discount is allowed to apply to setup fees.
2664 sub _items_discounts_avail {
2667 #maybe move this method from cust_bill when quotations support discount_plans
2668 return () unless $self->can('discount_plans');
2669 my %plans = $self->discount_plans;
2671 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2672 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2676 my $plan = $plans{$months};
2678 my $term_total = sprintf('%.2f', $plan->discounted_total);
2679 my $percent = sprintf('%.0f',
2680 100 * (1 - $term_total / $plan->base_total) );
2681 my $permonth = sprintf('%.2f', $term_total / $months);
2682 my $detail = $self->mt('discount on item'). ' '.
2683 join(', ', map { "#$_" } $plan->pkgnums)
2686 # discounts for non-integer months don't work anyway
2687 $months = sprintf("%d", $months);
2690 description => $self->mt('Save [_1]% by paying for [_2] months',
2692 amount => $self->mt('[_1] ([_2] per month)',
2693 $term_total, $money_char.$permonth),
2694 ext_description => ($detail || ''),
2697 sort { $b <=> $a } keys %plans;