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 $invoice_data{'multisection'} = $multisection;
805 my $late_sections = [];
806 my $extra_sections = [];
807 my $extra_lines = ();
809 my $default_section = { 'description' => '',
814 if ( $multisection ) {
815 ($extra_sections, $extra_lines) =
816 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
817 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
818 && $self->can('_items_extra_usage_sections');
820 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
822 push @detail_items, @$extra_lines if $extra_lines;
824 $self->_items_sections( $late_sections, # this could stand a refactor
826 $escape_function_nonbsp,
830 if ( $conf->exists('svc_phone_sections')
831 && $self->can('_items_svc_phone_sections')
834 my ($phone_sections, $phone_lines) =
835 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
836 push @{$late_sections}, @$phone_sections;
837 push @detail_items, @$phone_lines;
839 if ( $conf->exists('voip-cust_accountcode_cdr')
840 && $cust_main->accountcode_cdr
841 && $self->can('_items_accountcode_cdr')
844 my ($accountcode_section, $accountcode_lines) =
845 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
846 if ( scalar(@$accountcode_lines) ) {
847 push @{$late_sections}, $accountcode_section;
848 push @detail_items, @$accountcode_lines;
851 } else {# not multisection
852 # make a default section
853 push @sections, $default_section;
854 # and calculate the finance charge total, since it won't get done otherwise.
855 # XXX possibly other totals?
856 # XXX possibly finance_pkgclass should not be used in this manner?
857 if ( $conf->exists('finance_pkgclass') ) {
859 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
860 if ( grep { $_->section eq $invoice_data{finance_section} }
861 $cust_bill_pkg->cust_bill_pkg_display ) {
862 # I think these are always setup fees, but just to be sure...
863 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
866 $invoice_data{finance_amount} =
867 sprintf('%.2f', sum( @finance_charges ) || 0);
871 # previous invoice balances in the Previous Charges section if there
872 # is one, otherwise in the main detail section
873 if ( $self->can('_items_previous') &&
874 $self->enable_previous &&
875 ! $conf->exists('previous_balance-summary_only') ) {
877 warn "$me adding previous balances\n"
880 foreach my $line_item ( $self->_items_previous ) {
883 ext_description => [],
885 $detail->{'ref'} = $line_item->{'pkgnum'};
886 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
887 $detail->{'quantity'} = 1;
888 $detail->{'section'} = $multisection ? $previous_section
890 $detail->{'description'} = &$escape_function($line_item->{'description'});
891 if ( exists $line_item->{'ext_description'} ) {
892 @{$detail->{'ext_description'}} = map {
893 &$escape_function($_);
894 } @{$line_item->{'ext_description'}};
896 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
897 $line_item->{'amount'};
898 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
900 push @detail_items, $detail;
901 push @buf, [ $detail->{'description'},
902 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
908 if ( @pr_cust_bill && $self->enable_previous ) {
909 push @buf, ['','-----------'];
910 push @buf, [ $self->mt('Total Previous Balance'),
911 $money_char. sprintf("%10.2f", $pr_total) ];
915 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
916 warn "$me adding DID summary\n"
919 my ($didsummary,$minutes) = $self->_did_summary;
920 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
922 { 'description' => $didsummary_desc,
923 'ext_description' => [ $didsummary, $minutes ],
927 foreach my $section (@sections, @$late_sections) {
929 warn "$me adding section \n". Dumper($section)
932 # begin some normalization
933 $section->{'subtotal'} = $section->{'amount'}
935 && !exists($section->{subtotal})
936 && exists($section->{amount});
938 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
939 if ( $invoice_data{finance_section} &&
940 $section->{'description'} eq $invoice_data{finance_section} );
942 $section->{'subtotal'} = $other_money_char.
943 sprintf('%.2f', $section->{'subtotal'})
946 # continue some normalization
947 $section->{'amount'} = $section->{'subtotal'}
951 if ( $section->{'description'} ) {
952 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
957 warn "$me setting options\n"
961 $options{'section'} = $section if $multisection;
962 $options{'format'} = $format;
963 $options{'escape_function'} = $escape_function;
964 $options{'no_usage'} = 1 unless $unsquelched;
965 $options{'unsquelched'} = $unsquelched;
966 $options{'summary_page'} = $summarypage;
967 $options{'skip_usage'} =
968 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
969 $options{'multisection'} = $multisection;
971 warn "$me searching for line items\n"
974 foreach my $line_item ( $self->_items_pkg(%options) ) {
976 warn "$me adding line item $line_item\n"
980 ext_description => [],
982 $detail->{'ref'} = $line_item->{'pkgnum'};
983 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
984 $detail->{'quantity'} = $line_item->{'quantity'};
985 $detail->{'section'} = $section;
986 $detail->{'description'} = &$escape_function($line_item->{'description'});
987 if ( exists $line_item->{'ext_description'} ) {
988 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
990 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
991 $line_item->{'amount'};
992 if ( exists $line_item->{'unit_amount'} ) {
993 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
994 $line_item->{'unit_amount'};
996 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
998 $detail->{'sdate'} = $line_item->{'sdate'};
999 $detail->{'edate'} = $line_item->{'edate'};
1000 $detail->{'seconds'} = $line_item->{'seconds'};
1001 $detail->{'svc_label'} = $line_item->{'svc_label'};
1003 push @detail_items, $detail;
1004 push @buf, ( [ $detail->{'description'},
1005 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1007 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1011 if ( $section->{'description'} ) {
1012 push @buf, ( ['','-----------'],
1013 [ $section->{'description'}. ' sub-total',
1014 $section->{'subtotal'} # already formatted this
1023 $invoice_data{current_less_finance} =
1024 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1026 # create a major section for previous balance if we have major sections,
1027 # or if previous_section is in summary form
1028 if ( ( $multisection && $self->enable_previous )
1029 || $conf->exists('previous_balance-summary_only') )
1031 unshift @sections, $previous_section if $pr_total;
1034 warn "$me adding taxes\n"
1037 my @items_tax = $self->_items_tax;
1038 foreach my $tax ( @items_tax ) {
1040 $taxtotal += $tax->{'amount'};
1042 my $description = &$escape_function( $tax->{'description'} );
1043 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1045 if ( $multisection ) {
1047 my $money = $old_latex ? '' : $money_char;
1048 push @detail_items, {
1049 ext_description => [],
1052 description => $description,
1053 amount => $money. $amount,
1055 section => $tax_section,
1060 push @total_items, {
1061 'total_item' => $description,
1062 'total_amount' => $other_money_char. $amount,
1067 push @buf,[ $description,
1068 $money_char. $amount,
1075 $total->{'total_item'} = $self->mt('Sub-total');
1076 $total->{'total_amount'} =
1077 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1079 if ( $multisection ) {
1080 $tax_section->{'subtotal'} = $other_money_char.
1081 sprintf('%.2f', $taxtotal);
1082 $tax_section->{'pretotal'} = 'New charges sub-total '.
1083 $total->{'total_amount'};
1084 push @sections, $tax_section if $taxtotal;
1086 unshift @total_items, $total;
1089 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1091 push @buf,['','-----------'];
1092 push @buf,[$self->mt(
1093 (!$self->enable_previous)
1095 : 'Total New Charges'
1097 $money_char. sprintf("%10.2f",$self->charged) ];
1105 my %embolden_functions = (
1106 'latex' => sub { return '\textbf{'. shift(). '}' },
1107 'html' => sub { return '<b>'. shift(). '</b>' },
1108 'template' => sub { shift },
1110 my $embolden_function = $embolden_functions{$format};
1112 if ( $self->can('_items_total') ) { # quotations
1114 $self->_items_total(\@total_items);
1116 foreach ( @total_items ) {
1117 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1118 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1119 $_->{'total_amount'}
1123 } else { #normal invoice case
1125 # calculate total, possibly including total owed on previous
1129 $item = $conf->config('previous_balance-exclude_from_total')
1130 || 'Total New Charges'
1131 if $conf->exists('previous_balance-exclude_from_total');
1132 my $amount = $self->charged;
1133 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1134 $amount += $pr_total;
1137 $total->{'total_item'} = &$embolden_function($self->mt($item));
1138 $total->{'total_amount'} =
1139 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1140 if ( $multisection ) {
1141 if ( $adjust_section->{'sort_weight'} ) {
1142 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1143 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1145 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1146 $other_money_char. sprintf('%.2f', $self->charged );
1149 push @total_items, $total;
1151 push @buf,['','-----------'];
1154 sprintf( '%10.2f', $amount )
1158 # if we're showing previous invoices, also show previous
1159 # credits and payments
1160 if ( $self->enable_previous
1161 and $self->can('_items_credits')
1162 and $self->can('_items_payments') )
1164 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1167 my $credittotal = 0;
1168 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1171 $total->{'total_item'} = &$escape_function($credit->{'description'});
1172 $credittotal += $credit->{'amount'};
1173 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1174 $adjusttotal += $credit->{'amount'};
1175 if ( $multisection ) {
1176 my $money = $old_latex ? '' : $money_char;
1177 push @detail_items, {
1178 ext_description => [],
1181 description => &$escape_function($credit->{'description'}),
1182 amount => $money. $credit->{'amount'},
1184 section => $adjust_section,
1187 push @total_items, $total;
1191 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1194 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1195 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1199 my $paymenttotal = 0;
1200 foreach my $payment ( $self->_items_payments ) {
1202 $total->{'total_item'} = &$escape_function($payment->{'description'});
1203 $paymenttotal += $payment->{'amount'};
1204 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1205 $adjusttotal += $payment->{'amount'};
1206 if ( $multisection ) {
1207 my $money = $old_latex ? '' : $money_char;
1208 push @detail_items, {
1209 ext_description => [],
1212 description => &$escape_function($payment->{'description'}),
1213 amount => $money. $payment->{'amount'},
1215 section => $adjust_section,
1218 push @total_items, $total;
1220 push @buf, [ $payment->{'description'},
1221 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1224 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1226 if ( $multisection ) {
1227 $adjust_section->{'subtotal'} = $other_money_char.
1228 sprintf('%.2f', $adjusttotal);
1229 push @sections, $adjust_section
1230 unless $adjust_section->{sort_weight};
1233 # create Balance Due message
1236 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1237 $total->{'total_amount'} =
1238 &$embolden_function(
1239 $other_money_char. sprintf('%.2f', #why? $summarypage
1240 # ? $self->charged +
1241 # $self->billing_balance
1243 $self->owed + $pr_total
1246 if ( $multisection && !$adjust_section->{sort_weight} ) {
1247 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1248 $total->{'total_amount'};
1250 push @total_items, $total;
1252 push @buf,['','-----------'];
1253 push @buf,[$self->balance_due_msg, $money_char.
1254 sprintf("%10.2f", $balance_due ) ];
1257 if ( $conf->exists('previous_balance-show_credit')
1258 and $cust_main->balance < 0 ) {
1259 my $credit_total = {
1260 'total_item' => &$embolden_function($self->credit_balance_msg),
1261 'total_amount' => &$embolden_function(
1262 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1265 if ( $multisection ) {
1266 $adjust_section->{'posttotal'} .= $newline_token .
1267 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1270 push @total_items, $credit_total;
1272 push @buf,['','-----------'];
1273 push @buf,[$self->credit_balance_msg, $money_char.
1274 sprintf("%10.2f", -$cust_main->balance ) ];
1278 } #end of default total adding ! can('_items_total')
1280 if ( $multisection ) {
1281 if ( $conf->exists('svc_phone_sections')
1282 && $self->can('_items_svc_phone_sections')
1286 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1287 $total->{'total_amount'} =
1288 &$embolden_function(
1289 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1291 my $last_section = pop @sections;
1292 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1293 $total->{'total_amount'};
1294 push @sections, $last_section;
1296 push @sections, @$late_sections
1300 # make a discounts-available section, even without multisection
1301 if ( $conf->exists('discount-show_available')
1302 and my @discounts_avail = $self->_items_discounts_avail ) {
1303 my $discount_section = {
1304 'description' => $self->mt('Discounts Available'),
1309 push @sections, $discount_section;
1310 push @detail_items, map { +{
1311 'ref' => '', #should this be something else?
1312 'section' => $discount_section,
1313 'description' => &$escape_function( $_->{description} ),
1314 'amount' => $money_char . &$escape_function( $_->{amount} ),
1315 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1316 } } @discounts_avail;
1319 # debugging hook: call this with 'diag' => 1 to just get a hash of
1320 # the invoice variables
1321 return \%invoice_data if ( $params{'diag'} );
1323 # All sections and items are built; now fill in templates.
1324 my @includelist = ();
1325 push @includelist, 'summary' if $summarypage;
1326 foreach my $include ( @includelist ) {
1328 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1331 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1333 @inc_src = $conf->config($inc_file, $agentnum);
1337 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1339 my $convert_map = $convert_maps{$format}{$include};
1341 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1342 s/--\@\]/$delimiters{$format}[1]/g;
1345 &$convert_map( $conf->config($inc_file, $agentnum) );
1349 my $inc_tt = new Text::Template (
1351 SOURCE => [ map "$_\n", @inc_src ],
1352 DELIMITERS => $delimiters{$format},
1353 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1355 unless ( $inc_tt->compile() ) {
1356 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1357 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1361 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1363 $invoice_data{$include} =~ s/\n+$//
1364 if ($format eq 'latex');
1369 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1370 /invoice_lines\((\d*)\)/;
1371 $invoice_lines += $1 || scalar(@buf);
1374 die "no invoice_lines() functions in template?"
1375 if ( $format eq 'template' && !$wasfunc );
1377 if ($format eq 'template') {
1379 if ( $invoice_lines ) {
1380 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1381 $invoice_data{'total_pages'}++
1382 if scalar(@buf) % $invoice_lines;
1385 #setup subroutine for the template
1386 $invoice_data{invoice_lines} = sub {
1387 my $lines = shift || scalar(@buf);
1399 push @collect, split("\n",
1400 $text_template->fill_in( HASH => \%invoice_data )
1402 $invoice_data{'page'}++;
1404 map "$_\n", @collect;
1406 } else { # this is where we actually create the invoice
1408 warn "filling in template for invoice ". $self->invnum. "\n"
1410 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1413 $text_template->fill_in(HASH => \%invoice_data);
1417 sub notice_name { '('.shift->table.')'; }
1419 sub template_conf { 'invoice_'; }
1421 # helper routine for generating date ranges
1422 sub _prior_month30s {
1425 [ 1, 2592000 ], # 0-30 days ago
1426 [ 2592000, 5184000 ], # 30-60 days ago
1427 [ 5184000, 7776000 ], # 60-90 days ago
1428 [ 7776000, 0 ], # 90+ days ago
1431 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1432 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1437 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1439 Returns an postscript invoice, as a scalar.
1441 Options can be passed as a hashref (recommended) or as a list of time, template
1442 and then any key/value pairs for any other options.
1444 I<time> an optional value used to control the printing of overdue messages. The
1445 default is now. It isn't the date of the invoice; that's the `_date' field.
1446 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1447 L<Time::Local> and L<Date::Parse> for conversion functions.
1449 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1456 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1457 my $ps = generate_ps($file);
1459 unlink($barcodefile) if $barcodefile;
1464 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1466 Returns an PDF invoice, as a scalar.
1468 Options can be passed as a hashref (recommended) or as a list of time, template
1469 and then any key/value pairs for any other options.
1471 I<time> an optional value used to control the printing of overdue messages. The
1472 default is now. It isn't the date of the invoice; that's the `_date' field.
1473 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1474 L<Time::Local> and L<Date::Parse> for conversion functions.
1476 I<template>, if specified, is the name of a suffix for alternate invoices.
1478 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1485 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1486 my $pdf = generate_pdf($file);
1488 unlink($barcodefile) if $barcodefile;
1493 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1495 Returns an HTML invoice, as a scalar.
1497 I<time> an optional value used to control the printing of overdue messages. The
1498 default is now. It isn't the date of the invoice; that's the `_date' field.
1499 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1500 L<Time::Local> and L<Date::Parse> for conversion functions.
1502 I<template>, if specified, is the name of a suffix for alternate invoices.
1504 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1506 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1507 when emailing the invoice as part of a multipart/related MIME email.
1515 %params = %{ shift() };
1517 $params{'time'} = shift;
1518 $params{'template'} = shift;
1519 $params{'cid'} = shift;
1522 $params{'format'} = 'html';
1524 $self->print_generic( %params );
1527 # quick subroutine for print_latex
1529 # There are ten characters that LaTeX treats as special characters, which
1530 # means that they do not simply typeset themselves:
1531 # # $ % & ~ _ ^ \ { }
1533 # TeX ignores blanks following an escaped character; if you want a blank (as
1534 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1538 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1539 $value =~ s/([<>])/\$$1\$/g;
1545 encode_entities($value);
1549 sub _html_escape_nbsp {
1550 my $value = _html_escape(shift);
1551 $value =~ s/ +/ /g;
1555 #utility methods for print_*
1557 sub _translate_old_latex_format {
1558 warn "_translate_old_latex_format called\n"
1565 if ( $line =~ /^%%Detail\s*$/ ) {
1567 push @template, q![@--!,
1568 q! foreach my $_tr_line (@detail_items) {!,
1569 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1570 q! $_tr_line->{'description'} .= !,
1571 q! "\\tabularnewline\n~~".!,
1572 q! join( "\\tabularnewline\n~~",!,
1573 q! @{$_tr_line->{'ext_description'}}!,
1577 while ( ( my $line_item_line = shift )
1578 !~ /^%%EndDetail\s*$/ ) {
1579 $line_item_line =~ s/'/\\'/g; # nice LTS
1580 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1581 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1582 push @template, " \$OUT .= '$line_item_line';";
1585 push @template, '}',
1588 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1590 push @template, '[@--',
1591 ' foreach my $_tr_line (@total_items) {';
1593 while ( ( my $total_item_line = shift )
1594 !~ /^%%EndTotalDetails\s*$/ ) {
1595 $total_item_line =~ s/'/\\'/g; # nice LTS
1596 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1597 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1598 push @template, " \$OUT .= '$total_item_line';";
1601 push @template, '}',
1605 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1606 push @template, $line;
1612 warn "$_\n" foreach @template;
1620 my $conf = $self->conf;
1622 #check for an invoice-specific override
1623 return $self->invoice_terms if $self->invoice_terms;
1625 #check for a customer- specific override
1626 my $cust_main = $self->cust_main;
1627 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1629 #use configured default
1630 $conf->config('invoice_default_terms') || '';
1636 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1637 $duedate = $self->_date() + ( $1 * 86400 );
1644 $self->due_date ? time2str(shift, $self->due_date) : '';
1647 sub balance_due_msg {
1649 my $msg = $self->mt('Balance Due');
1650 return $msg unless $self->terms;
1651 if ( $self->due_date ) {
1652 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1653 $self->due_date2str($date_format);
1654 } elsif ( $self->terms ) {
1655 $msg .= ' - '. $self->terms;
1660 sub balance_due_date {
1662 my $conf = $self->conf;
1664 if ( $conf->exists('invoice_default_terms')
1665 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1666 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1671 sub credit_balance_msg {
1673 $self->mt('Credit Balance Remaining')
1678 Returns a string with the date, for example: "3/20/2008"
1684 time2str($date_format, $self->_date);
1687 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1689 Generate section information for all items appearing on this invoice.
1690 This will only be called for multi-section invoices.
1692 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1693 related display records (L<FS::cust_bill_pkg_display>) and organize
1694 them into two groups ("early" and "late" according to whether they come
1695 before or after the total), then into sections. A subtotal is calculated
1698 Section descriptions are returned in sort weight order. Each consists
1699 of a hash containing:
1701 description: the package category name, escaped
1702 subtotal: the total charges in that section
1703 tax_section: a flag indicating that the section contains only tax charges
1704 summarized: same as tax_section, for some reason
1705 sort_weight: the package category's sort weight
1707 If 'condense' is set on the display record, it also contains everything
1708 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1709 coderefs to generate parts of the invoice. This is not advised.
1713 LATE: an arrayref to push the "late" section hashes onto. The "early"
1714 group is simply returned from the method.
1716 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1717 Turning this on has the following effects:
1718 - Ignores display items with the 'summary' flag.
1719 - Combines all items into the "early" group.
1720 - Creates sections for all non-disabled package categories, even if they
1721 have no charges on this invoice, as well as a section with no name.
1723 ESCAPE: an escape function to use for section titles.
1725 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1726 sorted list. If there are any of these, section subtotals exclude
1729 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1730 passed through to C<_condense_section()>.
1734 use vars qw(%pkg_category_cache);
1735 sub _items_sections {
1738 my $summarypage = shift;
1740 my $extra_sections = shift;
1744 my %late_subtotal = ();
1747 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1750 my $usage = $cust_bill_pkg->usage;
1752 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1753 next if ( $display->summary && $summarypage );
1755 my $section = $display->section;
1756 my $type = $display->type;
1758 $not_tax{$section} = 1
1759 unless $cust_bill_pkg->pkgnum == 0;
1761 # there's actually a very important piece of logic buried in here:
1762 # incrementing $late_subtotal{$section} CREATES
1763 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1764 # to define the list of late sections, and likewise keys(%subtotal).
1765 # When _items_cust_bill_pkg is called to generate line items for
1766 # real, it will be called with 'section' => $section for each
1768 if ( $display->post_total && !$summarypage ) {
1769 if (! $type || $type eq 'S') {
1770 $late_subtotal{$section} += $cust_bill_pkg->setup
1771 if $cust_bill_pkg->setup != 0
1772 || $cust_bill_pkg->setup_show_zero;
1776 $late_subtotal{$section} += $cust_bill_pkg->recur
1777 if $cust_bill_pkg->recur != 0
1778 || $cust_bill_pkg->recur_show_zero;
1781 if ($type && $type eq 'R') {
1782 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1783 if $cust_bill_pkg->recur != 0
1784 || $cust_bill_pkg->recur_show_zero;
1787 if ($type && $type eq 'U') {
1788 $late_subtotal{$section} += $usage
1789 unless scalar(@$extra_sections);
1794 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1796 if (! $type || $type eq 'S') {
1797 $subtotal{$section} += $cust_bill_pkg->setup
1798 if $cust_bill_pkg->setup != 0
1799 || $cust_bill_pkg->setup_show_zero;
1803 $subtotal{$section} += $cust_bill_pkg->recur
1804 if $cust_bill_pkg->recur != 0
1805 || $cust_bill_pkg->recur_show_zero;
1808 if ($type && $type eq 'R') {
1809 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1810 if $cust_bill_pkg->recur != 0
1811 || $cust_bill_pkg->recur_show_zero;
1814 if ($type && $type eq 'U') {
1815 $subtotal{$section} += $usage
1816 unless scalar(@$extra_sections);
1825 %pkg_category_cache = ();
1827 push @$late, map { { 'description' => &{$escape}($_),
1828 'subtotal' => $late_subtotal{$_},
1830 'sort_weight' => ( _pkg_category($_)
1831 ? _pkg_category($_)->weight
1834 ((_pkg_category($_) && _pkg_category($_)->condense)
1835 ? $self->_condense_section($format)
1839 sort _sectionsort keys %late_subtotal;
1842 if ( $summarypage ) {
1843 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1844 map { $_->categoryname } qsearch('pkg_category', {});
1845 push @sections, '' if exists($subtotal{''});
1847 @sections = keys %subtotal;
1850 my @early = map { { 'description' => &{$escape}($_),
1851 'subtotal' => $subtotal{$_},
1852 'summarized' => $not_tax{$_} ? '' : 'Y',
1853 'tax_section' => $not_tax{$_} ? '' : 'Y',
1854 'sort_weight' => ( _pkg_category($_)
1855 ? _pkg_category($_)->weight
1858 ((_pkg_category($_) && _pkg_category($_)->condense)
1859 ? $self->_condense_section($format)
1864 push @early, @$extra_sections if $extra_sections;
1866 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1870 #helper subs for above
1873 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1877 my $categoryname = shift;
1878 $pkg_category_cache{$categoryname} ||=
1879 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1882 my %condensed_format = (
1883 'label' => [ qw( Description Qty Amount ) ],
1885 sub { shift->{description} },
1886 sub { shift->{quantity} },
1887 sub { my($href, %opt) = @_;
1888 ($opt{dollar} || ''). $href->{amount};
1891 'align' => [ qw( l r r ) ],
1892 'span' => [ qw( 5 1 1 ) ], # unitprices?
1893 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1896 sub _condense_section {
1897 my ( $self, $format ) = ( shift, shift );
1899 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1900 qw( description_generator
1903 total_line_generator
1908 sub _condensed_generator_defaults {
1909 my ( $self, $format ) = ( shift, shift );
1910 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1919 sub _condensed_header_generator {
1920 my ( $self, $format ) = ( shift, shift );
1922 my ( $f, $prefix, $suffix, $separator, $column ) =
1923 _condensed_generator_defaults($format);
1925 if ($format eq 'latex') {
1926 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1927 $suffix = "\\\\\n\\hline";
1930 sub { my ($d,$a,$s,$w) = @_;
1931 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1933 } elsif ( $format eq 'html' ) {
1934 $prefix = '<th></th>';
1938 sub { my ($d,$a,$s,$w) = @_;
1939 return qq!<th align="$html_align{$a}">$d</th>!;
1947 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1949 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1952 $prefix. join($separator, @result). $suffix;
1957 sub _condensed_description_generator {
1958 my ( $self, $format ) = ( shift, shift );
1960 my ( $f, $prefix, $suffix, $separator, $column ) =
1961 _condensed_generator_defaults($format);
1963 my $money_char = '$';
1964 if ($format eq 'latex') {
1965 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1967 $separator = " & \n";
1969 sub { my ($d,$a,$s,$w) = @_;
1970 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1972 $money_char = '\\dollar';
1973 }elsif ( $format eq 'html' ) {
1974 $prefix = '"><td align="center"></td>';
1978 sub { my ($d,$a,$s,$w) = @_;
1979 return qq!<td align="$html_align{$a}">$d</td>!;
1981 #$money_char = $conf->config('money_char') || '$';
1982 $money_char = ''; # this is madness
1990 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1992 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1994 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1995 map { $f->{$_}->[$i] } qw(align span width)
1999 $prefix. join( $separator, @result ). $suffix;
2004 sub _condensed_total_generator {
2005 my ( $self, $format ) = ( shift, shift );
2007 my ( $f, $prefix, $suffix, $separator, $column ) =
2008 _condensed_generator_defaults($format);
2011 if ($format eq 'latex') {
2014 $separator = " & \n";
2016 sub { my ($d,$a,$s,$w) = @_;
2017 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2019 }elsif ( $format eq 'html' ) {
2023 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2025 sub { my ($d,$a,$s,$w) = @_;
2026 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2035 # my $r = &{$f->{fields}->[$i]}(@args);
2036 # $r .= ' Total' unless $i;
2038 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2040 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2041 map { $f->{$_}->[$i] } qw(align span width)
2045 $prefix. join( $separator, @result ). $suffix;
2050 =item total_line_generator FORMAT
2052 Returns a coderef used for generation of invoice total line items for this
2053 usage_class. FORMAT is either html or latex
2057 # should not be used: will have issues with hash element names (description vs
2058 # total_item and amount vs total_amount -- another array of functions?
2060 sub _condensed_total_line_generator {
2061 my ( $self, $format ) = ( shift, shift );
2063 my ( $f, $prefix, $suffix, $separator, $column ) =
2064 _condensed_generator_defaults($format);
2067 if ($format eq 'latex') {
2070 $separator = " & \n";
2072 sub { my ($d,$a,$s,$w) = @_;
2073 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2075 }elsif ( $format eq 'html' ) {
2079 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2081 sub { my ($d,$a,$s,$w) = @_;
2082 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2091 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2093 &{$column}( &{$f->{fields}->[$i]}(@args),
2094 map { $f->{$_}->[$i] } qw(align span width)
2098 $prefix. join( $separator, @result ). $suffix;
2103 # sub _items { # seems to be unused
2106 # #my @display = scalar(@_)
2108 # # : qw( _items_previous _items_pkg );
2109 # # #: qw( _items_pkg );
2110 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2111 # my @display = qw( _items_previous _items_pkg );
2114 # foreach my $display ( @display ) {
2115 # push @b, $self->$display(@_);
2120 =item _items_pkg [ OPTIONS ]
2122 Return line item hashes for each package item on this invoice. Nearly
2125 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2127 The only OPTIONS accepted is 'section', which may point to a hashref
2128 with a key named 'condensed', which may have a true value. If it
2129 does, this method tries to merge identical items into items with
2130 'quantity' equal to the number of items (not the sum of their
2131 separate quantities, for some reason).
2137 grep { $_->pkgnum } $self->cust_bill_pkg;
2144 warn "$me _items_pkg searching for all package line items\n"
2147 my @cust_bill_pkg = $self->_items_nontax;
2149 warn "$me _items_pkg filtering line items\n"
2151 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2153 if ($options{section} && $options{section}->{condensed}) {
2155 warn "$me _items_pkg condensing section\n"
2159 local $Storable::canonical = 1;
2160 foreach ( @items ) {
2162 delete $item->{ref};
2163 delete $item->{ext_description};
2164 my $key = freeze($item);
2165 $itemshash{$key} ||= 0;
2166 $itemshash{$key} ++; # += $item->{quantity};
2168 @items = sort { $a->{description} cmp $b->{description} }
2169 map { my $i = thaw($_);
2170 $i->{quantity} = $itemshash{$_};
2172 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2178 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2185 return 0 unless $a->itemdesc cmp $b->itemdesc;
2186 return -1 if $b->itemdesc eq 'Tax';
2187 return 1 if $a->itemdesc eq 'Tax';
2188 return -1 if $b->itemdesc eq 'Other surcharges';
2189 return 1 if $a->itemdesc eq 'Other surcharges';
2190 $a->itemdesc cmp $b->itemdesc;
2195 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2196 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2198 if ( $self->conf->exists('always_show_tax') ) {
2199 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2200 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2202 { 'description' => $itemdesc,
2209 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2211 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2212 list of hashrefs describing the line items they generate on the invoice.
2214 OPTIONS may include:
2216 format: the invoice format.
2218 escape_function: the function used to escape strings.
2220 DEPRECATED? (expensive, mostly unused?)
2221 format_function: the function used to format CDRs.
2223 section: a hashref containing 'description'; if this is present,
2224 cust_bill_pkg_display records not belonging to this section are
2227 multisection: a flag indicating that this is a multisection invoice,
2228 which does something complicated.
2230 Returns a list of hashrefs, each of which may contain:
2232 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2233 ext_description, which is an arrayref of detail lines to show below
2238 sub _items_cust_bill_pkg {
2240 my $conf = $self->conf;
2241 my $cust_bill_pkgs = shift;
2244 my $format = $opt{format} || '';
2245 my $escape_function = $opt{escape_function} || sub { shift };
2246 my $format_function = $opt{format_function} || '';
2247 my $no_usage = $opt{no_usage} || '';
2248 my $unsquelched = $opt{unsquelched} || ''; #unused
2249 my $section = $opt{section}->{description} if $opt{section};
2250 my $summary_page = $opt{summary_page} || ''; #unused
2251 my $multisection = $opt{multisection} || '';
2252 my $discount_show_always = 0;
2254 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2256 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2257 # and location labels
2260 my ($s, $r, $u) = ( undef, undef, undef );
2261 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2264 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2265 if ( $_ && !$cust_bill_pkg->hidden ) {
2266 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2267 $_->{amount} =~ s/^\-0\.00$/0.00/;
2268 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2270 if $_->{amount} != 0
2271 || $discount_show_always
2272 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2273 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2279 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2280 ? $cust_bill_pkg->cust_bill_pkg_display
2281 : ( $cust_bill_pkg );
2283 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2284 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2287 foreach my $display ( grep { defined($section)
2288 ? $_->section eq $section
2291 grep { !$_->summary || $multisection }
2292 @cust_bill_pkg_display
2296 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2297 $display->billpkgdisplaynum. "\n"
2300 my $type = $display->type;
2302 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2303 $desc = substr($desc, 0, $maxlength). '...'
2304 if $format eq 'latex' && length($desc) > $maxlength;
2306 my %details_opt = ( 'format' => $format,
2307 'escape_function' => $escape_function,
2308 'format_function' => $format_function,
2309 'no_usage' => $opt{'no_usage'},
2312 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2314 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2317 if ( $cust_bill_pkg->setup != 0 ) {
2318 my $description = $desc;
2319 $description .= ' Setup'
2320 if $cust_bill_pkg->recur != 0
2321 || $discount_show_always
2322 || $cust_bill_pkg->recur_show_zero;
2324 'description' => $description,
2325 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2328 if ( $cust_bill_pkg->recur != 0 ) {
2330 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2331 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2335 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2337 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2340 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2342 # which pkgpart to show for display purposes?
2343 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2345 # start/end dates for invoice formats that do nonstandard
2347 my %item_dates = ();
2348 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2349 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2351 if ( (!$type || $type eq 'S')
2352 && ( $cust_bill_pkg->setup != 0
2353 || $cust_bill_pkg->setup_show_zero
2358 warn "$me _items_cust_bill_pkg adding setup\n"
2361 my $description = $desc;
2362 $description .= ' Setup'
2363 if $cust_bill_pkg->recur != 0
2364 || $discount_show_always
2365 || $cust_bill_pkg->recur_show_zero;
2369 unless ( $cust_pkg->part_pkg->hide_svc_detail
2370 || $cust_bill_pkg->hidden )
2373 my @svc_labels = map &{$escape_function}($_),
2374 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2375 push @d, @svc_labels
2376 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2377 $svc_label = $svc_labels[0];
2379 my $lnum = $cust_main ? $cust_main->ship_locationnum
2380 : $self->prospect_main->locationnum;
2381 if ( ! $cust_pkg->locationnum or $cust_pkg->locationnum != $lnum ) {
2382 my $loc = $cust_pkg->location_label;
2383 $loc = substr($loc, 0, $maxlength). '...'
2384 if $format eq 'latex' && length($loc) > $maxlength;
2385 push @d, &{$escape_function}($loc);
2388 } #unless hiding service details
2390 push @d, $cust_bill_pkg->details(%details_opt)
2391 if $cust_bill_pkg->recur == 0;
2393 if ( $cust_bill_pkg->hidden ) {
2394 $s->{amount} += $cust_bill_pkg->setup;
2395 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2396 push @{ $s->{ext_description} }, @d;
2400 description => $description,
2401 pkgpart => $pkgpart,
2402 pkgnum => $cust_bill_pkg->pkgnum,
2403 amount => $cust_bill_pkg->setup,
2404 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2405 unit_amount => $cust_bill_pkg->unitsetup,
2406 quantity => $cust_bill_pkg->quantity,
2407 ext_description => \@d,
2408 svc_label => ($svc_label || ''),
2414 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2416 $cust_bill_pkg->recur != 0
2417 || $cust_bill_pkg->setup == 0
2418 || $discount_show_always
2419 || $cust_bill_pkg->recur_show_zero
2424 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2427 my $is_summary = $display->summary;
2428 my $description = ($is_summary && $type && $type eq 'U')
2429 ? "Usage charges" : $desc;
2431 my $part_pkg = $cust_pkg->part_pkg;
2433 #pry be a bit more efficient to look some of this conf stuff up
2436 $conf->exists('disable_line_item_date_ranges')
2437 || $part_pkg->option('disable_line_item_date_ranges',1)
2438 || ! $cust_bill_pkg->sdate
2439 || ! $cust_bill_pkg->edate
2442 my $date_style = '';
2443 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2446 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2447 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2450 if ( defined($date_style) && $date_style eq 'month_of' ) {
2451 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2452 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2453 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2456 $desc .= ' ' unless $desc =~ /\s$/;
2457 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2459 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2460 " - ". time2str($date_format, $cust_bill_pkg->edate);
2462 $description .= " ($time_period)";
2466 my @seconds = (); # for display of usage info
2469 #at least until cust_bill_pkg has "past" ranges in addition to
2470 #the "future" sdate/edate ones... see #3032
2471 my @dates = ( $self->_date );
2472 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2473 push @dates, $prev->sdate if $prev;
2474 push @dates, undef if !$prev;
2476 unless ( $part_pkg->hide_svc_detail
2477 || $cust_bill_pkg->itemdesc
2478 || $cust_bill_pkg->hidden
2479 || $is_summary && $type && $type eq 'U'
2483 warn "$me _items_cust_bill_pkg adding service details\n"
2486 my @svc_labels = map &{$escape_function}($_),
2487 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2488 push @d, @svc_labels
2489 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2490 $svc_label = $svc_labels[0];
2492 warn "$me _items_cust_bill_pkg done adding service details\n"
2495 my $lnum = $cust_main ? $cust_main->ship_locationnum
2496 : $self->prospect_main->locationnum;
2497 if ( $cust_pkg->locationnum != $lnum ) {
2498 my $loc = $cust_pkg->location_label;
2499 $loc = substr($loc, 0, $maxlength). '...'
2500 if $format eq 'latex' && length($loc) > $maxlength;
2501 push @d, &{$escape_function}($loc);
2504 # Display of seconds_since_sqlradacct:
2505 # On the invoice, when processing @detail_items, look for a field
2506 # named 'seconds'. This will contain total seconds for each
2507 # service, in the same order as @ext_description. For services
2508 # that don't support this it will show undef.
2509 if ( $conf->exists('svc_acct-usage_seconds')
2510 and ! $cust_bill_pkg->pkgpart_override ) {
2511 foreach my $cust_svc (
2512 $cust_pkg->h_cust_svc(@dates, 'I')
2515 # eval because not having any part_export_usage exports
2516 # is a fatal error, last_bill/_date because that's how
2517 # sqlradius_hour billing does it
2519 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2521 push @seconds, $sec;
2523 } #if svc_acct-usage_seconds
2527 unless ( $is_summary ) {
2528 warn "$me _items_cust_bill_pkg adding details\n"
2531 #instead of omitting details entirely in this case (unwanted side
2532 # effects), just omit CDRs
2533 $details_opt{'no_usage'} = 1
2534 if $type && $type eq 'R';
2536 push @d, $cust_bill_pkg->details(%details_opt);
2539 warn "$me _items_cust_bill_pkg calculating amount\n"
2544 $amount = $cust_bill_pkg->recur;
2545 } elsif ($type eq 'R') {
2546 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2547 } elsif ($type eq 'U') {
2548 $amount = $cust_bill_pkg->usage;
2552 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2555 if ( !$type || $type eq 'R' ) {
2557 warn "$me _items_cust_bill_pkg adding recur\n"
2560 if ( $cust_bill_pkg->hidden ) {
2561 $r->{amount} += $amount;
2562 $r->{unit_amount} += $unit_amount;
2563 push @{ $r->{ext_description} }, @d;
2566 description => $description,
2567 pkgpart => $pkgpart,
2568 pkgnum => $cust_bill_pkg->pkgnum,
2570 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2571 unit_amount => $unit_amount,
2572 quantity => $cust_bill_pkg->quantity,
2574 ext_description => \@d,
2575 svc_label => ($svc_label || ''),
2577 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2580 } else { # $type eq 'U'
2582 warn "$me _items_cust_bill_pkg adding usage\n"
2585 if ( $cust_bill_pkg->hidden ) {
2586 $u->{amount} += $amount;
2587 $u->{unit_amount} += $unit_amount,
2588 push @{ $u->{ext_description} }, @d;
2591 description => $description,
2592 pkgpart => $pkgpart,
2593 pkgnum => $cust_bill_pkg->pkgnum,
2595 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2596 unit_amount => $unit_amount,
2597 quantity => $cust_bill_pkg->quantity,
2599 ext_description => \@d,
2604 } # recurring or usage with recurring charge
2606 } else { #pkgnum tax or one-shot line item (??)
2608 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2611 if ( $cust_bill_pkg->setup != 0 ) {
2613 'description' => $desc,
2614 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2617 if ( $cust_bill_pkg->recur != 0 ) {
2619 'description' => "$desc (".
2620 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2621 time2str($date_format, $cust_bill_pkg->edate). ')',
2622 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2630 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2631 && $conf->exists('discount-show-always'));
2635 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2637 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2638 $_->{amount} =~ s/^\-0\.00$/0.00/;
2639 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2641 if $_->{amount} != 0
2642 || $discount_show_always
2643 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2644 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2648 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2655 =item _items_discounts_avail
2657 Returns an array of line item hashrefs representing available term discounts
2658 for this invoice. This makes the same assumptions that apply to term
2659 discounts in general: that the package is billed monthly, at a flat rate,
2660 with no usage charges. A prorated first month will be handled, as will
2661 a setup fee if the discount is allowed to apply to setup fees.
2665 sub _items_discounts_avail {
2668 #maybe move this method from cust_bill when quotations support discount_plans
2669 return () unless $self->can('discount_plans');
2670 my %plans = $self->discount_plans;
2672 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2673 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2677 my $plan = $plans{$months};
2679 my $term_total = sprintf('%.2f', $plan->discounted_total);
2680 my $percent = sprintf('%.0f',
2681 100 * (1 - $term_total / $plan->base_total) );
2682 my $permonth = sprintf('%.2f', $term_total / $months);
2683 my $detail = $self->mt('discount on item'). ' '.
2684 join(', ', map { "#$_" } $plan->pkgnums)
2687 # discounts for non-integer months don't work anyway
2688 $months = sprintf("%d", $months);
2691 description => $self->mt('Save [_1]% by paying for [_2] months',
2693 amount => $self->mt('[_1] ([_2] per month)',
2694 $term_total, $money_char.$permonth),
2695 ext_description => ($detail || ''),
2698 sort { $b <=> $a } keys %plans;