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 $cust_main = $self->cust_main;
126 my $prospect_main = $self->prospect_main;
127 my $agentnum = $cust_main ? $cust_main->agentnum : $prospect_main->agentnum;
129 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
130 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
131 or die "can't write temp file: $!\n";
133 print $lh $conf->config_binary('logo.eps', $agentnum)
134 or die "can't write temp file: $!\n";
137 $params{'logo_file'} = $lh->filename;
139 if( $conf->exists('invoice-barcode')
140 && $self->can('invoice_barcode')
141 && $self->invnum ) { # don't try to barcode statements
142 my $png_file = $self->invoice_barcode($dir);
143 my $eps_file = $png_file;
144 $eps_file =~ s/\.png$/.eps/g;
145 $png_file =~ /(barcode.*png)/;
147 $eps_file =~ /(barcode.*eps)/;
150 my $curr_dir = cwd();
152 # after painfuly long experimentation, it was determined that sam2p won't
153 # accept : and other chars in the path, no matter how hard I tried to
154 # escape them, hence the chdir (and chdir back, just to be safe)
155 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
156 or die "sam2p failed: $!\n";
160 $params{'barcode_file'} = $eps_file;
163 my @filled_in = $self->print_generic( %params );
165 my $fh = new File::Temp( TEMPLATE => $tmp_template,
169 ) or die "can't open temp file: $!\n";
170 binmode($fh, ':utf8'); # language support
171 print $fh join('', @filled_in );
174 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
175 return ($1, $params{'logo_file'}, $params{'barcode_file'});
179 =item print_generic OPTION => VALUE ...
181 Internal method - returns a filled-in template for this invoice as a scalar.
183 See print_ps and print_pdf for methods that return PostScript and PDF output.
185 Non optional options include
186 format - latex, html, template
188 Optional options include
190 template - a value used as a suffix for a configuration template
192 time - a value used to control the printing of overdue messages. The
193 default is now. It isn't the date of the invoice; that's the `_date' field.
194 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
195 L<Time::Local> and L<Date::Parse> for conversion functions.
199 unsquelch_cdr - overrides any per customer cdr squelching when true
201 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
203 locale - override customer's locale
207 #what's with all the sprintf('%10.2f')'s in here? will it cause any
208 # (alignment in text invoice?) problems to change them all to '%.2f' ?
209 # yes: fixed width/plain text printing will be borked
211 my( $self, %params ) = @_;
212 my $conf = $self->conf;
213 my $today = $params{today} ? $params{today} : time;
214 warn "$me print_generic called on $self with suffix $params{template}\n"
217 my $format = $params{format};
218 die "Unknown format: $format"
219 unless $format =~ /^(latex|html|template)$/;
221 my $cust_main = $self->cust_main || $self->prospect_main;
222 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
223 unless $cust_main->payname
224 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
226 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
227 'html' => [ '<%=', '%>' ],
228 'template' => [ '{', '}' ],
231 warn "$me print_generic creating template\n"
235 my $template = $params{template} ? $params{template} : $self->_agent_template;
236 my $templatefile = $self->template_conf. $format;
237 $templatefile .= "_$template"
238 if length($template) && $conf->exists($templatefile."_$template");
239 my @invoice_template = map "$_\n", $conf->config($templatefile)
240 or die "cannot load config data $templatefile";
243 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
244 #change this to a die when the old code is removed
245 warn "old-style invoice template $templatefile; ".
246 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
248 @invoice_template = _translate_old_latex_format(@invoice_template);
251 warn "$me print_generic creating T:T object\n"
254 my $text_template = new Text::Template(
256 SOURCE => \@invoice_template,
257 DELIMITERS => $delimiters{$format},
260 warn "$me print_generic compiling T:T object\n"
263 $text_template->compile()
264 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
267 # additional substitution could possibly cause breakage in existing templates
270 'notes' => sub { map "$_", @_ },
271 'footer' => sub { map "$_", @_ },
272 'smallfooter' => sub { map "$_", @_ },
273 'returnaddress' => sub { map "$_", @_ },
274 'coupon' => sub { map "$_", @_ },
275 'summary' => sub { map "$_", @_ },
281 s/%%(.*)$/<!-- $1 -->/g;
282 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
283 s/\\begin\{enumerate\}/<ol>/g;
285 s/\\end\{enumerate\}/<\/ol>/g;
286 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
295 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
297 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
303 s/\\hyphenation\{[\w\s\-]+}//;
308 'coupon' => sub { "" },
309 'summary' => sub { "" },
316 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
317 s/\\begin\{enumerate\}//g;
319 s/\\end\{enumerate\}//g;
320 s/\\textbf\{(.*)\}/$1/g;
327 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
329 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
334 s/\\\\\*?\s*$/\n/; # dubious
335 s/\\hyphenation\{[\w\s\-]+}//;
339 'coupon' => sub { "" },
340 'summary' => sub { "" },
345 # hashes for differing output formats
346 my %nbsps = ( 'latex' => '~',
347 'html' => '', # '&nbps;' would be nice
348 'template' => '', # not used
350 my $nbsp = $nbsps{$format};
352 my %escape_functions = ( 'latex' => \&_latex_escape,
353 'html' => \&_html_escape_nbsp,#\&encode_entities,
354 'template' => sub { shift },
356 my $escape_function = $escape_functions{$format};
357 my $escape_function_nonbsp = ($format eq 'html')
358 ? \&_html_escape : $escape_function;
360 my %date_formats = ( 'latex' => $date_format_long,
361 'html' => $date_format_long,
364 $date_formats{'html'} =~ s/ / /g;
366 my $date_format = $date_formats{$format};
368 my %newline_tokens = ( 'latex' => '\\\\',
372 my $newline_token = $newline_tokens{$format};
374 warn "$me generating template variables\n"
377 # generate template variables
380 defined( $conf->config_orbase( "invoice_${format}returnaddress",
384 && length( $conf->config_orbase( "invoice_${format}returnaddress",
390 $returnaddress = join("\n",
391 $conf->config_orbase("invoice_${format}returnaddress", $template)
395 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
397 my $convert_map = $convert_maps{$format}{'returnaddress'};
400 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
405 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
407 my $convert_map = $convert_maps{$format}{'returnaddress'};
408 $returnaddress = join( "\n", &$convert_map(
409 map { s/( {2,})/'~' x length($1)/eg;
413 ( $conf->config('company_name', $cust_main->agentnum),
414 $conf->config('company_address', $cust_main->agentnum),
421 my $warning = "Couldn't find a return address; ".
422 "do you need to set the company_address configuration value?";
424 $returnaddress = $nbsp;
425 #$returnaddress = $warning;
429 warn "$me generating invoice data\n"
432 my $agentnum = $cust_main->agentnum;
437 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
438 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
439 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
440 'returnaddress' => $returnaddress,
441 'agent' => &$escape_function($cust_main->agent->agent),
443 #invoice/quotation info
444 'invnum' => $self->invnum,
445 'quotationnum' => $self->quotationnum,
446 'date' => time2str($date_format, $self->_date),
447 'today' => time2str($date_format_long, $today),
448 'terms' => $self->terms,
449 'template' => $template, #params{'template'},
450 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
451 'current_charges' => sprintf("%.2f", $self->charged),
452 'duedate' => $self->due_date2str($rdate_format), #date_format?
455 'custnum' => $cust_main->display_custnum,
456 'prospectnum' => $cust_main->prospectnum,
457 'agent_custid' => &$escape_function($cust_main->agent_custid),
458 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
459 payname company address1 address2 city state zip fax
463 'ship_enable' => $conf->exists('invoice-ship_address'),
464 'unitprices' => $conf->exists('invoice-unitprice'),
465 'smallernotes' => $conf->exists('invoice-smallernotes'),
466 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
467 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
469 #layout info -- would be fancy to calc some of this and bury the template
471 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
472 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
473 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
474 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
475 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
476 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
477 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
478 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
479 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
480 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
482 # better hang on to conf_dir for a while (for old templates)
483 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
485 #these are only used when doing paged plaintext
492 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
493 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
494 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
495 # eval to avoid death for unimplemented languages
496 my $dh = eval { Date::Language->new($info{'name'}) } ||
497 Date::Language->new(); # fall back to English
498 # prototype here to silence warnings
499 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
500 # eventually use this date handle everywhere in here, too
502 my $min_sdate = 999999999999;
504 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
505 next unless $cust_bill_pkg->pkgnum > 0;
506 $min_sdate = $cust_bill_pkg->sdate
507 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
508 $max_edate = $cust_bill_pkg->edate
509 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
512 $invoice_data{'bill_period'} = '';
513 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
514 . " to " . time2str('%e %h', $max_edate)
515 if ($max_edate != 0 && $min_sdate != 999999999999);
517 $invoice_data{finance_section} = '';
518 if ( $conf->config('finance_pkgclass') ) {
520 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
521 $invoice_data{finance_section} = $pkg_class->categoryname;
523 $invoice_data{finance_amount} = '0.00';
524 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
526 my $countrydefault = $conf->config('countrydefault') || 'US';
527 foreach ( qw( address1 address2 city state zip country fax) ){
528 my $method = 'ship_'.$_;
529 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
531 foreach ( qw( contact company ) ) { #compatibility
532 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
534 $invoice_data{'ship_country'} = ''
535 if ( $invoice_data{'ship_country'} eq $countrydefault );
537 $invoice_data{'cid'} = $params{'cid'}
540 if ( $cust_main->country eq $countrydefault ) {
541 $invoice_data{'country'} = '';
543 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
547 $invoice_data{'address'} = \@address;
550 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
551 ? " (P.O. #". $cust_main->payinfo. ")"
555 push @address, $cust_main->company
556 if $cust_main->company;
557 push @address, $cust_main->address1;
558 push @address, $cust_main->address2
559 if $cust_main->address2;
561 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
562 push @address, $invoice_data{'country'}
563 if $invoice_data{'country'};
565 while (scalar(@address) < 5);
567 $invoice_data{'logo_file'} = $params{'logo_file'}
568 if $params{'logo_file'};
569 $invoice_data{'barcode_file'} = $params{'barcode_file'}
570 if $params{'barcode_file'};
571 $invoice_data{'barcode_img'} = $params{'barcode_img'}
572 if $params{'barcode_img'};
573 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
574 if $params{'barcode_cid'};
576 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
577 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
578 #my $balance_due = $self->owed + $pr_total - $cr_total;
579 my $balance_due = $self->owed + $pr_total;
581 #these are used on the summary page only
583 # the customer's current balance as shown on the invoice before this one
584 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
586 # the change in balance from that invoice to this one
587 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
589 # the sum of amount owed on all previous invoices
590 # ($pr_total is used elsewhere but not as $previous_balance)
591 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
593 # the sum of amount owed on all invoices
594 # (this is used in the summary & on the payment coupon)
595 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
597 # info from customer's last invoice before this one, for some
599 $invoice_data{'last_bill'} = {};
600 # returns the last unpaid bill, not the last bill
601 #my $last_bill = $pr_cust_bill[-1];
602 # THIS returns the customer's last bill before this one
603 my $last_bill = qsearchs({
604 'table' => 'cust_bill',
605 'hashref' => { 'custnum' => $self->custnum,
606 'invnum' => { op => '<', value => $self->invnum },
608 'order_by' => ' ORDER BY invnum DESC LIMIT 1'
611 $invoice_data{'last_bill'} = {
612 '_date' => $last_bill->_date, #unformatted
613 # all we need for now
615 my (@payments, @credits);
616 # for formats that itemize previous payments
617 foreach my $cust_pay ( qsearch('cust_pay', {
618 'custnum' => $self->custnum,
619 '_date' => { op => '>=',
620 value => $last_bill->_date }
623 next if $cust_pay->_date > $self->_date;
625 '_date' => $cust_pay->_date,
626 'date' => time2str($date_format, $cust_pay->_date),
627 'payinfo' => $cust_pay->payby_payinfo_pretty,
628 'amount' => sprintf('%.2f', $cust_pay->paid),
630 # not concerned about applications
632 foreach my $cust_credit ( qsearch('cust_credit', {
633 'custnum' => $self->custnum,
634 '_date' => { op => '>=',
635 value => $last_bill->_date }
638 next if $cust_credit->_date > $self->_date;
640 '_date' => $cust_credit->_date,
641 'date' => time2str($date_format, $cust_credit->_date),
642 'creditreason'=> $cust_credit->cust_credit->reason,
643 'amount' => sprintf('%.2f', $cust_credit->amount),
646 $invoice_data{'previous_payments'} = \@payments;
647 $invoice_data{'previous_credits'} = \@credits;
650 my $summarypage = '';
651 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
654 $invoice_data{'summarypage'} = $summarypage;
656 warn "$me substituting variables in notes, footer, smallfooter\n"
659 my $tc = $self->template_conf;
660 my @include = ( [ $tc, 'notes' ],
661 [ 'invoice_', 'footer' ],
662 [ 'invoice_', 'smallfooter', ],
664 push @include, [ $tc, 'coupon', ]
665 unless $params{'no_coupon'};
667 foreach my $i (@include) {
669 my($base, $include) = @$i;
671 my $inc_file = $conf->key_orbase("$base$format$include", $template);
674 if ( $conf->exists($inc_file, $agentnum)
675 && length( $conf->config($inc_file, $agentnum) ) ) {
677 @inc_src = $conf->config($inc_file, $agentnum);
681 $inc_file = $conf->key_orbase("${base}latex$include", $template);
683 my $convert_map = $convert_maps{$format}{$include};
685 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
686 s/--\@\]/$delimiters{$format}[1]/g;
689 &$convert_map( $conf->config($inc_file, $agentnum) );
693 my $inc_tt = new Text::Template (
695 SOURCE => [ map "$_\n", @inc_src ],
696 DELIMITERS => $delimiters{$format},
697 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
699 unless ( $inc_tt->compile() ) {
700 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
701 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
705 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
707 $invoice_data{$include} =~ s/\n+$//
708 if ($format eq 'latex');
711 # let invoices use either of these as needed
712 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
713 ? $cust_main->payinfo : '';
714 $invoice_data{'po_line'} =
715 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
716 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
719 my %money_chars = ( 'latex' => '',
720 'html' => $conf->config('money_char') || '$',
723 my $money_char = $money_chars{$format};
725 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
726 'html' => $conf->config('money_char') || '$',
729 my $other_money_char = $other_money_chars{$format};
730 $invoice_data{'dollar'} = $other_money_char;
732 my %minus_signs = ( 'latex' => '$-$',
734 'template' => '- ' );
735 my $minus = $minus_signs{$format};
737 my @detail_items = ();
738 my @total_items = ();
742 $invoice_data{'detail_items'} = \@detail_items;
743 $invoice_data{'total_items'} = \@total_items;
744 $invoice_data{'buf'} = \@buf;
745 $invoice_data{'sections'} = \@sections;
747 warn "$me generating sections\n"
750 # Previous Charges section
751 # subtotal is the first return value from $self->previous
752 my $previous_section = { 'description' => $self->mt('Previous Charges'),
753 'subtotal' => $other_money_char.
754 sprintf('%.2f', $pr_total),
755 'summarized' => '', #why? $summarypage ? 'Y' : '',
757 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
758 join(' / ', map { $cust_main->balance_date_range(@$_) }
759 $self->_prior_month30s
761 if $conf->exists('invoice_include_aging');
764 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
765 'subtotal' => $taxtotal, # adjusted below
767 my $tax_weight = _pkg_category($tax_section->{description})
768 ? _pkg_category($tax_section->{description})->weight
770 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
771 $tax_section->{'sort_weight'} = $tax_weight;
775 my $adjust_section = {
776 'description' => $self->mt('Credits, Payments, and Adjustments'),
777 'adjust_section' => 1,
778 'subtotal' => 0, # adjusted below
780 my $adjust_weight = _pkg_category($adjust_section->{description})
781 ? _pkg_category($adjust_section->{description})->weight
783 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
784 $adjust_section->{'sort_weight'} = $adjust_weight;
786 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
787 my $multisection = $conf->exists($tc.'_sections', $cust_main->agentnum);
788 $invoice_data{'multisection'} = $multisection;
789 my $late_sections = [];
790 my $extra_sections = [];
791 my $extra_lines = ();
793 my $default_section = { 'description' => '',
798 if ( $multisection ) {
799 ($extra_sections, $extra_lines) =
800 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
801 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
802 && $self->can('_items_extra_usage_sections');
804 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
806 push @detail_items, @$extra_lines if $extra_lines;
808 $self->_items_sections( $late_sections, # this could stand a refactor
810 $escape_function_nonbsp,
814 if ( $conf->exists('svc_phone_sections')
815 && $self->can('_items_svc_phone_sections')
818 my ($phone_sections, $phone_lines) =
819 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
820 push @{$late_sections}, @$phone_sections;
821 push @detail_items, @$phone_lines;
823 if ( $conf->exists('voip-cust_accountcode_cdr')
824 && $cust_main->accountcode_cdr
825 && $self->can('_items_accountcode_cdr')
828 my ($accountcode_section, $accountcode_lines) =
829 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
830 if ( scalar(@$accountcode_lines) ) {
831 push @{$late_sections}, $accountcode_section;
832 push @detail_items, @$accountcode_lines;
835 } else {# not multisection
836 # make a default section
837 push @sections, $default_section;
838 # and calculate the finance charge total, since it won't get done otherwise.
839 # XXX possibly other totals?
840 # XXX possibly finance_pkgclass should not be used in this manner?
841 if ( $conf->exists('finance_pkgclass') ) {
843 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
844 if ( grep { $_->section eq $invoice_data{finance_section} }
845 $cust_bill_pkg->cust_bill_pkg_display ) {
846 # I think these are always setup fees, but just to be sure...
847 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
850 $invoice_data{finance_amount} =
851 sprintf('%.2f', sum( @finance_charges ) || 0);
855 # previous invoice balances in the Previous Charges section if there
856 # is one, otherwise in the main detail section
857 if ( $self->can('_items_previous') &&
858 $self->enable_previous &&
859 ! $conf->exists('previous_balance-summary_only') ) {
861 warn "$me adding previous balances\n"
864 foreach my $line_item ( $self->_items_previous ) {
867 ext_description => [],
869 $detail->{'ref'} = $line_item->{'pkgnum'};
870 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
871 $detail->{'quantity'} = 1;
872 $detail->{'section'} = $multisection ? $previous_section
874 $detail->{'description'} = &$escape_function($line_item->{'description'});
875 if ( exists $line_item->{'ext_description'} ) {
876 @{$detail->{'ext_description'}} = map {
877 &$escape_function($_);
878 } @{$line_item->{'ext_description'}};
880 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
881 $line_item->{'amount'};
882 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
884 push @detail_items, $detail;
885 push @buf, [ $detail->{'description'},
886 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
892 if ( @pr_cust_bill && $self->enable_previous ) {
893 push @buf, ['','-----------'];
894 push @buf, [ $self->mt('Total Previous Balance'),
895 $money_char. sprintf("%10.2f", $pr_total) ];
899 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
900 warn "$me adding DID summary\n"
903 my ($didsummary,$minutes) = $self->_did_summary;
904 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
906 { 'description' => $didsummary_desc,
907 'ext_description' => [ $didsummary, $minutes ],
911 foreach my $section (@sections, @$late_sections) {
913 warn "$me adding section \n". Dumper($section)
916 # begin some normalization
917 $section->{'subtotal'} = $section->{'amount'}
919 && !exists($section->{subtotal})
920 && exists($section->{amount});
922 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
923 if ( $invoice_data{finance_section} &&
924 $section->{'description'} eq $invoice_data{finance_section} );
926 $section->{'subtotal'} = $other_money_char.
927 sprintf('%.2f', $section->{'subtotal'})
930 # continue some normalization
931 $section->{'amount'} = $section->{'subtotal'}
935 if ( $section->{'description'} ) {
936 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
941 warn "$me setting options\n"
945 $options{'section'} = $section if $multisection;
946 $options{'format'} = $format;
947 $options{'escape_function'} = $escape_function;
948 $options{'no_usage'} = 1 unless $unsquelched;
949 $options{'unsquelched'} = $unsquelched;
950 $options{'summary_page'} = $summarypage;
951 $options{'skip_usage'} =
952 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
953 $options{'multisection'} = $multisection;
955 warn "$me searching for line items\n"
958 foreach my $line_item ( $self->_items_pkg(%options) ) {
960 warn "$me adding line item $line_item\n"
964 ext_description => [],
966 $detail->{'ref'} = $line_item->{'pkgnum'};
967 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
968 $detail->{'quantity'} = $line_item->{'quantity'};
969 $detail->{'section'} = $section;
970 $detail->{'description'} = &$escape_function($line_item->{'description'});
971 if ( exists $line_item->{'ext_description'} ) {
972 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
974 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
975 $line_item->{'amount'};
976 if ( exists $line_item->{'unit_amount'} ) {
977 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
978 $line_item->{'unit_amount'};
980 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
982 $detail->{'sdate'} = $line_item->{'sdate'};
983 $detail->{'edate'} = $line_item->{'edate'};
984 $detail->{'seconds'} = $line_item->{'seconds'};
985 $detail->{'svc_label'} = $line_item->{'svc_label'};
987 push @detail_items, $detail;
988 push @buf, ( [ $detail->{'description'},
989 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
991 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
995 if ( $section->{'description'} ) {
996 push @buf, ( ['','-----------'],
997 [ $section->{'description'}. ' sub-total',
998 $section->{'subtotal'} # already formatted this
1007 $invoice_data{current_less_finance} =
1008 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
1010 # create a major section for previous balance if we have major sections,
1011 # or if previous_section is in summary form
1012 if ( ( $multisection && $self->enable_previous )
1013 || $conf->exists('previous_balance-summary_only') )
1015 unshift @sections, $previous_section if $pr_total;
1018 warn "$me adding taxes\n"
1021 my @items_tax = $self->_items_tax;
1022 foreach my $tax ( @items_tax ) {
1024 $taxtotal += $tax->{'amount'};
1026 my $description = &$escape_function( $tax->{'description'} );
1027 my $amount = sprintf( '%.2f', $tax->{'amount'} );
1029 if ( $multisection ) {
1031 my $money = $old_latex ? '' : $money_char;
1032 push @detail_items, {
1033 ext_description => [],
1036 description => $description,
1037 amount => $money. $amount,
1039 section => $tax_section,
1044 push @total_items, {
1045 'total_item' => $description,
1046 'total_amount' => $other_money_char. $amount,
1051 push @buf,[ $description,
1052 $money_char. $amount,
1059 $total->{'total_item'} = $self->mt('Sub-total');
1060 $total->{'total_amount'} =
1061 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1063 if ( $multisection ) {
1064 $tax_section->{'subtotal'} = $other_money_char.
1065 sprintf('%.2f', $taxtotal);
1066 $tax_section->{'pretotal'} = 'New charges sub-total '.
1067 $total->{'total_amount'};
1068 push @sections, $tax_section if $taxtotal;
1070 unshift @total_items, $total;
1073 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1075 push @buf,['','-----------'];
1076 push @buf,[$self->mt(
1077 (!$self->enable_previous)
1079 : 'Total New Charges'
1081 $money_char. sprintf("%10.2f",$self->charged) ];
1089 my %embolden_functions = (
1090 'latex' => sub { return '\textbf{'. shift(). '}' },
1091 'html' => sub { return '<b>'. shift(). '</b>' },
1092 'template' => sub { shift },
1094 my $embolden_function = $embolden_functions{$format};
1096 if ( $self->can('_items_total') ) { # quotations
1098 $self->_items_total(\@total_items);
1100 foreach ( @total_items ) {
1101 $_->{'total_item'} = &$embolden_function( $_->{'total_item'} );
1102 $_->{'total_amount'} = &$embolden_function( $other_money_char.
1103 $_->{'total_amount'}
1107 } else { #normal invoice case
1109 # calculate total, possibly including total owed on previous
1113 $item = $conf->config('previous_balance-exclude_from_total')
1114 || 'Total New Charges'
1115 if $conf->exists('previous_balance-exclude_from_total');
1116 my $amount = $self->charged;
1117 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1118 $amount += $pr_total;
1121 $total->{'total_item'} = &$embolden_function($self->mt($item));
1122 $total->{'total_amount'} =
1123 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1124 if ( $multisection ) {
1125 if ( $adjust_section->{'sort_weight'} ) {
1126 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1127 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1129 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1130 $other_money_char. sprintf('%.2f', $self->charged );
1133 push @total_items, $total;
1135 push @buf,['','-----------'];
1138 sprintf( '%10.2f', $amount )
1142 # if we're showing previous invoices, also show previous
1143 # credits and payments
1144 if ( $self->enable_previous
1145 and $self->can('_items_credits')
1146 and $self->can('_items_payments') )
1148 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1151 my $credittotal = 0;
1152 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1155 $total->{'total_item'} = &$escape_function($credit->{'description'});
1156 $credittotal += $credit->{'amount'};
1157 $total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
1158 $adjusttotal += $credit->{'amount'};
1159 if ( $multisection ) {
1160 my $money = $old_latex ? '' : $money_char;
1161 push @detail_items, {
1162 ext_description => [],
1165 description => &$escape_function($credit->{'description'}),
1166 amount => $money. $credit->{'amount'},
1168 section => $adjust_section,
1171 push @total_items, $total;
1175 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1178 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1179 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1183 my $paymenttotal = 0;
1184 foreach my $payment ( $self->_items_payments ) {
1186 $total->{'total_item'} = &$escape_function($payment->{'description'});
1187 $paymenttotal += $payment->{'amount'};
1188 $total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
1189 $adjusttotal += $payment->{'amount'};
1190 if ( $multisection ) {
1191 my $money = $old_latex ? '' : $money_char;
1192 push @detail_items, {
1193 ext_description => [],
1196 description => &$escape_function($payment->{'description'}),
1197 amount => $money. $payment->{'amount'},
1199 section => $adjust_section,
1202 push @total_items, $total;
1204 push @buf, [ $payment->{'description'},
1205 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1208 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1210 if ( $multisection ) {
1211 $adjust_section->{'subtotal'} = $other_money_char.
1212 sprintf('%.2f', $adjusttotal);
1213 push @sections, $adjust_section
1214 unless $adjust_section->{sort_weight};
1217 # create Balance Due message
1220 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1221 $total->{'total_amount'} =
1222 &$embolden_function(
1223 $other_money_char. sprintf('%.2f', #why? $summarypage
1224 # ? $self->charged +
1225 # $self->billing_balance
1227 $self->owed + $pr_total
1230 if ( $multisection && !$adjust_section->{sort_weight} ) {
1231 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1232 $total->{'total_amount'};
1234 push @total_items, $total;
1236 push @buf,['','-----------'];
1237 push @buf,[$self->balance_due_msg, $money_char.
1238 sprintf("%10.2f", $balance_due ) ];
1241 if ( $conf->exists('previous_balance-show_credit')
1242 and $cust_main->balance < 0 ) {
1243 my $credit_total = {
1244 'total_item' => &$embolden_function($self->credit_balance_msg),
1245 'total_amount' => &$embolden_function(
1246 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1249 if ( $multisection ) {
1250 $adjust_section->{'posttotal'} .= $newline_token .
1251 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1254 push @total_items, $credit_total;
1256 push @buf,['','-----------'];
1257 push @buf,[$self->credit_balance_msg, $money_char.
1258 sprintf("%10.2f", -$cust_main->balance ) ];
1262 } #end of default total adding ! can('_items_total')
1264 if ( $multisection ) {
1265 if ( $conf->exists('svc_phone_sections')
1266 && $self->can('_items_svc_phone_sections')
1270 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1271 $total->{'total_amount'} =
1272 &$embolden_function(
1273 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1275 my $last_section = pop @sections;
1276 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1277 $total->{'total_amount'};
1278 push @sections, $last_section;
1280 push @sections, @$late_sections
1284 # make a discounts-available section, even without multisection
1285 if ( $conf->exists('discount-show_available')
1286 and my @discounts_avail = $self->_items_discounts_avail ) {
1287 my $discount_section = {
1288 'description' => $self->mt('Discounts Available'),
1293 push @sections, $discount_section;
1294 push @detail_items, map { +{
1295 'ref' => '', #should this be something else?
1296 'section' => $discount_section,
1297 'description' => &$escape_function( $_->{description} ),
1298 'amount' => $money_char . &$escape_function( $_->{amount} ),
1299 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1300 } } @discounts_avail;
1303 # debugging hook: call this with 'diag' => 1 to just get a hash of
1304 # the invoice variables
1305 return \%invoice_data if ( $params{'diag'} );
1307 # All sections and items are built; now fill in templates.
1308 my @includelist = ();
1309 push @includelist, 'summary' if $summarypage;
1310 foreach my $include ( @includelist ) {
1312 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1315 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1317 @inc_src = $conf->config($inc_file, $agentnum);
1321 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1323 my $convert_map = $convert_maps{$format}{$include};
1325 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1326 s/--\@\]/$delimiters{$format}[1]/g;
1329 &$convert_map( $conf->config($inc_file, $agentnum) );
1333 my $inc_tt = new Text::Template (
1335 SOURCE => [ map "$_\n", @inc_src ],
1336 DELIMITERS => $delimiters{$format},
1337 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1339 unless ( $inc_tt->compile() ) {
1340 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1341 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1345 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1347 $invoice_data{$include} =~ s/\n+$//
1348 if ($format eq 'latex');
1353 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1354 /invoice_lines\((\d*)\)/;
1355 $invoice_lines += $1 || scalar(@buf);
1358 die "no invoice_lines() functions in template?"
1359 if ( $format eq 'template' && !$wasfunc );
1361 if ($format eq 'template') {
1363 if ( $invoice_lines ) {
1364 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1365 $invoice_data{'total_pages'}++
1366 if scalar(@buf) % $invoice_lines;
1369 #setup subroutine for the template
1370 $invoice_data{invoice_lines} = sub {
1371 my $lines = shift || scalar(@buf);
1383 push @collect, split("\n",
1384 $text_template->fill_in( HASH => \%invoice_data )
1386 $invoice_data{'page'}++;
1388 map "$_\n", @collect;
1390 } else { # this is where we actually create the invoice
1392 warn "filling in template for invoice ". $self->invnum. "\n"
1394 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1397 $text_template->fill_in(HASH => \%invoice_data);
1401 sub notice_name { '('.shift->table.')'; }
1403 sub template_conf { 'invoice_'; }
1405 # helper routine for generating date ranges
1406 sub _prior_month30s {
1409 [ 1, 2592000 ], # 0-30 days ago
1410 [ 2592000, 5184000 ], # 30-60 days ago
1411 [ 5184000, 7776000 ], # 60-90 days ago
1412 [ 7776000, 0 ], # 90+ days ago
1415 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1416 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1421 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1423 Returns an postscript invoice, as a scalar.
1425 Options can be passed as a hashref (recommended) or as a list of time, template
1426 and then any key/value pairs for any other options.
1428 I<time> an optional value used to control the printing of overdue messages. The
1429 default is now. It isn't the date of the invoice; that's the `_date' field.
1430 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1431 L<Time::Local> and L<Date::Parse> for conversion functions.
1433 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1440 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1441 my $ps = generate_ps($file);
1443 unlink($barcodefile) if $barcodefile;
1448 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1450 Returns an PDF invoice, as a scalar.
1452 Options can be passed as a hashref (recommended) or as a list of time, template
1453 and then any key/value pairs for any other options.
1455 I<time> an optional value used to control the printing of overdue messages. The
1456 default is now. It isn't the date of the invoice; that's the `_date' field.
1457 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1458 L<Time::Local> and L<Date::Parse> for conversion functions.
1460 I<template>, if specified, is the name of a suffix for alternate invoices.
1462 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1469 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1470 my $pdf = generate_pdf($file);
1472 unlink($barcodefile) if $barcodefile;
1477 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1479 Returns an HTML invoice, as a scalar.
1481 I<time> an optional value used to control the printing of overdue messages. The
1482 default is now. It isn't the date of the invoice; that's the `_date' field.
1483 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1484 L<Time::Local> and L<Date::Parse> for conversion functions.
1486 I<template>, if specified, is the name of a suffix for alternate invoices.
1488 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1490 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1491 when emailing the invoice as part of a multipart/related MIME email.
1499 %params = %{ shift() };
1501 $params{'time'} = shift;
1502 $params{'template'} = shift;
1503 $params{'cid'} = shift;
1506 $params{'format'} = 'html';
1508 $self->print_generic( %params );
1511 # quick subroutine for print_latex
1513 # There are ten characters that LaTeX treats as special characters, which
1514 # means that they do not simply typeset themselves:
1515 # # $ % & ~ _ ^ \ { }
1517 # TeX ignores blanks following an escaped character; if you want a blank (as
1518 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1522 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1523 $value =~ s/([<>])/\$$1\$/g;
1529 encode_entities($value);
1533 sub _html_escape_nbsp {
1534 my $value = _html_escape(shift);
1535 $value =~ s/ +/ /g;
1539 #utility methods for print_*
1541 sub _translate_old_latex_format {
1542 warn "_translate_old_latex_format called\n"
1549 if ( $line =~ /^%%Detail\s*$/ ) {
1551 push @template, q![@--!,
1552 q! foreach my $_tr_line (@detail_items) {!,
1553 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1554 q! $_tr_line->{'description'} .= !,
1555 q! "\\tabularnewline\n~~".!,
1556 q! join( "\\tabularnewline\n~~",!,
1557 q! @{$_tr_line->{'ext_description'}}!,
1561 while ( ( my $line_item_line = shift )
1562 !~ /^%%EndDetail\s*$/ ) {
1563 $line_item_line =~ s/'/\\'/g; # nice LTS
1564 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1565 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1566 push @template, " \$OUT .= '$line_item_line';";
1569 push @template, '}',
1572 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1574 push @template, '[@--',
1575 ' foreach my $_tr_line (@total_items) {';
1577 while ( ( my $total_item_line = shift )
1578 !~ /^%%EndTotalDetails\s*$/ ) {
1579 $total_item_line =~ s/'/\\'/g; # nice LTS
1580 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1581 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1582 push @template, " \$OUT .= '$total_item_line';";
1585 push @template, '}',
1589 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1590 push @template, $line;
1596 warn "$_\n" foreach @template;
1604 my $conf = $self->conf;
1606 #check for an invoice-specific override
1607 return $self->invoice_terms if $self->invoice_terms;
1609 #check for a customer- specific override
1610 my $cust_main = $self->cust_main;
1611 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1613 #use configured default
1614 $conf->config('invoice_default_terms') || '';
1620 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1621 $duedate = $self->_date() + ( $1 * 86400 );
1628 $self->due_date ? time2str(shift, $self->due_date) : '';
1631 sub balance_due_msg {
1633 my $msg = $self->mt('Balance Due');
1634 return $msg unless $self->terms;
1635 if ( $self->due_date ) {
1636 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1637 $self->due_date2str($date_format);
1638 } elsif ( $self->terms ) {
1639 $msg .= ' - '. $self->terms;
1644 sub balance_due_date {
1646 my $conf = $self->conf;
1648 if ( $conf->exists('invoice_default_terms')
1649 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1650 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1655 sub credit_balance_msg {
1657 $self->mt('Credit Balance Remaining')
1662 Returns a string with the date, for example: "3/20/2008"
1668 time2str($date_format, $self->_date);
1671 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1673 Generate section information for all items appearing on this invoice.
1674 This will only be called for multi-section invoices.
1676 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1677 related display records (L<FS::cust_bill_pkg_display>) and organize
1678 them into two groups ("early" and "late" according to whether they come
1679 before or after the total), then into sections. A subtotal is calculated
1682 Section descriptions are returned in sort weight order. Each consists
1683 of a hash containing:
1685 description: the package category name, escaped
1686 subtotal: the total charges in that section
1687 tax_section: a flag indicating that the section contains only tax charges
1688 summarized: same as tax_section, for some reason
1689 sort_weight: the package category's sort weight
1691 If 'condense' is set on the display record, it also contains everything
1692 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1693 coderefs to generate parts of the invoice. This is not advised.
1697 LATE: an arrayref to push the "late" section hashes onto. The "early"
1698 group is simply returned from the method.
1700 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1701 Turning this on has the following effects:
1702 - Ignores display items with the 'summary' flag.
1703 - Combines all items into the "early" group.
1704 - Creates sections for all non-disabled package categories, even if they
1705 have no charges on this invoice, as well as a section with no name.
1707 ESCAPE: an escape function to use for section titles.
1709 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1710 sorted list. If there are any of these, section subtotals exclude
1713 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1714 passed through to C<_condense_section()>.
1718 use vars qw(%pkg_category_cache);
1719 sub _items_sections {
1722 my $summarypage = shift;
1724 my $extra_sections = shift;
1728 my %late_subtotal = ();
1731 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1734 my $usage = $cust_bill_pkg->usage;
1736 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1737 next if ( $display->summary && $summarypage );
1739 my $section = $display->section;
1740 my $type = $display->type;
1742 $not_tax{$section} = 1
1743 unless $cust_bill_pkg->pkgnum == 0;
1745 # there's actually a very important piece of logic buried in here:
1746 # incrementing $late_subtotal{$section} CREATES
1747 # $late_subtotal{$section}. keys(%late_subtotal) is later used
1748 # to define the list of late sections, and likewise keys(%subtotal).
1749 # When _items_cust_bill_pkg is called to generate line items for
1750 # real, it will be called with 'section' => $section for each
1752 if ( $display->post_total && !$summarypage ) {
1753 if (! $type || $type eq 'S') {
1754 $late_subtotal{$section} += $cust_bill_pkg->setup
1755 if $cust_bill_pkg->setup != 0
1756 || $cust_bill_pkg->setup_show_zero;
1760 $late_subtotal{$section} += $cust_bill_pkg->recur
1761 if $cust_bill_pkg->recur != 0
1762 || $cust_bill_pkg->recur_show_zero;
1765 if ($type && $type eq 'R') {
1766 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1767 if $cust_bill_pkg->recur != 0
1768 || $cust_bill_pkg->recur_show_zero;
1771 if ($type && $type eq 'U') {
1772 $late_subtotal{$section} += $usage
1773 unless scalar(@$extra_sections);
1778 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1780 if (! $type || $type eq 'S') {
1781 $subtotal{$section} += $cust_bill_pkg->setup
1782 if $cust_bill_pkg->setup != 0
1783 || $cust_bill_pkg->setup_show_zero;
1787 $subtotal{$section} += $cust_bill_pkg->recur
1788 if $cust_bill_pkg->recur != 0
1789 || $cust_bill_pkg->recur_show_zero;
1792 if ($type && $type eq 'R') {
1793 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1794 if $cust_bill_pkg->recur != 0
1795 || $cust_bill_pkg->recur_show_zero;
1798 if ($type && $type eq 'U') {
1799 $subtotal{$section} += $usage
1800 unless scalar(@$extra_sections);
1809 %pkg_category_cache = ();
1811 push @$late, map { { 'description' => &{$escape}($_),
1812 'subtotal' => $late_subtotal{$_},
1814 'sort_weight' => ( _pkg_category($_)
1815 ? _pkg_category($_)->weight
1818 ((_pkg_category($_) && _pkg_category($_)->condense)
1819 ? $self->_condense_section($format)
1823 sort _sectionsort keys %late_subtotal;
1826 if ( $summarypage ) {
1827 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1828 map { $_->categoryname } qsearch('pkg_category', {});
1829 push @sections, '' if exists($subtotal{''});
1831 @sections = keys %subtotal;
1834 my @early = map { { 'description' => &{$escape}($_),
1835 'subtotal' => $subtotal{$_},
1836 'summarized' => $not_tax{$_} ? '' : 'Y',
1837 'tax_section' => $not_tax{$_} ? '' : 'Y',
1838 'sort_weight' => ( _pkg_category($_)
1839 ? _pkg_category($_)->weight
1842 ((_pkg_category($_) && _pkg_category($_)->condense)
1843 ? $self->_condense_section($format)
1848 push @early, @$extra_sections if $extra_sections;
1850 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1854 #helper subs for above
1857 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1861 my $categoryname = shift;
1862 $pkg_category_cache{$categoryname} ||=
1863 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1866 my %condensed_format = (
1867 'label' => [ qw( Description Qty Amount ) ],
1869 sub { shift->{description} },
1870 sub { shift->{quantity} },
1871 sub { my($href, %opt) = @_;
1872 ($opt{dollar} || ''). $href->{amount};
1875 'align' => [ qw( l r r ) ],
1876 'span' => [ qw( 5 1 1 ) ], # unitprices?
1877 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1880 sub _condense_section {
1881 my ( $self, $format ) = ( shift, shift );
1883 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1884 qw( description_generator
1887 total_line_generator
1892 sub _condensed_generator_defaults {
1893 my ( $self, $format ) = ( shift, shift );
1894 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1903 sub _condensed_header_generator {
1904 my ( $self, $format ) = ( shift, shift );
1906 my ( $f, $prefix, $suffix, $separator, $column ) =
1907 _condensed_generator_defaults($format);
1909 if ($format eq 'latex') {
1910 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1911 $suffix = "\\\\\n\\hline";
1914 sub { my ($d,$a,$s,$w) = @_;
1915 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1917 } elsif ( $format eq 'html' ) {
1918 $prefix = '<th></th>';
1922 sub { my ($d,$a,$s,$w) = @_;
1923 return qq!<th align="$html_align{$a}">$d</th>!;
1931 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1933 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1936 $prefix. join($separator, @result). $suffix;
1941 sub _condensed_description_generator {
1942 my ( $self, $format ) = ( shift, shift );
1944 my ( $f, $prefix, $suffix, $separator, $column ) =
1945 _condensed_generator_defaults($format);
1947 my $money_char = '$';
1948 if ($format eq 'latex') {
1949 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1951 $separator = " & \n";
1953 sub { my ($d,$a,$s,$w) = @_;
1954 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1956 $money_char = '\\dollar';
1957 }elsif ( $format eq 'html' ) {
1958 $prefix = '"><td align="center"></td>';
1962 sub { my ($d,$a,$s,$w) = @_;
1963 return qq!<td align="$html_align{$a}">$d</td>!;
1965 #$money_char = $conf->config('money_char') || '$';
1966 $money_char = ''; # this is madness
1974 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1976 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1978 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1979 map { $f->{$_}->[$i] } qw(align span width)
1983 $prefix. join( $separator, @result ). $suffix;
1988 sub _condensed_total_generator {
1989 my ( $self, $format ) = ( shift, shift );
1991 my ( $f, $prefix, $suffix, $separator, $column ) =
1992 _condensed_generator_defaults($format);
1995 if ($format eq 'latex') {
1998 $separator = " & \n";
2000 sub { my ($d,$a,$s,$w) = @_;
2001 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2003 }elsif ( $format eq 'html' ) {
2007 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2009 sub { my ($d,$a,$s,$w) = @_;
2010 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2019 # my $r = &{$f->{fields}->[$i]}(@args);
2020 # $r .= ' Total' unless $i;
2022 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2024 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
2025 map { $f->{$_}->[$i] } qw(align span width)
2029 $prefix. join( $separator, @result ). $suffix;
2034 =item total_line_generator FORMAT
2036 Returns a coderef used for generation of invoice total line items for this
2037 usage_class. FORMAT is either html or latex
2041 # should not be used: will have issues with hash element names (description vs
2042 # total_item and amount vs total_amount -- another array of functions?
2044 sub _condensed_total_line_generator {
2045 my ( $self, $format ) = ( shift, shift );
2047 my ( $f, $prefix, $suffix, $separator, $column ) =
2048 _condensed_generator_defaults($format);
2051 if ($format eq 'latex') {
2054 $separator = " & \n";
2056 sub { my ($d,$a,$s,$w) = @_;
2057 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
2059 }elsif ( $format eq 'html' ) {
2063 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
2065 sub { my ($d,$a,$s,$w) = @_;
2066 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
2075 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
2077 &{$column}( &{$f->{fields}->[$i]}(@args),
2078 map { $f->{$_}->[$i] } qw(align span width)
2082 $prefix. join( $separator, @result ). $suffix;
2087 # sub _items { # seems to be unused
2090 # #my @display = scalar(@_)
2092 # # : qw( _items_previous _items_pkg );
2093 # # #: qw( _items_pkg );
2094 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2095 # my @display = qw( _items_previous _items_pkg );
2098 # foreach my $display ( @display ) {
2099 # push @b, $self->$display(@_);
2104 =item _items_pkg [ OPTIONS ]
2106 Return line item hashes for each package item on this invoice. Nearly
2109 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2111 The only OPTIONS accepted is 'section', which may point to a hashref
2112 with a key named 'condensed', which may have a true value. If it
2113 does, this method tries to merge identical items into items with
2114 'quantity' equal to the number of items (not the sum of their
2115 separate quantities, for some reason).
2121 grep { $_->pkgnum } $self->cust_bill_pkg;
2128 warn "$me _items_pkg searching for all package line items\n"
2131 my @cust_bill_pkg = $self->_items_nontax;
2133 warn "$me _items_pkg filtering line items\n"
2135 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2137 if ($options{section} && $options{section}->{condensed}) {
2139 warn "$me _items_pkg condensing section\n"
2143 local $Storable::canonical = 1;
2144 foreach ( @items ) {
2146 delete $item->{ref};
2147 delete $item->{ext_description};
2148 my $key = freeze($item);
2149 $itemshash{$key} ||= 0;
2150 $itemshash{$key} ++; # += $item->{quantity};
2152 @items = sort { $a->{description} cmp $b->{description} }
2153 map { my $i = thaw($_);
2154 $i->{quantity} = $itemshash{$_};
2156 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2162 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2169 return 0 unless $a->itemdesc cmp $b->itemdesc;
2170 return -1 if $b->itemdesc eq 'Tax';
2171 return 1 if $a->itemdesc eq 'Tax';
2172 return -1 if $b->itemdesc eq 'Other surcharges';
2173 return 1 if $a->itemdesc eq 'Other surcharges';
2174 $a->itemdesc cmp $b->itemdesc;
2179 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2180 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2182 if ( $self->conf->exists('always_show_tax') ) {
2183 my $itemdesc = $self->conf->config('always_show_tax') || 'Tax';
2184 if (0 == grep { $_->{description} eq $itemdesc } @items) {
2186 { 'description' => $itemdesc,
2193 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2195 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2196 list of hashrefs describing the line items they generate on the invoice.
2198 OPTIONS may include:
2200 format: the invoice format.
2202 escape_function: the function used to escape strings.
2204 DEPRECATED? (expensive, mostly unused?)
2205 format_function: the function used to format CDRs.
2207 section: a hashref containing 'description'; if this is present,
2208 cust_bill_pkg_display records not belonging to this section are
2211 multisection: a flag indicating that this is a multisection invoice,
2212 which does something complicated.
2214 Returns a list of hashrefs, each of which may contain:
2216 pkgnum, description, amount, unit_amount, quantity, pkgpart, _is_setup, and
2217 ext_description, which is an arrayref of detail lines to show below
2222 sub _items_cust_bill_pkg {
2224 my $conf = $self->conf;
2225 my $cust_bill_pkgs = shift;
2228 my $format = $opt{format} || '';
2229 my $escape_function = $opt{escape_function} || sub { shift };
2230 my $format_function = $opt{format_function} || '';
2231 my $no_usage = $opt{no_usage} || '';
2232 my $unsquelched = $opt{unsquelched} || ''; #unused
2233 my $section = $opt{section}->{description} if $opt{section};
2234 my $summary_page = $opt{summary_page} || ''; #unused
2235 my $multisection = $opt{multisection} || '';
2236 my $discount_show_always = 0;
2238 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2240 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2241 # and location labels
2242 my $locale = $cust_main->locale;
2245 my ($s, $r, $u) = ( undef, undef, undef );
2246 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2249 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2250 if ( $_ && !$cust_bill_pkg->hidden ) {
2251 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2252 $_->{amount} =~ s/^\-0\.00$/0.00/;
2253 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2255 if $_->{amount} != 0
2256 || $discount_show_always
2257 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2258 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2264 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2265 ? $cust_bill_pkg->cust_bill_pkg_display
2266 : ( $cust_bill_pkg );
2268 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2269 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2272 foreach my $display ( grep { defined($section)
2273 ? $_->section eq $section
2276 grep { !$_->summary || $multisection }
2277 @cust_bill_pkg_display
2281 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2282 $display->billpkgdisplaynum. "\n"
2285 my $type = $display->type;
2287 my $desc = $cust_bill_pkg->desc( $cust_main->locale );
2288 $desc = substr($desc, 0, $maxlength). '...'
2289 if $format eq 'latex' && length($desc) > $maxlength;
2291 my %details_opt = ( 'format' => $format,
2292 'escape_function' => $escape_function,
2293 'format_function' => $format_function,
2294 'no_usage' => $opt{'no_usage'},
2297 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2299 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2302 if ( $cust_bill_pkg->setup != 0 ) {
2303 my $description = $desc;
2304 $description .= ' Setup'
2305 if $cust_bill_pkg->recur != 0
2306 || $discount_show_always
2307 || $cust_bill_pkg->recur_show_zero;
2309 'description' => $description,
2310 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2313 if ( $cust_bill_pkg->recur != 0 ) {
2315 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2316 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2320 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2322 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2325 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2327 # which pkgpart to show for display purposes?
2328 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
2330 # start/end dates for invoice formats that do nonstandard
2332 my %item_dates = ();
2333 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
2334 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
2336 if ( (!$type || $type eq 'S')
2337 && ( $cust_bill_pkg->setup != 0
2338 || $cust_bill_pkg->setup_show_zero
2343 warn "$me _items_cust_bill_pkg adding setup\n"
2346 my $description = $desc;
2347 $description .= ' Setup'
2348 if $cust_bill_pkg->recur != 0
2349 || $discount_show_always
2350 || $cust_bill_pkg->recur_show_zero;
2354 unless ( $cust_pkg->part_pkg->hide_svc_detail
2355 || $cust_bill_pkg->hidden )
2358 my @svc_labels = map &{$escape_function}($_),
2359 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2360 push @d, @svc_labels
2361 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2362 $svc_label = $svc_labels[0];
2364 if ( ! $cust_pkg->locationnum or
2365 $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2366 my $loc = $cust_pkg->location_label;
2367 $loc = substr($loc, 0, $maxlength). '...'
2368 if $format eq 'latex' && length($loc) > $maxlength;
2369 push @d, &{$escape_function}($loc);
2372 } #unless hiding service details
2374 push @d, $cust_bill_pkg->details(%details_opt)
2375 if $cust_bill_pkg->recur == 0;
2377 if ( $cust_bill_pkg->hidden ) {
2378 $s->{amount} += $cust_bill_pkg->setup;
2379 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2380 push @{ $s->{ext_description} }, @d;
2384 description => $description,
2385 pkgpart => $pkgpart,
2386 pkgnum => $cust_bill_pkg->pkgnum,
2387 amount => $cust_bill_pkg->setup,
2388 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2389 unit_amount => $cust_bill_pkg->unitsetup,
2390 quantity => $cust_bill_pkg->quantity,
2391 ext_description => \@d,
2392 svc_label => ($svc_label || ''),
2398 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2400 $cust_bill_pkg->recur != 0
2401 || $cust_bill_pkg->setup == 0
2402 || $discount_show_always
2403 || $cust_bill_pkg->recur_show_zero
2408 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2411 my $is_summary = $display->summary;
2412 my $description = ($is_summary && $type && $type eq 'U')
2413 ? "Usage charges" : $desc;
2415 my $part_pkg = $cust_pkg->part_pkg;
2417 #pry be a bit more efficient to look some of this conf stuff up
2420 $conf->exists('disable_line_item_date_ranges')
2421 || $part_pkg->option('disable_line_item_date_ranges',1)
2422 || ! $cust_bill_pkg->sdate
2423 || ! $cust_bill_pkg->edate
2426 my $date_style = '';
2427 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monhtly',
2428 $cust_main->agentnum
2430 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
2431 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
2432 $cust_main->agentnum
2434 if ( defined($date_style) && $date_style eq 'month_of' ) {
2435 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2436 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2437 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2438 $cust_main->agentnum
2440 $desc .= ' ' unless $desc =~ /\s$/;
2441 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2443 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2444 " - ". time2str($date_format, $cust_bill_pkg->edate);
2446 $description .= " ($time_period)";
2450 my @seconds = (); # for display of usage info
2453 #at least until cust_bill_pkg has "past" ranges in addition to
2454 #the "future" sdate/edate ones... see #3032
2455 my @dates = ( $self->_date );
2456 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2457 push @dates, $prev->sdate if $prev;
2458 push @dates, undef if !$prev;
2460 unless ( $part_pkg->hide_svc_detail
2461 || $cust_bill_pkg->itemdesc
2462 || $cust_bill_pkg->hidden
2463 || $is_summary && $type && $type eq 'U'
2467 warn "$me _items_cust_bill_pkg adding service details\n"
2470 my @svc_labels = map &{$escape_function}($_),
2471 $cust_pkg->h_labels_short($self->_date, undef, 'I');
2472 push @d, @svc_labels
2473 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2474 $svc_label = $svc_labels[0];
2476 warn "$me _items_cust_bill_pkg done adding service details\n"
2479 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2480 my $loc = $cust_pkg->location_label;
2481 $loc = substr($loc, 0, $maxlength). '...'
2482 if $format eq 'latex' && length($loc) > $maxlength;
2483 push @d, &{$escape_function}($loc);
2486 # Display of seconds_since_sqlradacct:
2487 # On the invoice, when processing @detail_items, look for a field
2488 # named 'seconds'. This will contain total seconds for each
2489 # service, in the same order as @ext_description. For services
2490 # that don't support this it will show undef.
2491 if ( $conf->exists('svc_acct-usage_seconds')
2492 and ! $cust_bill_pkg->pkgpart_override ) {
2493 foreach my $cust_svc (
2494 $cust_pkg->h_cust_svc(@dates, 'I')
2497 # eval because not having any part_export_usage exports
2498 # is a fatal error, last_bill/_date because that's how
2499 # sqlradius_hour billing does it
2501 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2503 push @seconds, $sec;
2505 } #if svc_acct-usage_seconds
2509 unless ( $is_summary ) {
2510 warn "$me _items_cust_bill_pkg adding details\n"
2513 #instead of omitting details entirely in this case (unwanted side
2514 # effects), just omit CDRs
2515 $details_opt{'no_usage'} = 1
2516 if $type && $type eq 'R';
2518 push @d, $cust_bill_pkg->details(%details_opt);
2521 warn "$me _items_cust_bill_pkg calculating amount\n"
2526 $amount = $cust_bill_pkg->recur;
2527 } elsif ($type eq 'R') {
2528 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2529 } elsif ($type eq 'U') {
2530 $amount = $cust_bill_pkg->usage;
2534 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
2537 if ( !$type || $type eq 'R' ) {
2539 warn "$me _items_cust_bill_pkg adding recur\n"
2542 if ( $cust_bill_pkg->hidden ) {
2543 $r->{amount} += $amount;
2544 $r->{unit_amount} += $unit_amount;
2545 push @{ $r->{ext_description} }, @d;
2548 description => $description,
2549 pkgpart => $pkgpart,
2550 pkgnum => $cust_bill_pkg->pkgnum,
2552 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2553 unit_amount => $unit_amount,
2554 quantity => $cust_bill_pkg->quantity,
2556 ext_description => \@d,
2557 svc_label => ($svc_label || ''),
2559 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2562 } else { # $type eq 'U'
2564 warn "$me _items_cust_bill_pkg adding usage\n"
2567 if ( $cust_bill_pkg->hidden ) {
2568 $u->{amount} += $amount;
2569 $u->{unit_amount} += $unit_amount,
2570 push @{ $u->{ext_description} }, @d;
2573 description => $description,
2574 pkgpart => $pkgpart,
2575 pkgnum => $cust_bill_pkg->pkgnum,
2577 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2578 unit_amount => $unit_amount,
2579 quantity => $cust_bill_pkg->quantity,
2581 ext_description => \@d,
2586 } # recurring or usage with recurring charge
2588 } else { #pkgnum tax or one-shot line item (??)
2590 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2593 if ( $cust_bill_pkg->setup != 0 ) {
2595 'description' => $desc,
2596 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2599 if ( $cust_bill_pkg->recur != 0 ) {
2601 'description' => "$desc (".
2602 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2603 time2str($date_format, $cust_bill_pkg->edate). ')',
2604 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2612 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2613 && $conf->exists('discount-show-always'));
2617 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2619 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2620 $_->{amount} =~ s/^\-0\.00$/0.00/;
2621 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2623 if $_->{amount} != 0
2624 || $discount_show_always
2625 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2626 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2630 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2637 =item _items_discounts_avail
2639 Returns an array of line item hashrefs representing available term discounts
2640 for this invoice. This makes the same assumptions that apply to term
2641 discounts in general: that the package is billed monthly, at a flat rate,
2642 with no usage charges. A prorated first month will be handled, as will
2643 a setup fee if the discount is allowed to apply to setup fees.
2647 sub _items_discounts_avail {
2650 #maybe move this method from cust_bill when quotations support discount_plans
2651 return () unless $self->can('discount_plans');
2652 my %plans = $self->discount_plans;
2654 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2655 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2659 my $plan = $plans{$months};
2661 my $term_total = sprintf('%.2f', $plan->discounted_total);
2662 my $percent = sprintf('%.0f',
2663 100 * (1 - $term_total / $plan->base_total) );
2664 my $permonth = sprintf('%.2f', $term_total / $months);
2665 my $detail = $self->mt('discount on item'). ' '.
2666 join(', ', map { "#$_" } $plan->pkgnums)
2669 # discounts for non-integer months don't work anyway
2670 $months = sprintf("%d", $months);
2673 description => $self->mt('Save [_1]% by paying for [_2] months',
2675 amount => $self->mt('[_1] ([_2] per month)',
2676 $term_total, $money_char.$permonth),
2677 ext_description => ($detail || ''),
2680 sort { $b <=> $a } keys %plans;