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 );
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 'invnum' => $self->invnum,
449 'quotationnum' => $self->quotationnum,
450 'date' => time2str($date_format, $self->_date),
451 'today' => time2str($date_format_long, $today),
452 'terms' => $self->terms,
453 'template' => $template, #params{'template'},
454 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
455 'current_charges' => sprintf("%.2f", $self->charged),
456 'duedate' => $self->due_date2str($rdate_format), #date_format?
459 'custnum' => $cust_main->display_custnum,
460 'prospectnum' => $cust_main->prospectnum,
461 'agent_custid' => &$escape_function($cust_main->agent_custid),
462 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
463 payname company address1 address2 city state zip fax
467 'ship_enable' => $conf->exists('invoice-ship_address'),
468 'unitprices' => $conf->exists('invoice-unitprice'),
469 'smallernotes' => $conf->exists('invoice-smallernotes'),
470 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
471 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
473 #layout info -- would be fancy to calc some of this and bury the template
475 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
476 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
477 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
478 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
479 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
480 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
481 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
482 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
483 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
484 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
486 # better hang on to conf_dir for a while (for old templates)
487 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
489 #these are only used when doing paged plaintext
496 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
497 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
498 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
499 # eval to avoid death for unimplemented languages
500 my $dh = eval { Date::Language->new($info{'name'}) } ||
501 Date::Language->new(); # fall back to English
502 # prototype here to silence warnings
503 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
504 # eventually use this date handle everywhere in here, too
506 my $min_sdate = 999999999999;
508 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
509 next unless $cust_bill_pkg->pkgnum > 0;
510 $min_sdate = $cust_bill_pkg->sdate
511 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
512 $max_edate = $cust_bill_pkg->edate
513 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
516 $invoice_data{'bill_period'} = '';
517 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
518 . " to " . time2str('%e %h', $max_edate)
519 if ($max_edate != 0 && $min_sdate != 999999999999);
521 $invoice_data{finance_section} = '';
522 if ( $conf->config('finance_pkgclass') ) {
524 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
525 $invoice_data{finance_section} = $pkg_class->categoryname;
527 $invoice_data{finance_amount} = '0.00';
528 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
530 my $countrydefault = $conf->config('countrydefault') || 'US';
531 foreach ( qw( address1 address2 city state zip country fax) ){
532 my $method = 'ship_'.$_;
533 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
535 foreach ( qw( contact company ) ) { #compatibility
536 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
538 $invoice_data{'ship_country'} = ''
539 if ( $invoice_data{'ship_country'} eq $countrydefault );
541 $invoice_data{'cid'} = $params{'cid'}
544 if ( $cust_main->country eq $countrydefault ) {
545 $invoice_data{'country'} = '';
547 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
551 $invoice_data{'address'} = \@address;
554 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
555 ? " (P.O. #". $cust_main->payinfo. ")"
559 push @address, $cust_main->company
560 if $cust_main->company;
561 push @address, $cust_main->address1;
562 push @address, $cust_main->address2
563 if $cust_main->address2;
565 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
566 push @address, $invoice_data{'country'}
567 if $invoice_data{'country'};
569 while (scalar(@address) < 5);
571 $invoice_data{'logo_file'} = $params{'logo_file'}
572 if $params{'logo_file'};
573 $invoice_data{'barcode_file'} = $params{'barcode_file'}
574 if $params{'barcode_file'};
575 $invoice_data{'barcode_img'} = $params{'barcode_img'}
576 if $params{'barcode_img'};
577 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
578 if $params{'barcode_cid'};
580 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
581 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
582 #my $balance_due = $self->owed + $pr_total - $cr_total;
583 my $balance_due = $self->owed + $pr_total;
585 #these are used on the summary page only
587 # the customer's current balance as shown on the invoice before this one
588 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
590 # the change in balance from that invoice to this one
591 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
593 # the sum of amount owed on all previous invoices
594 # ($pr_total is used elsewhere but not as $previous_balance)
595 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
597 # the sum of amount owed on all invoices
598 # (this is used in the summary & on the payment coupon)
599 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
601 # info from customer's last invoice before this one, for some
603 $invoice_data{'last_bill'} = {};
605 # returns the last unpaid bill, not the last bill
606 #my $last_bill = $pr_cust_bill[-1];
608 if ( $self->custnum && $self->invnum ) {
610 # THIS returns the customer's last bill before this one
611 my $last_bill = qsearchs({
612 'table' => 'cust_bill',
613 'hashref' => { 'custnum' => $self->custnum,
614 'invnum' => { op => '<', value => $self->invnum },
616 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
619 $invoice_data{'last_bill'} = {
620 '_date' => $last_bill->_date, #unformatted
621 # all we need for now
623 my (@payments, @credits);
624 # for formats that itemize previous payments
625 foreach my $cust_pay ( qsearch('cust_pay', {
626 'custnum' => $self->custnum,
627 '_date' => { op => '>=',
628 value => $last_bill->_date }
631 next if $cust_pay->_date > $self->_date;
633 '_date' => $cust_pay->_date,
634 'date' => time2str($date_format, $cust_pay->_date),
635 'payinfo' => $cust_pay->payby_payinfo_pretty,
636 'amount' => sprintf('%.2f', $cust_pay->paid),
638 # not concerned about applications
640 foreach my $cust_credit ( qsearch('cust_credit', {
641 'custnum' => $self->custnum,
642 '_date' => { op => '>=',
643 value => $last_bill->_date }
646 next if $cust_credit->_date > $self->_date;
648 '_date' => $cust_credit->_date,
649 'date' => time2str($date_format, $cust_credit->_date),
650 'creditreason'=> $cust_credit->cust_credit->reason,
651 'amount' => sprintf('%.2f', $cust_credit->amount),
654 $invoice_data{'previous_payments'} = \@payments;
655 $invoice_data{'previous_credits'} = \@credits;
660 my $summarypage = '';
661 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
664 $invoice_data{'summarypage'} = $summarypage;
666 warn "$me substituting variables in notes, footer, smallfooter\n"
669 my $tc = $self->template_conf;
670 my @include = ( [ $tc, 'notes' ],
671 [ 'invoice_', 'footer' ],
672 [ 'invoice_', 'smallfooter', ],
674 push @include, [ $tc, 'coupon', ]
675 unless $params{'no_coupon'};
677 foreach my $i (@include) {
679 my($base, $include) = @$i;
681 my $inc_file = $conf->key_orbase("$base$format$include", $template);
684 if ( $conf->exists($inc_file, $agentnum)
685 && length( $conf->config($inc_file, $agentnum) ) ) {
687 @inc_src = $conf->config($inc_file, $agentnum);
691 $inc_file = $conf->key_orbase("${base}latex$include", $template);
693 my $convert_map = $convert_maps{$format}{$include};
695 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
696 s/--\@\]/$delimiters{$format}[1]/g;
699 &$convert_map( $conf->config($inc_file, $agentnum) );
703 my $inc_tt = new Text::Template (
705 SOURCE => [ map "$_\n", @inc_src ],
706 DELIMITERS => $delimiters{$format},
707 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
709 unless ( $inc_tt->compile() ) {
710 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
711 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
715 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
717 $invoice_data{$include} =~ s/\n+$//
718 if ($format eq 'latex');
721 # let invoices use either of these as needed
722 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
723 ? $cust_main->payinfo : '';
724 $invoice_data{'po_line'} =
725 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
726 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
729 my %money_chars = ( 'latex' => '',
730 'html' => $conf->config('money_char') || '$',
733 my $money_char = $money_chars{$format};
735 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
736 'html' => $conf->config('money_char') || '$',
739 my $other_money_char = $other_money_chars{$format};
740 $invoice_data{'dollar'} = $other_money_char;
742 my %minus_signs = ( 'latex' => '$-$',
744 'template' => '- ' );
745 my $minus = $minus_signs{$format};
747 my @detail_items = ();
748 my @total_items = ();
752 $invoice_data{'detail_items'} = \@detail_items;
753 $invoice_data{'total_items'} = \@total_items;
754 $invoice_data{'buf'} = \@buf;
755 $invoice_data{'sections'} = \@sections;
757 warn "$me generating sections\n"
760 # Previous Charges section
761 # subtotal is the first return value from $self->previous
762 my $previous_section = { 'description' => $self->mt('Previous Charges'),
763 'subtotal' => $other_money_char.
764 sprintf('%.2f', $pr_total),
765 'summarized' => '', #why? $summarypage ? 'Y' : '',
767 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
768 join(' / ', map { $cust_main->balance_date_range(@$_) }
769 $self->_prior_month30s
771 if $conf->exists('invoice_include_aging');
774 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
775 'subtotal' => $taxtotal, # adjusted below
777 my $tax_weight = _pkg_category($tax_section->{description})
778 ? _pkg_category($tax_section->{description})->weight
780 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
781 $tax_section->{'sort_weight'} = $tax_weight;
785 my $adjust_section = {
786 'description' => $self->mt('Credits, Payments, and Adjustments'),
787 'adjust_section' => 1,
788 'subtotal' => 0, # adjusted below
790 my $adjust_weight = _pkg_category($adjust_section->{description})
791 ? _pkg_category($adjust_section->{description})->weight
793 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
794 $adjust_section->{'sort_weight'} = $adjust_weight;
796 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
797 my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum);
798 $invoice_data{'multisection'} = $multisection;
799 my $late_sections = [];
800 my $extra_sections = [];
801 my $extra_lines = ();
803 my $default_section = { 'description' => '',
808 if ( $multisection ) {
809 ($extra_sections, $extra_lines) =
810 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
811 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
812 && $self->can('_items_extra_usage_sections');
814 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
816 push @detail_items, @$extra_lines if $extra_lines;
818 $self->_items_sections( $late_sections, # this could stand a refactor
820 $escape_function_nonbsp,
824 if ( $conf->exists('svc_phone_sections')
825 && $self->can('_items_svc_phone_sections')
828 my ($phone_sections, $phone_lines) =
829 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
830 push @{$late_sections}, @$phone_sections;
831 push @detail_items, @$phone_lines;
833 if ( $conf->exists('voip-cust_accountcode_cdr')
834 && $cust_main->accountcode_cdr
835 && $self->can('_items_accountcode_cdr')
838 my ($accountcode_section, $accountcode_lines) =
839 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
840 if ( scalar(@$accountcode_lines) ) {
841 push @{$late_sections}, $accountcode_section;
842 push @detail_items, @$accountcode_lines;
845 } else {# not multisection
846 # make a default section
847 push @sections, $default_section;
848 # and calculate the finance charge total, since it won't get done otherwise.
849 # XXX possibly other totals?
850 # XXX possibly finance_pkgclass should not be used in this manner?
851 if ( $conf->exists('finance_pkgclass') ) {
853 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
854 if ( grep { $_->section eq $invoice_data{finance_section} }
855 $cust_bill_pkg->cust_bill_pkg_display ) {
856 # I think these are always setup fees, but just to be sure...
857 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
860 $invoice_data{finance_amount} =
861 sprintf('%.2f', sum( @finance_charges ) || 0);
865 # previous invoice balances in the Previous Charges section if there
866 # is one, otherwise in the main detail section
867 if ( $self->can('_items_previous') &&
868 $self->enable_previous &&
869 ! $conf->exists('previous_balance-summary_only') ) {
871 warn "$me adding previous balances\n"
874 foreach my $line_item ( $self->_items_previous ) {
877 ext_description => [],
879 $detail->{'ref'} = $line_item->{'pkgnum'};
880 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
881 $detail->{'quantity'} = 1;
882 $detail->{'section'} = $multisection ? $previous_section
884 $detail->{'description'} = &$escape_function($line_item->{'description'});
885 if ( exists $line_item->{'ext_description'} ) {
886 @{$detail->{'ext_description'}} = map {
887 &$escape_function($_);
888 } @{$line_item->{'ext_description'}};
890 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
891 $line_item->{'amount'};
892 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
894 push @detail_items, $detail;
895 push @buf, [ $detail->{'description'},
896 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
902 if ( @pr_cust_bill && $self->enable_previous ) {
903 push @buf, ['','-----------'];
904 push @buf, [ $self->mt('Total Previous Balance'),
905 $money_char. sprintf("%10.2f", $pr_total) ];
909 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
910 warn "$me adding DID summary\n"
913 my ($didsummary,$minutes) = $self->_did_summary;
914 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
916 { 'description' => $didsummary_desc,
917 'ext_description' => [ $didsummary, $minutes ],
921 foreach my $section (@sections, @$late_sections) {
923 warn "$me adding section \n". Dumper($section)
926 # begin some normalization
927 $section->{'subtotal'} = $section->{'amount'}
929 && !exists($section->{subtotal})
930 && exists($section->{amount});
932 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
933 if ( $invoice_data{finance_section} &&
934 $section->{'description'} eq $invoice_data{finance_section} );
936 $section->{'subtotal'} = $other_money_char.
937 sprintf('%.2f', $section->{'subtotal'})
940 # continue some normalization
941 $section->{'amount'} = $section->{'subtotal'}
945 if ( $section->{'description'} ) {
946 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
951 warn "$me setting options\n"
955 $options{'section'} = $section if $multisection;
956 $options{'format'} = $format;
957 $options{'escape_function'} = $escape_function;
958 $options{'no_usage'} = 1 unless $unsquelched;
959 $options{'unsquelched'} = $unsquelched;
960 $options{'summary_page'} = $summarypage;
961 $options{'skip_usage'} =
962 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
963 $options{'multisection'} = $multisection;
965 warn "$me searching for line items\n"
968 foreach my $line_item ( $self->_items_pkg(%options) ) {
970 warn "$me adding line item $line_item\n"
974 ext_description => [],
976 $detail->{'ref'} = $line_item->{'pkgnum'};
977 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
978 $detail->{'quantity'} = $line_item->{'quantity'};
979 $detail->{'section'} = $section;
980 $detail->{'description'} = &$escape_function($line_item->{'description'});
981 if ( exists $line_item->{'ext_description'} ) {
982 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
984 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
985 $line_item->{'amount'};
986 if ( exists $line_item->{'unit_amount'} ) {
987 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
988 $line_item->{'unit_amount'};
990 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
992 $detail->{'sdate'} = $line_item->{'sdate'};
993 $detail->{'edate'} = $line_item->{'edate'};
994 $detail->{'seconds'} = $line_item->{'seconds'};
995 $detail->{'svc_label'} = $line_item->{'svc_label'};
997 push @detail_items, $detail;
998 push @buf, ( [ $detail->{'description'},
999 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
1001 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
1005 if ( $section->{'description'} ) {
1006 push @buf, ( ['','-----------'],
1007 [ $section->{'description'}. ' sub-total',
1008 $section->{'subtotal'} # already formatted this
1017 $invoice_data{current_less_finance} =
1018 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1020 # create a major section for previous balance if we have major sections,
1021 # or if previous_section is in summary form
1022 if ( ( $multisection && $self->enable_previous )
1023 || $conf->exists('previous_balance-summary_only') )
1025 unshift @sections, $previous_section if $pr_total;
1028 warn "$me adding taxes\n"
1031 my @items_tax = $self->_items_tax;
1032 foreach my $tax ( @items_tax ) {
1034 $taxtotal += $tax->{'amount'};
1036 my $description = &$escape_function( $tax->{'description'} );
1037 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1039 if ( $multisection ) {
1041 my $money = $old_latex ? '' : $money_char;
1042 push @detail_items, {
1043 ext_description => [],
1046 description => $description,
1047 amount => $money. $amount,
1049 section => $tax_section,
1054 push @total_items, {
1055 'total_item' => $description,
1056 'total_amount' => $other_money_char. $amount,
1061 push @buf,[ $description,
1062 $money_char. $amount,
1069 $total->{'total_item'} = $self->mt('Sub-total');
1070 $total->{'total_amount'} =
1071 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1073 if ( $multisection ) {
1074 $tax_section->{'subtotal'} = $other_money_char.
1075 sprintf('%.2f', $taxtotal);
1076 $tax_section->{'pretotal'} = 'New charges sub-total '.
1077 $total->{'total_amount'};
1078 push @sections, $tax_section if $taxtotal;
1080 unshift @total_items, $total;
1083 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1085 push @buf,['','-----------'];
1086 push @buf,[$self->mt(
1087 (!$self->enable_previous)
1089 : 'Total New Charges'
1091 $money_char. sprintf("%10.2f",$self->charged) ];
1099 my %embolden_functions = (
1100 'latex' => sub { return '\textbf{'. shift(). '}' },
1101 'html' => sub { return '<b>'. shift(). '</b>' },
1102 'template' => sub { shift },
1104 my $embolden_function = $embolden_functions{$format};
1106 if ( $self->can('_items_total') ) { # quotations
1108 $self->_items_total(\@total_items);
1110 foreach ( @total_items ) {
1111 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1112 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1113 $_->{'total_amount'}
1117 } else { #normal invoice case
1119 # calculate total, possibly including total owed on previous
1123 $item = $conf->config('previous_balance-exclude_from_total')
1124 || 'Total New Charges'
1125 if $conf->exists('previous_balance-exclude_from_total');
1126 my $amount = $self->charged;
1127 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1128 $amount += $pr_total;
1131 $total->{'total_item'} = &$embolden_function($self->mt($item));
1132 $total->{'total_amount'} =
1133 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1134 if ( $multisection ) {
1135 if ( $adjust_section->{'sort_weight'} ) {
1136 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1137 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1139 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1140 $other_money_char. sprintf('%.2f', $self->charged );
1143 push @total_items, $total;
1145 push @buf,['','-----------'];
1148 sprintf( '%10.2f', $amount )
1152 # if we're showing previous invoices, also show previous
1153 # credits and payments
1154 if ( $self->enable_previous
1155 and $self->can('_items_credits')
1156 and $self->can('_items_payments') )
1158 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1161 my $credittotal = 0;
1162 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1165 $total->{'total_item'} = &$escape_function($credit->{'description'});
1166 $credittotal += $credit->{'amount'};
1167 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1168 $adjusttotal += $credit->{'amount'};
1169 if ( $multisection ) {
1170 my $money = $old_latex ? '' : $money_char;
1171 push @detail_items, {
1172 ext_description => [],
1175 description => &$escape_function($credit->{'description'}),
1176 amount => $money. $credit->{'amount'},
1178 section => $adjust_section,
1181 push @total_items, $total;
1185 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1188 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1189 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1193 my $paymenttotal = 0;
1194 foreach my $payment ( $self->_items_payments ) {
1196 $total->{'total_item'} = &$escape_function($payment->{'description'});
1197 $paymenttotal += $payment->{'amount'};
1198 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1199 $adjusttotal += $payment->{'amount'};
1200 if ( $multisection ) {
1201 my $money = $old_latex ? '' : $money_char;
1202 push @detail_items, {
1203 ext_description => [],
1206 description => &$escape_function($payment->{'description'}),
1207 amount => $money. $payment->{'amount'},
1209 section => $adjust_section,
1212 push @total_items, $total;
1214 push @buf, [ $payment->{'description'},
1215 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1218 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1220 if ( $multisection ) {
1221 $adjust_section->{'subtotal'} = $other_money_char.
1222 sprintf('%.2f', $adjusttotal);
1223 push @sections, $adjust_section
1224 unless $adjust_section->{sort_weight};
1227 # create Balance Due message
1230 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1231 $total->{'total_amount'} =
1232 &$embolden_function(
1233 $other_money_char. sprintf('%.2f', #why? $summarypage
1234 # ? $self->charged +
1235 # $self->billing_balance
1237 $self->owed + $pr_total
1240 if ( $multisection && !$adjust_section->{sort_weight} ) {
1241 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1242 $total->{'total_amount'};
1244 push @total_items, $total;
1246 push @buf,['','-----------'];
1247 push @buf,[$self->balance_due_msg, $money_char.
1248 sprintf("%10.2f", $balance_due ) ];
1251 if ( $conf->exists('previous_balance-show_credit')
1252 and $cust_main->balance < 0 ) {
1253 my $credit_total = {
1254 'total_item' => &$embolden_function($self->credit_balance_msg),
1255 'total_amount' => &$embolden_function(
1256 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1259 if ( $multisection ) {
1260 $adjust_section->{'posttotal'} .= $newline_token .
1261 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1264 push @total_items, $credit_total;
1266 push @buf,['','-----------'];
1267 push @buf,[$self->credit_balance_msg, $money_char.
1268 sprintf("%10.2f", -$cust_main->balance ) ];
1272 } #end of default total adding ! can('_items_total')
1274 if ( $multisection ) {
1275 if ( $conf->exists('svc_phone_sections')
1276 && $self->can('_items_svc_phone_sections')
1280 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1281 $total->{'total_amount'} =
1282 &$embolden_function(
1283 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1285 my $last_section = pop @sections;
1286 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1287 $total->{'total_amount'};
1288 push @sections, $last_section;
1290 push @sections, @$late_sections
1294 # make a discounts-available section, even without multisection
1295 if ( $conf->exists('discount-show_available')
1296 and my @discounts_avail = $self->_items_discounts_avail ) {
1297 my $discount_section = {
1298 'description' => $self->mt('Discounts Available'),
1303 push @sections, $discount_section;
1304 push @detail_items, map { +{
1305 'ref' => '', #should this be something else?
1306 'section' => $discount_section,
1307 'description' => &$escape_function( $_->{description} ),
1308 'amount' => $money_char . &$escape_function( $_->{amount} ),
1309 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1310 } } @discounts_avail;
1313 # debugging hook: call this with 'diag' => 1 to just get a hash of
1314 # the invoice variables
1315 return \%invoice_data if ( $params{'diag'} );
1317 # All sections and items are built; now fill in templates.
1318 my @includelist = ();
1319 push @includelist, 'summary' if $summarypage;
1320 foreach my $include ( @includelist ) {
1322 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1325 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1327 @inc_src = $conf->config($inc_file, $agentnum);
1331 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1333 my $convert_map = $convert_maps{$format}{$include};
1335 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1336 s/--\@\]/$delimiters{$format}[1]/g;
1339 &$convert_map( $conf->config($inc_file, $agentnum) );
1343 my $inc_tt = new Text::Template (
1345 SOURCE => [ map "$_\n", @inc_src ],
1346 DELIMITERS => $delimiters{$format},
1347 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1349 unless ( $inc_tt->compile() ) {
1350 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1351 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1355 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1357 $invoice_data{$include} =~ s/\n+$//
1358 if ($format eq 'latex');
1363 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1364 /invoice_lines\((\d*)\)/;
1365 $invoice_lines += $1 || scalar(@buf);
1368 die "no invoice_lines() functions in template?"
1369 if ( $format eq 'template' && !$wasfunc );
1371 if ($format eq 'template') {
1373 if ( $invoice_lines ) {
1374 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1375 $invoice_data{'total_pages'}++
1376 if scalar(@buf) % $invoice_lines;
1379 #setup subroutine for the template
1380 $invoice_data{invoice_lines} = sub {
1381 my $lines = shift || scalar(@buf);
1393 push @collect, split("\n",
1394 $text_template->fill_in( HASH => \%invoice_data )
1396 $invoice_data{'page'}++;
1398 map "$_\n", @collect;
1400 } else { # this is where we actually create the invoice
1402 warn "filling in template for invoice ". $self->invnum. "\n"
1404 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1407 $text_template->fill_in(HASH => \%invoice_data);
1411 sub notice_name { '('.shift->table.')'; }
1413 sub template_conf { 'invoice_'; }
1415 # helper routine for generating date ranges
1416 sub _prior_month30s {
1419 [ 1, 2592000 ], # 0-30 days ago
1420 [ 2592000, 5184000 ], # 30-60 days ago
1421 [ 5184000, 7776000 ], # 60-90 days ago
1422 [ 7776000, 0 ], # 90+ days ago
1425 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1426 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1431 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1433 Returns an postscript invoice, as a scalar.
1435 Options can be passed as a hashref (recommended) or as a list of time, template
1436 and then any key/value pairs for any other options.
1438 I<time> an optional value used to control the printing of overdue messages. The
1439 default is now. It isn't the date of the invoice; that's the `_date' field.
1440 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1441 L<Time::Local> and L<Date::Parse> for conversion functions.
1443 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1450 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1451 my $ps = generate_ps($file);
1453 unlink($barcodefile) if $barcodefile;
1458 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1460 Returns an PDF invoice, as a scalar.
1462 Options can be passed as a hashref (recommended) or as a list of time, template
1463 and then any key/value pairs for any other options.
1465 I<time> an optional value used to control the printing of overdue messages. The
1466 default is now. It isn't the date of the invoice; that's the `_date' field.
1467 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1468 L<Time::Local> and L<Date::Parse> for conversion functions.
1470 I<template>, if specified, is the name of a suffix for alternate invoices.
1472 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1479 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1480 my $pdf = generate_pdf($file);
1482 unlink($barcodefile) if $barcodefile;
1487 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1489 Returns an HTML invoice, as a scalar.
1491 I<time> an optional value used to control the printing of overdue messages. The
1492 default is now. It isn't the date of the invoice; that's the `_date' field.
1493 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1494 L<Time::Local> and L<Date::Parse> for conversion functions.
1496 I<template>, if specified, is the name of a suffix for alternate invoices.
1498 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1500 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1501 when emailing the invoice as part of a multipart/related MIME email.
1509 %params = %{ shift() };
1511 $params{'time'} = shift;
1512 $params{'template'} = shift;
1513 $params{'cid'} = shift;
1516 $params{'format'} = 'html';
1518 $self->print_generic( %params );
1521 # quick subroutine for print_latex
1523 # There are ten characters that LaTeX treats as special characters, which
1524 # means that they do not simply typeset themselves:
1525 # # $ % & ~ _ ^ \ { }
1527 # TeX ignores blanks following an escaped character; if you want a blank (as
1528 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1532 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1533 $value =~ s/([<>])/\$$1\$/g;
1539 encode_entities($value);
1543 sub _html_escape_nbsp {
1544 my $value = _html_escape(shift);
1545 $value =~ s/ +/ /g;
1549 #utility methods for print_*
1551 sub _translate_old_latex_format {
1552 warn "_translate_old_latex_format called\n"
1559 if ( $line =~ /^%%Detail\s*$/ ) {
1561 push @template, q![@--!,
1562 q! foreach my $_tr_line (@detail_items) {!,
1563 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1564 q! $_tr_line->{'description'} .= !,
1565 q! "\\tabularnewline\n~~".!,
1566 q! join( "\\tabularnewline\n~~",!,
1567 q! @{$_tr_line->{'ext_description'}}!,
1571 while ( ( my $line_item_line = shift )
1572 !~ /^%%EndDetail\s*$/ ) {
1573 $line_item_line =~ s/'/\\'/g; # nice LTS
1574 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1575 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1576 push @template, " \$OUT .= '$line_item_line';";
1579 push @template, '}',
1582 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1584 push @template, '[@--',
1585 ' foreach my $_tr_line (@total_items) {';
1587 while ( ( my $total_item_line = shift )
1588 !~ /^%%EndTotalDetails\s*$/ ) {
1589 $total_item_line =~ s/'/\\'/g; # nice LTS
1590 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1591 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1592 push @template, " \$OUT .= '$total_item_line';";
1595 push @template, '}',
1599 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1600 push @template, $line;
1606 warn "$_\n" foreach @template;
1614 my $conf = $self->conf;
1616 #check for an invoice-specific override
1617 return $self->invoice_terms if $self->invoice_terms;
1619 #check for a customer- specific override
1620 my $cust_main = $self->cust_main;
1621 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1623 #use configured default
1624 $conf->config('invoice_default_terms') || '';
1630 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1631 $duedate = $self->_date() + ( $1 * 86400 );
1638 $self->due_date ? time2str(shift, $self->due_date) : '';
1641 sub balance_due_msg {
1643 my $msg = $self->mt('Balance Due');
1644 return $msg unless $self->terms;
1645 if ( $self->due_date ) {
1646 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1647 $self->due_date2str($date_format);
1648 } elsif ( $self->terms ) {
1649 $msg .= ' - '. $self->terms;
1654 sub balance_due_date {
1656 my $conf = $self->conf;
1658 if ( $conf->exists('invoice_default_terms')
1659 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1660 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1665 sub credit_balance_msg {
1667 $self->mt('Credit Balance Remaining')
1672 Returns a string with the date, for example: "3/20/2008"
1678 time2str($date_format, $self->_date);
1681 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1683 Generate section information for all items appearing on this invoice.
1684 This will only be called for multi-section invoices.
1686 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1687 related display records (L<FS::cust_bill_pkg_display>) and organize
1688 them into two groups ("early" and "late" according to whether they come
1689 before or after the total), then into sections. A subtotal is calculated
1692 Section descriptions are returned in sort weight order. Each consists
1693 of a hash containing:
1695 description: the package category name, escaped
1696 subtotal: the total charges in that section
1697 tax_section: a flag indicating that the section contains only tax charges
1698 summarized: same as tax_section, for some reason
1699 sort_weight: the package category's sort weight
1701 If 'condense' is set on the display record, it also contains everything
1702 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1703 coderefs to generate parts of the invoice. This is not advised.
1707 LATE: an arrayref to push the "late" section hashes onto. The "early"
1708 group is simply returned from the method.
1710 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1711 Turning this on has the following effects:
1712 - Ignores display items with the 'summary' flag.
1713 - Combines all items into the "early" group.
1714 - Creates sections for all non-disabled package categories, even if they
1715 have no charges on this invoice, as well as a section with no name.
1717 ESCAPE: an escape function to use for section titles.
1719 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1720 sorted list. If there are any of these, section subtotals exclude
1723 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1724 passed through to C<_condense_section()>.
1728 use vars qw(%pkg_category_cache);
1729 sub _items_sections {
1732 my $summarypage = shift;
1734 my $extra_sections = shift;
1738 my %late_subtotal = ();
1741 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1744 my $usage = $cust_bill_pkg->usage;
1746 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1747 next if ( $display->summary && $summarypage );
1749 my $section = $display->section;
1750 my $type = $display->type;
1752 $not_tax{$section} = 1
1753 unless $cust_bill_pkg->pkgnum == 0;
1755 # there's actually a very important piece of logic buried in here:
1756 # incrementing $late_subtotal{$section} CREATES
1757 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1758 # to define the list of late sections, and likewise keys(%subtotal).
1759 # When _items_cust_bill_pkg is called to generate line items for
1760 # real, it will be called with 'section' => $section for each
1762 if ( $display->post_total && !$summarypage ) {
1763 if (! $type || $type eq 'S') {
1764 $late_subtotal{$section} += $cust_bill_pkg->setup
1765 if $cust_bill_pkg->setup != 0
1766 || $cust_bill_pkg->setup_show_zero;
1770 $late_subtotal{$section} += $cust_bill_pkg->recur
1771 if $cust_bill_pkg->recur != 0
1772 || $cust_bill_pkg->recur_show_zero;
1775 if ($type && $type eq 'R') {
1776 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1777 if $cust_bill_pkg->recur != 0
1778 || $cust_bill_pkg->recur_show_zero;
1781 if ($type && $type eq 'U') {
1782 $late_subtotal{$section} += $usage
1783 unless scalar(@$extra_sections);
1788 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1790 if (! $type || $type eq 'S') {
1791 $subtotal{$section} += $cust_bill_pkg->setup
1792 if $cust_bill_pkg->setup != 0
1793 || $cust_bill_pkg->setup_show_zero;
1797 $subtotal{$section} += $cust_bill_pkg->recur
1798 if $cust_bill_pkg->recur != 0
1799 || $cust_bill_pkg->recur_show_zero;
1802 if ($type && $type eq 'R') {
1803 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1804 if $cust_bill_pkg->recur != 0
1805 || $cust_bill_pkg->recur_show_zero;
1808 if ($type && $type eq 'U') {
1809 $subtotal{$section} += $usage
1810 unless scalar(@$extra_sections);
1819 %pkg_category_cache = ();
1821 push @$late, map { { 'description' => &{$escape}($_),
1822 'subtotal' => $late_subtotal{$_},
1824 'sort_weight' => ( _pkg_category($_)
1825 ? _pkg_category($_)->weight
1828 ((_pkg_category($_) && _pkg_category($_)->condense)
1829 ? $self->_condense_section($format)
1833 sort _sectionsort keys %late_subtotal;
1836 if ( $summarypage ) {
1837 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1838 map { $_->categoryname } qsearch('pkg_category', {});
1839 push @sections, '' if exists($subtotal{''});
1841 @sections = keys %subtotal;
1844 my @early = map { { 'description' => &{$escape}($_),
1845 'subtotal' => $subtotal{$_},
1846 'summarized' => $not_tax{$_} ? '' : 'Y',
1847 'tax_section' => $not_tax{$_} ? '' : 'Y',
1848 'sort_weight' => ( _pkg_category($_)
1849 ? _pkg_category($_)->weight
1852 ((_pkg_category($_) && _pkg_category($_)->condense)
1853 ? $self->_condense_section($format)
1858 push @early, @$extra_sections if $extra_sections;
1860 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1864 #helper subs for above
1867 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1871 my $categoryname = shift;
1872 $pkg_category_cache{$categoryname} ||=
1873 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1876 my %condensed_format = (
1877 'label' => [ qw( Description Qty Amount ) ],
1879 sub { shift->{description} },
1880 sub { shift->{quantity} },
1881 sub { my($href, %opt) = @_;
1882 ($opt{dollar} || ''). $href->{amount};
1885 'align' => [ qw( l r r ) ],
1886 'span' => [ qw( 5 1 1 ) ], # unitprices?
1887 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1890 sub _condense_section {
1891 my ( $self, $format ) = ( shift, shift );
1893 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1894 qw( description_generator
1897 total_line_generator
1902 sub _condensed_generator_defaults {
1903 my ( $self, $format ) = ( shift, shift );
1904 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1913 sub _condensed_header_generator {
1914 my ( $self, $format ) = ( shift, shift );
1916 my ( $f, $prefix, $suffix, $separator, $column ) =
1917 _condensed_generator_defaults($format);
1919 if ($format eq 'latex') {
1920 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1921 $suffix = "\\\\\n\\hline";
1924 sub { my ($d,$a,$s,$w) = @_;
1925 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1927 } elsif ( $format eq 'html' ) {
1928 $prefix = '<th></th>';
1932 sub { my ($d,$a,$s,$w) = @_;
1933 return qq!<th align="$html_align{$a}">$d</th>!;
1941 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1943 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1946 $prefix. join($separator, @result). $suffix;
1951 sub _condensed_description_generator {
1952 my ( $self, $format ) = ( shift, shift );
1954 my ( $f, $prefix, $suffix, $separator, $column ) =
1955 _condensed_generator_defaults($format);
1957 my $money_char = '$';
1958 if ($format eq 'latex') {
1959 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1961 $separator = " & \n";
1963 sub { my ($d,$a,$s,$w) = @_;
1964 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1966 $money_char = '\\dollar';
1967 }elsif ( $format eq 'html' ) {
1968 $prefix = '"><td align="center"></td>';
1972 sub { my ($d,$a,$s,$w) = @_;
1973 return qq!<td align="$html_align{$a}">$d</td>!;
1975 #$money_char = $conf->config('money_char') || '$';
1976 $money_char = ''; # this is madness
1984 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1986 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1988 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1989 map { $f->{$_}->[$i] } qw(align span width)
1993 $prefix. join( $separator, @result ). $suffix;
1998 sub _condensed_total_generator {
1999 my ( $self, $format ) = ( shift, shift );
2001 my ( $f, $prefix, $suffix, $separator, $column ) =
2002 _condensed_generator_defaults($format);
2005 if ($format eq 'latex') {
2008 $separator = " & \n";
2010 sub { my ($d,$a,$s,$w) = @_;
2011 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2013 }elsif ( $format eq 'html' ) {
2017 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2019 sub { my ($d,$a,$s,$w) = @_;
2020 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2029 # my $r = &{$f->{fields}->[$i]}(@args);
2030 # $r .= ' Total' unless $i;
2032 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2034 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2035 map { $f->{$_}->[$i] } qw(align span width)
2039 $prefix. join( $separator, @result ). $suffix;
2044 =item total_line_generator FORMAT
2046 Returns a coderef used for generation of invoice total line items for this
2047 usage_class. FORMAT is either html or latex
2051 # should not be used: will have issues with hash element names (description vs
2052 # total_item and amount vs total_amount -- another array of functions?
2054 sub _condensed_total_line_generator {
2055 my ( $self, $format ) = ( shift, shift );
2057 my ( $f, $prefix, $suffix, $separator, $column ) =
2058 _condensed_generator_defaults($format);
2061 if ($format eq 'latex') {
2064 $separator = " & \n";
2066 sub { my ($d,$a,$s,$w) = @_;
2067 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2069 }elsif ( $format eq 'html' ) {
2073 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2075 sub { my ($d,$a,$s,$w) = @_;
2076 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2085 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2087 &{$column}( &{$f->{fields}->[$i]}(@args),
2088 map { $f->{$_}->[$i] } qw(align span width)
2092 $prefix. join( $separator, @result ). $suffix;
2097 # sub _items { # seems to be unused
2100 # #my @display = scalar(@_)
2102 # # : qw( _items_previous _items_pkg );
2103 # # #: qw( _items_pkg );
2104 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2105 # my @display = qw( _items_previous _items_pkg );
2108 # foreach my $display ( @display ) {
2109 # push @b, $self->$display(@_);
2114 =item _items_pkg [ OPTIONS ]
2116 Return line item hashes for each package item on this invoice. Nearly
2119 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2121 The only OPTIONS accepted is 'section', which may point to a hashref
2122 with a key named 'condensed', which may have a true value. If it
2123 does, this method tries to merge identical items into items with
2124 'quantity' equal to the number of items (not the sum of their
2125 separate quantities, for some reason).
2131 grep { $_->pkgnum } $self->cust_bill_pkg;
2138 warn "$me _items_pkg searching for all package line items\n"
2141 my @cust_bill_pkg = $self->_items_nontax;
2143 warn "$me _items_pkg filtering line items\n"
2145 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2147 if ($options{section} && $options{section}->{condensed}) {
2149 warn "$me _items_pkg condensing section\n"
2153 local $Storable::canonical = 1;
2154 foreach ( @items ) {
2156 delete $item->{ref};
2157 delete $item->{ext_description};
2158 my $key = freeze($item);
2159 $itemshash{$key} ||= 0;
2160 $itemshash{$key} ++; # += $item->{quantity};
2162 @items = sort { $a->{description} cmp $b->{description} }
2163 map { my $i = thaw($_);
2164 $i->{quantity} = $itemshash{$_};
2166 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2172 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2179 return 0 unless $a->itemdesc cmp $b->itemdesc;
2180 return -1 if $b->itemdesc eq 'Tax';
2181 return 1 if $a->itemdesc eq 'Tax';
2182 return -1 if $b->itemdesc eq 'Other surcharges';
2183 return 1 if $a->itemdesc eq 'Other surcharges';
2184 $a->itemdesc cmp $b->itemdesc;
2189 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2190 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2192 if ( $self->conf->exists('always_show_tax') ) {
2193 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2194 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2196 { 'description' => $itemdesc,
2203 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2205 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2206 list of hashrefs describing the line items they generate on the invoice.
2208 OPTIONS may include:
2210 format: the invoice format.
2212 escape_function: the function used to escape strings.
2214 DEPRECATED? (expensive, mostly unused?)
2215 format_function: the function used to format CDRs.
2217 section: a hashref containing 'description'; if this is present,
2218 cust_bill_pkg_display records not belonging to this section are
2221 multisection: a flag indicating that this is a multisection invoice,
2222 which does something complicated.
2224 Returns a list of hashrefs, each of which may contain:
2226 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2227 ext_description, which is an arrayref of detail lines to show below
2232 sub _items_cust_bill_pkg {
2234 my $conf = $self->conf;
2235 my $cust_bill_pkgs = shift;
2238 my $format = $opt{format} || '';
2239 my $escape_function = $opt{escape_function} || sub { shift };
2240 my $format_function = $opt{format_function} || '';
2241 my $no_usage = $opt{no_usage} || '';
2242 my $unsquelched = $opt{unsquelched} || ''; #unused
2243 my $section = $opt{section}->{description} if $opt{section};
2244 my $summary_page = $opt{summary_page} || ''; #unused
2245 my $multisection = $opt{multisection} || '';
2246 my $discount_show_always = 0;
2248 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2250 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2251 # and location labels
2254 my ($s, $r, $u) = ( undef, undef, undef );
2255 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2258 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2259 if ( $_ && !$cust_bill_pkg->hidden ) {
2260 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2261 $_->{amount} =~ s/^\-0\.00$/0.00/;
2262 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2264 if $_->{amount} != 0
2265 || $discount_show_always
2266 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2267 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2273 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2274 ? $cust_bill_pkg->cust_bill_pkg_display
2275 : ( $cust_bill_pkg );
2277 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2278 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2281 foreach my $display ( grep { defined($section)
2282 ? $_->section eq $section
2285 grep { !$_->summary || $multisection }
2286 @cust_bill_pkg_display
2290 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2291 $display->billpkgdisplaynum. "\n"
2294 my $type = $display->type;
2296 my $desc = $cust_bill_pkg->desc( $cust_main ? $cust_main->locale : '' );
2297 $desc = substr($desc, 0, $maxlength). '...'
2298 if $format eq 'latex' && length($desc) > $maxlength;
2300 my %details_opt = ( 'format' => $format,
2301 'escape_function' => $escape_function,
2302 'format_function' => $format_function,
2303 'no_usage' => $opt{'no_usage'},
2306 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2308 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2311 if ( $cust_bill_pkg->setup != 0 ) {
2312 my $description = $desc;
2313 $description .= ' Setup'
2314 if $cust_bill_pkg->recur != 0
2315 || $discount_show_always
2316 || $cust_bill_pkg->recur_show_zero;
2318 'description' => $description,
2319 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2322 if ( $cust_bill_pkg->recur != 0 ) {
2324 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2325 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2329 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2331 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2334 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2336 # which pkgpart to show for display purposes?
2337 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2339 # start/end dates for invoice formats that do nonstandard
2341 my %item_dates = ();
2342 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2343 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2345 if ( (!$type || $type eq 'S')
2346 && ( $cust_bill_pkg->setup != 0
2347 || $cust_bill_pkg->setup_show_zero
2352 warn "$me _items_cust_bill_pkg adding setup\n"
2355 my $description = $desc;
2356 $description .= ' Setup'
2357 if $cust_bill_pkg->recur != 0
2358 || $discount_show_always
2359 || $cust_bill_pkg->recur_show_zero;
2363 unless ( $cust_pkg->part_pkg->hide_svc_detail
2364 || $cust_bill_pkg->hidden )
2367 my @svc_labels = map &{$escape_function}($_),
2368 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2369 push @d, @svc_labels
2370 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2371 $svc_label = $svc_labels[0];
2373 my $lnum = $cust_main ? $cust_main->ship_locationnum
2374 : $self->prospect_main->locationnum;
2375 if ( ! $cust_pkg->locationnum or $cust_pkg->locationnum != $lnum ) {
2376 my $loc = $cust_pkg->location_label;
2377 $loc = substr($loc, 0, $maxlength). '...'
2378 if $format eq 'latex' && length($loc) > $maxlength;
2379 push @d, &{$escape_function}($loc);
2382 } #unless hiding service details
2384 push @d, $cust_bill_pkg->details(%details_opt)
2385 if $cust_bill_pkg->recur == 0;
2387 if ( $cust_bill_pkg->hidden ) {
2388 $s->{amount} += $cust_bill_pkg->setup;
2389 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2390 push @{ $s->{ext_description} }, @d;
2394 description => $description,
2395 pkgpart => $pkgpart,
2396 pkgnum => $cust_bill_pkg->pkgnum,
2397 amount => $cust_bill_pkg->setup,
2398 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2399 unit_amount => $cust_bill_pkg->unitsetup,
2400 quantity => $cust_bill_pkg->quantity,
2401 ext_description => \@d,
2402 svc_label => ($svc_label || ''),
2408 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2410 $cust_bill_pkg->recur != 0
2411 || $cust_bill_pkg->setup == 0
2412 || $discount_show_always
2413 || $cust_bill_pkg->recur_show_zero
2418 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2421 my $is_summary = $display->summary;
2422 my $description = ($is_summary && $type && $type eq 'U')
2423 ? "Usage charges" : $desc;
2425 my $part_pkg = $cust_pkg->part_pkg;
2427 #pry be a bit more efficient to look some of this conf stuff up
2430 $conf->exists('disable_line_item_date_ranges')
2431 || $part_pkg->option('disable_line_item_date_ranges',1)
2432 || ! $cust_bill_pkg->sdate
2433 || ! $cust_bill_pkg->edate
2436 my $date_style = '';
2437 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2440 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2441 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2444 if ( defined($date_style) && $date_style eq 'month_of' ) {
2445 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2446 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2447 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2450 $desc .= ' ' unless $desc =~ /\s$/;
2451 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2453 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2454 " - ". time2str($date_format, $cust_bill_pkg->edate);
2456 $description .= " ($time_period)";
2460 my @seconds = (); # for display of usage info
2463 #at least until cust_bill_pkg has "past" ranges in addition to
2464 #the "future" sdate/edate ones... see #3032
2465 my @dates = ( $self->_date );
2466 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2467 push @dates, $prev->sdate if $prev;
2468 push @dates, undef if !$prev;
2470 unless ( $part_pkg->hide_svc_detail
2471 || $cust_bill_pkg->itemdesc
2472 || $cust_bill_pkg->hidden
2473 || $is_summary && $type && $type eq 'U'
2477 warn "$me _items_cust_bill_pkg adding service details\n"
2480 my @svc_labels = map &{$escape_function}($_),
2481 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2482 push @d, @svc_labels
2483 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2484 $svc_label = $svc_labels[0];
2486 warn "$me _items_cust_bill_pkg done adding service details\n"
2489 my $lnum = $cust_main ? $cust_main->ship_locationnum
2490 : $self->prospect_main->locationnum;
2491 if ( $cust_pkg->locationnum != $lnum ) {
2492 my $loc = $cust_pkg->location_label;
2493 $loc = substr($loc, 0, $maxlength). '...'
2494 if $format eq 'latex' && length($loc) > $maxlength;
2495 push @d, &{$escape_function}($loc);
2498 # Display of seconds_since_sqlradacct:
2499 # On the invoice, when processing @detail_items, look for a field
2500 # named 'seconds'. This will contain total seconds for each
2501 # service, in the same order as @ext_description. For services
2502 # that don't support this it will show undef.
2503 if ( $conf->exists('svc_acct-usage_seconds')
2504 and ! $cust_bill_pkg->pkgpart_override ) {
2505 foreach my $cust_svc (
2506 $cust_pkg->h_cust_svc(@dates, 'I')
2509 # eval because not having any part_export_usage exports
2510 # is a fatal error, last_bill/_date because that's how
2511 # sqlradius_hour billing does it
2513 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2515 push @seconds, $sec;
2517 } #if svc_acct-usage_seconds
2521 unless ( $is_summary ) {
2522 warn "$me _items_cust_bill_pkg adding details\n"
2525 #instead of omitting details entirely in this case (unwanted side
2526 # effects), just omit CDRs
2527 $details_opt{'no_usage'} = 1
2528 if $type && $type eq 'R';
2530 push @d, $cust_bill_pkg->details(%details_opt);
2533 warn "$me _items_cust_bill_pkg calculating amount\n"
2538 $amount = $cust_bill_pkg->recur;
2539 } elsif ($type eq 'R') {
2540 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2541 } elsif ($type eq 'U') {
2542 $amount = $cust_bill_pkg->usage;
2546 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2549 if ( !$type || $type eq 'R' ) {
2551 warn "$me _items_cust_bill_pkg adding recur\n"
2554 if ( $cust_bill_pkg->hidden ) {
2555 $r->{amount} += $amount;
2556 $r->{unit_amount} += $unit_amount;
2557 push @{ $r->{ext_description} }, @d;
2560 description => $description,
2561 pkgpart => $pkgpart,
2562 pkgnum => $cust_bill_pkg->pkgnum,
2564 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2565 unit_amount => $unit_amount,
2566 quantity => $cust_bill_pkg->quantity,
2568 ext_description => \@d,
2569 svc_label => ($svc_label || ''),
2571 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2574 } else { # $type eq 'U'
2576 warn "$me _items_cust_bill_pkg adding usage\n"
2579 if ( $cust_bill_pkg->hidden ) {
2580 $u->{amount} += $amount;
2581 $u->{unit_amount} += $unit_amount,
2582 push @{ $u->{ext_description} }, @d;
2585 description => $description,
2586 pkgpart => $pkgpart,
2587 pkgnum => $cust_bill_pkg->pkgnum,
2589 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2590 unit_amount => $unit_amount,
2591 quantity => $cust_bill_pkg->quantity,
2593 ext_description => \@d,
2598 } # recurring or usage with recurring charge
2600 } else { #pkgnum tax or one-shot line item (??)
2602 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2605 if ( $cust_bill_pkg->setup != 0 ) {
2607 'description' => $desc,
2608 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2611 if ( $cust_bill_pkg->recur != 0 ) {
2613 'description' => "$desc (".
2614 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2615 time2str($date_format, $cust_bill_pkg->edate). ')',
2616 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2624 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2625 && $conf->exists('discount-show-always'));
2629 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2631 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2632 $_->{amount} =~ s/^\-0\.00$/0.00/;
2633 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2635 if $_->{amount} != 0
2636 || $discount_show_always
2637 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2638 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2642 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2649 =item _items_discounts_avail
2651 Returns an array of line item hashrefs representing available term discounts
2652 for this invoice. This makes the same assumptions that apply to term
2653 discounts in general: that the package is billed monthly, at a flat rate,
2654 with no usage charges. A prorated first month will be handled, as will
2655 a setup fee if the discount is allowed to apply to setup fees.
2659 sub _items_discounts_avail {
2662 #maybe move this method from cust_bill when quotations support discount_plans
2663 return () unless $self->can('discount_plans');
2664 my %plans = $self->discount_plans;
2666 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2667 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2671 my $plan = $plans{$months};
2673 my $term_total = sprintf('%.2f', $plan->discounted_total);
2674 my $percent = sprintf('%.0f',
2675 100 * (1 - $term_total / $plan->base_total) );
2676 my $permonth = sprintf('%.2f', $term_total / $months);
2677 my $detail = $self->mt('discount on item'). ' '.
2678 join(', ', map { "#$_" } $plan->pkgnums)
2681 # discounts for non-integer months don't work anyway
2682 $months = sprintf("%d", $months);
2685 description => $self->mt('Save [_1]% by paying for [_2] months',
2687 amount => $self->mt('[_1] ([_2] per month)',
2688 $term_total, $money_char.$permonth),
2689 ext_description => ($detail || ''),
2692 sort { $b <=> $a } keys %plans;