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->cust_main->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'});
177 =item print_generic OPTION => VALUE ...
179 Internal method - returns a filled-in template for this invoice as a scalar.
181 See print_ps and print_pdf for methods that return PostScript and PDF output.
183 Non optional options include
184 format - latex, html, template
186 Optional options include
188 template - a value used as a suffix for a configuration template
190 time - a value used to control the printing of overdue messages. The
191 default is now. It isn't the date of the invoice; that's the `_date' field.
192 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
193 L<Time::Local> and L<Date::Parse> for conversion functions.
197 unsquelch_cdr - overrides any per customer cdr squelching when true
199 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
201 locale - override customer's locale
205 #what's with all the sprintf('%10.2f')'s in here? will it cause any
206 # (alignment in text invoice?) problems to change them all to '%.2f' ?
207 # yes: fixed width/plain text printing will be borked
209 my( $self, %params ) = @_;
210 my $conf = $self->conf;
211 my $today = $params{today} ? $params{today} : time;
212 warn "$me print_generic called on $self with suffix $params{template}\n"
215 my $format = $params{format};
216 die "Unknown format: $format"
217 unless $format =~ /^(latex|html|template)$/;
219 my $cust_main = $self->cust_main || $self->prospect_main;
220 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
221 unless $cust_main->payname
222 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
224 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
225 'html' => [ '<%=', '%>' ],
226 'template' => [ '{', '}' ],
229 warn "$me print_generic creating template\n"
233 my $template = $params{template} ? $params{template} : $self->_agent_template;
234 my $templatefile = $self->template_conf. $format;
235 $templatefile .= "_$template"
236 if length($template) && $conf->exists($templatefile."_$template");
237 my @invoice_template = map "$_\n", $conf->config($templatefile)
238 or die "cannot load config data $templatefile";
241 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
242 #change this to a die when the old code is removed
243 warn "old-style invoice template $templatefile; ".
244 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
246 @invoice_template = _translate_old_latex_format(@invoice_template);
249 warn "$me print_generic creating T:T object\n"
252 my $text_template = new Text::Template(
254 SOURCE => \@invoice_template,
255 DELIMITERS => $delimiters{$format},
258 warn "$me print_generic compiling T:T object\n"
261 $text_template->compile()
262 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
265 # additional substitution could possibly cause breakage in existing templates
268 'notes' => sub { map "$_", @_ },
269 'footer' => sub { map "$_", @_ },
270 'smallfooter' => sub { map "$_", @_ },
271 'returnaddress' => sub { map "$_", @_ },
272 'coupon' => sub { map "$_", @_ },
273 'summary' => sub { map "$_", @_ },
279 s/%%(.*)$/<!-- $1 -->/g;
280 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
281 s/\\begin\{enumerate\}/<ol>/g;
283 s/\\end\{enumerate\}/<\/ol>/g;
284 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
293 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
295 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
301 s/\\hyphenation\{[\w\s\-]+}//;
306 'coupon' => sub { "" },
307 'summary' => sub { "" },
314 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
315 s/\\begin\{enumerate\}//g;
317 s/\\end\{enumerate\}//g;
318 s/\\textbf\{(.*)\}/$1/g;
325 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
327 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
332 s/\\\\\*?\s*$/\n/; # dubious
333 s/\\hyphenation\{[\w\s\-]+}//;
337 'coupon' => sub { "" },
338 'summary' => sub { "" },
343 # hashes for differing output formats
344 my %nbsps = ( 'latex' => '~',
345 'html' => '', # '&nbps;' would be nice
346 'template' => '', # not used
348 my $nbsp = $nbsps{$format};
350 my %escape_functions = ( 'latex' => \&_latex_escape,
351 'html' => \&_html_escape_nbsp,#\&encode_entities,
352 'template' => sub { shift },
354 my $escape_function = $escape_functions{$format};
355 my $escape_function_nonbsp = ($format eq 'html')
356 ? \&_html_escape : $escape_function;
358 my %date_formats = ( 'latex' => $date_format_long,
359 'html' => $date_format_long,
362 $date_formats{'html'} =~ s/ / /g;
364 my $date_format = $date_formats{$format};
366 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
368 'html' => sub { return '<b>'. shift(). '</b>'
370 'template' => sub { shift },
372 my $embolden_function = $embolden_functions{$format};
374 my %newline_tokens = ( 'latex' => '\\\\',
378 my $newline_token = $newline_tokens{$format};
380 warn "$me generating template variables\n"
383 # generate template variables
386 defined( $conf->config_orbase( "invoice_${format}returnaddress",
390 && length( $conf->config_orbase( "invoice_${format}returnaddress",
396 $returnaddress = join("\n",
397 $conf->config_orbase("invoice_${format}returnaddress", $template)
401 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
403 my $convert_map = $convert_maps{$format}{'returnaddress'};
406 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
411 } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
413 my $convert_map = $convert_maps{$format}{'returnaddress'};
414 $returnaddress = join( "\n", &$convert_map(
415 map { s/( {2,})/'~' x length($1)/eg;
419 ( $conf->config('company_name', $cust_main->agentnum),
420 $conf->config('company_address', $cust_main->agentnum),
427 my $warning = "Couldn't find a return address; ".
428 "do you need to set the company_address configuration value?";
430 $returnaddress = $nbsp;
431 #$returnaddress = $warning;
435 warn "$me generating invoice data\n"
438 my $agentnum = $cust_main->agentnum;
443 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
444 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
445 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
446 'returnaddress' => $returnaddress,
447 'agent' => &$escape_function($cust_main->agent->agent),
449 #invoice/quotation info
450 'invnum' => $self->invnum,
451 'quotationnum' => $self->quotationnum,
452 'date' => time2str($date_format, $self->_date),
453 'today' => time2str($date_format_long, $today),
454 'terms' => $self->terms,
455 'template' => $template, #params{'template'},
456 'notice_name' => ($params{'notice_name'} || $self->notice_name),#escape_function?
457 'current_charges' => sprintf("%.2f", $self->charged),
458 'duedate' => $self->due_date2str($rdate_format), #date_format?
461 'custnum' => $cust_main->display_custnum,
462 'prospectnum' => $cust_main->prospectnum,
463 'agent_custid' => &$escape_function($cust_main->agent_custid),
464 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
465 payname company address1 address2 city state zip fax
469 'ship_enable' => $conf->exists('invoice-ship_address'),
470 'unitprices' => $conf->exists('invoice-unitprice'),
471 'smallernotes' => $conf->exists('invoice-smallernotes'),
472 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
473 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
475 #layout info -- would be fancy to calc some of this and bury the template
477 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
478 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
479 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
480 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
481 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
482 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
483 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
484 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
485 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
486 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
488 # better hang on to conf_dir for a while (for old templates)
489 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
491 #these are only used when doing paged plaintext
498 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
499 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
500 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
501 # eval to avoid death for unimplemented languages
502 my $dh = eval { Date::Language->new($info{'name'}) } ||
503 Date::Language->new(); # fall back to English
504 # prototype here to silence warnings
505 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
506 # eventually use this date handle everywhere in here, too
508 my $min_sdate = 999999999999;
510 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
511 next unless $cust_bill_pkg->pkgnum > 0;
512 $min_sdate = $cust_bill_pkg->sdate
513 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
514 $max_edate = $cust_bill_pkg->edate
515 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
518 $invoice_data{'bill_period'} = '';
519 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
520 . " to " . time2str('%e %h', $max_edate)
521 if ($max_edate != 0 && $min_sdate != 999999999999);
523 $invoice_data{finance_section} = '';
524 if ( $conf->config('finance_pkgclass') ) {
526 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
527 $invoice_data{finance_section} = $pkg_class->categoryname;
529 $invoice_data{finance_amount} = '0.00';
530 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
532 my $countrydefault = $conf->config('countrydefault') || 'US';
533 foreach ( qw( address1 address2 city state zip country fax) ){
534 my $method = 'ship_'.$_;
535 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
537 foreach ( qw( contact company ) ) { #compatibility
538 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
540 $invoice_data{'ship_country'} = ''
541 if ( $invoice_data{'ship_country'} eq $countrydefault );
543 $invoice_data{'cid'} = $params{'cid'}
546 if ( $cust_main->country eq $countrydefault ) {
547 $invoice_data{'country'} = '';
549 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
553 $invoice_data{'address'} = \@address;
556 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
557 ? " (P.O. #". $cust_main->payinfo. ")"
561 push @address, $cust_main->company
562 if $cust_main->company;
563 push @address, $cust_main->address1;
564 push @address, $cust_main->address2
565 if $cust_main->address2;
567 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
568 push @address, $invoice_data{'country'}
569 if $invoice_data{'country'};
571 while (scalar(@address) < 5);
573 $invoice_data{'logo_file'} = $params{'logo_file'}
574 if $params{'logo_file'};
575 $invoice_data{'barcode_file'} = $params{'barcode_file'}
576 if $params{'barcode_file'};
577 $invoice_data{'barcode_img'} = $params{'barcode_img'}
578 if $params{'barcode_img'};
579 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
580 if $params{'barcode_cid'};
582 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
583 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
584 #my $balance_due = $self->owed + $pr_total - $cr_total;
585 my $balance_due = $self->owed + $pr_total;
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 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
596 # the sum of amount owed on all invoices
597 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
599 # info from customer's last invoice before this one, for some
601 $invoice_data{'last_bill'} = {};
602 my $last_bill = $pr_cust_bill[-1];
604 $invoice_data{'last_bill'} = {
605 '_date' => $last_bill->_date, #unformatted
606 # all we need for now
610 my $summarypage = '';
611 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
614 $invoice_data{'summarypage'} = $summarypage;
616 warn "$me substituting variables in notes, footer, smallfooter\n"
619 my $tc = $self->template_conf;
620 my @include = ( [ $tc, 'notes' ],
621 [ 'invoice_', 'footer' ],
622 [ 'invoice_', 'smallfooter', ],
624 push @include, [ $tc, 'coupon', ]
625 unless $params{'no_coupon'};
627 foreach my $i (@include) {
629 my($base, $include) = @$i;
631 my $inc_file = $conf->key_orbase("$base$format$include", $template);
634 if ( $conf->exists($inc_file, $agentnum)
635 && length( $conf->config($inc_file, $agentnum) ) ) {
637 @inc_src = $conf->config($inc_file, $agentnum);
641 $inc_file = $conf->key_orbase("${base}latex$include", $template);
643 my $convert_map = $convert_maps{$format}{$include};
645 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
646 s/--\@\]/$delimiters{$format}[1]/g;
649 &$convert_map( $conf->config($inc_file, $agentnum) );
653 my $inc_tt = new Text::Template (
655 SOURCE => [ map "$_\n", @inc_src ],
656 DELIMITERS => $delimiters{$format},
657 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
659 unless ( $inc_tt->compile() ) {
660 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
661 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
665 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
667 $invoice_data{$include} =~ s/\n+$//
668 if ($format eq 'latex');
671 # let invoices use either of these as needed
672 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
673 ? $cust_main->payinfo : '';
674 $invoice_data{'po_line'} =
675 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
676 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
679 my %money_chars = ( 'latex' => '',
680 'html' => $conf->config('money_char') || '$',
683 my $money_char = $money_chars{$format};
685 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
686 'html' => $conf->config('money_char') || '$',
689 my $other_money_char = $other_money_chars{$format};
690 $invoice_data{'dollar'} = $other_money_char;
692 my @detail_items = ();
693 my @total_items = ();
697 $invoice_data{'detail_items'} = \@detail_items;
698 $invoice_data{'total_items'} = \@total_items;
699 $invoice_data{'buf'} = \@buf;
700 $invoice_data{'sections'} = \@sections;
702 warn "$me generating sections\n"
705 # Previous Charges section
706 # subtotal is the first return value from $self->previous
707 my $previous_section = { 'description' => $self->mt('Previous Charges'),
708 'subtotal' => $other_money_char.
709 sprintf('%.2f', $pr_total),
710 'summarized' => '', #why? $summarypage ? 'Y' : '',
712 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
713 join(' / ', map { $cust_main->balance_date_range(@$_) }
714 $self->_prior_month30s
716 if $conf->exists('invoice_include_aging');
719 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
720 'subtotal' => $taxtotal, # adjusted below
722 my $tax_weight = _pkg_category($tax_section->{description})
723 ? _pkg_category($tax_section->{description})->weight
725 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
726 $tax_section->{'sort_weight'} = $tax_weight;
730 my $adjust_section = { 'description' =>
731 $self->mt('Credits, Payments, and Adjustments'),
732 'subtotal' => 0, # adjusted below
734 my $adjust_weight = _pkg_category($adjust_section->{description})
735 ? _pkg_category($adjust_section->{description})->weight
737 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
738 $adjust_section->{'sort_weight'} = $adjust_weight;
740 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
741 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
742 $invoice_data{'multisection'} = $multisection;
743 my $late_sections = [];
744 my $extra_sections = [];
745 my $extra_lines = ();
747 my $default_section = { 'description' => '',
752 if ( $multisection ) {
753 ($extra_sections, $extra_lines) =
754 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
755 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
756 && $self->can('_items_extra_usage_sections');
758 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
760 push @detail_items, @$extra_lines if $extra_lines;
762 $self->_items_sections( $late_sections, # this could stand a refactor
764 $escape_function_nonbsp,
768 if ( $conf->exists('svc_phone_sections')
769 && $self->can('_items_svc_phone_sections')
772 my ($phone_sections, $phone_lines) =
773 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
774 push @{$late_sections}, @$phone_sections;
775 push @detail_items, @$phone_lines;
777 if ( $conf->exists('voip-cust_accountcode_cdr')
778 && $cust_main->accountcode_cdr
779 && $self->can('_items_accountcode_cdr')
782 my ($accountcode_section, $accountcode_lines) =
783 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
784 if ( scalar(@$accountcode_lines) ) {
785 push @{$late_sections}, $accountcode_section;
786 push @detail_items, @$accountcode_lines;
789 } else {# not multisection
790 # make a default section
791 push @sections, $default_section;
792 # and calculate the finance charge total, since it won't get done otherwise.
793 # XXX possibly other totals?
794 # XXX possibly finance_pkgclass should not be used in this manner?
795 if ( $conf->exists('finance_pkgclass') ) {
797 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
798 if ( grep { $_->section eq $invoice_data{finance_section} }
799 $cust_bill_pkg->cust_bill_pkg_display ) {
800 # I think these are always setup fees, but just to be sure...
801 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
804 $invoice_data{finance_amount} =
805 sprintf('%.2f', sum( @finance_charges ) || 0);
809 # previous invoice balances in the Previous Charges section if there
810 # is one, otherwise in the main detail section
811 if ( $self->can('_items_previous') &&
812 $self->enable_previous &&
813 ! $conf->exists('previous_balance-summary_only') ) {
815 warn "$me adding previous balances\n"
818 foreach my $line_item ( $self->_items_previous ) {
821 ext_description => [],
823 $detail->{'ref'} = $line_item->{'pkgnum'};
824 $detail->{'quantity'} = 1;
825 $detail->{'section'} = $multisection ? $previous_section
827 $detail->{'description'} = &$escape_function($line_item->{'description'});
828 if ( exists $line_item->{'ext_description'} ) {
829 @{$detail->{'ext_description'}} = map {
830 &$escape_function($_);
831 } @{$line_item->{'ext_description'}};
833 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
834 $line_item->{'amount'};
835 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
837 push @detail_items, $detail;
838 push @buf, [ $detail->{'description'},
839 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
845 if ( @pr_cust_bill && $self->enable_previous ) {
846 push @buf, ['','-----------'];
847 push @buf, [ $self->mt('Total Previous Balance'),
848 $money_char. sprintf("%10.2f", $pr_total) ];
852 if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
853 warn "$me adding DID summary\n"
856 my ($didsummary,$minutes) = $self->_did_summary;
857 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
859 { 'description' => $didsummary_desc,
860 'ext_description' => [ $didsummary, $minutes ],
864 foreach my $section (@sections, @$late_sections) {
866 warn "$me adding section \n". Dumper($section)
869 # begin some normalization
870 $section->{'subtotal'} = $section->{'amount'}
872 && !exists($section->{subtotal})
873 && exists($section->{amount});
875 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
876 if ( $invoice_data{finance_section} &&
877 $section->{'description'} eq $invoice_data{finance_section} );
879 $section->{'subtotal'} = $other_money_char.
880 sprintf('%.2f', $section->{'subtotal'})
883 # continue some normalization
884 $section->{'amount'} = $section->{'subtotal'}
888 if ( $section->{'description'} ) {
889 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
894 warn "$me setting options\n"
897 my $multilocation = scalar($cust_main->cust_location); #too expensive?
899 $options{'section'} = $section if $multisection;
900 $options{'format'} = $format;
901 $options{'escape_function'} = $escape_function;
902 $options{'no_usage'} = 1 unless $unsquelched;
903 $options{'unsquelched'} = $unsquelched;
904 $options{'summary_page'} = $summarypage;
905 $options{'skip_usage'} =
906 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
907 $options{'multilocation'} = $multilocation;
908 $options{'multisection'} = $multisection;
910 warn "$me searching for line items\n"
913 foreach my $line_item ( $self->_items_pkg(%options) ) {
915 warn "$me adding line item $line_item\n"
919 ext_description => [],
921 $detail->{'ref'} = $line_item->{'pkgnum'};
922 $detail->{'quantity'} = $line_item->{'quantity'};
923 $detail->{'section'} = $section;
924 $detail->{'description'} = &$escape_function($line_item->{'description'});
925 if ( exists $line_item->{'ext_description'} ) {
926 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
928 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
929 $line_item->{'amount'};
930 if ( exists $line_item->{'unit_amount'} ) {
931 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
932 $line_item->{'unit_amount'};
934 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
936 $detail->{'sdate'} = $line_item->{'sdate'};
937 $detail->{'edate'} = $line_item->{'edate'};
938 $detail->{'seconds'} = $line_item->{'seconds'};
940 push @detail_items, $detail;
941 push @buf, ( [ $detail->{'description'},
942 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
944 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
948 if ( $section->{'description'} ) {
949 push @buf, ( ['','-----------'],
950 [ $section->{'description'}. ' sub-total',
951 $section->{'subtotal'} # already formatted this
960 $invoice_data{current_less_finance} =
961 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
963 # create a major section for previous balance if we have major sections,
964 # or if previous_section is in summary form
965 if ( ( $multisection && $self->enable_previous )
966 || $conf->exists('previous_balance-summary_only') )
968 unshift @sections, $previous_section if $pr_total;
971 warn "$me adding taxes\n"
974 foreach my $tax ( $self->_items_tax ) {
976 $taxtotal += $tax->{'amount'};
978 my $description = &$escape_function( $tax->{'description'} );
979 my $amount = sprintf( '%.2f', $tax->{'amount'} );
981 if ( $multisection ) {
983 my $money = $old_latex ? '' : $money_char;
984 push @detail_items, {
985 ext_description => [],
988 description => $description,
989 amount => $money. $amount,
991 section => $tax_section,
997 'total_item' => $description,
998 'total_amount' => $other_money_char. $amount,
1003 push @buf,[ $description,
1004 $money_char. $amount,
1011 $total->{'total_item'} = $self->mt('Sub-total');
1012 $total->{'total_amount'} =
1013 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1015 if ( $multisection ) {
1016 $tax_section->{'subtotal'} = $other_money_char.
1017 sprintf('%.2f', $taxtotal);
1018 $tax_section->{'pretotal'} = 'New charges sub-total '.
1019 $total->{'total_amount'};
1020 push @sections, $tax_section if $taxtotal;
1022 unshift @total_items, $total;
1025 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1027 push @buf,['','-----------'];
1028 push @buf,[$self->mt(
1029 (!$self->enable_previous)
1031 : 'Total New Charges'
1033 $money_char. sprintf("%10.2f",$self->charged) ];
1036 # calculate total, possibly including total owed on previous
1041 $item = $conf->config('previous_balance-exclude_from_total')
1042 || 'Total New Charges'
1043 if $conf->exists('previous_balance-exclude_from_total');
1044 my $amount = $self->charged;
1045 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1046 $amount += $pr_total;
1049 $total->{'total_item'} = &$embolden_function($self->mt($item));
1050 $total->{'total_amount'} =
1051 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1052 if ( $multisection ) {
1053 if ( $adjust_section->{'sort_weight'} ) {
1054 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1055 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1057 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1058 $other_money_char. sprintf('%.2f', $self->charged );
1061 push @total_items, $total;
1063 push @buf,['','-----------'];
1066 sprintf( '%10.2f', $amount )
1071 # if we're showing previous invoices, also show previous
1072 # credits and payments
1073 if ( $self->enable_previous
1074 and $self->can('_items_credits')
1075 and $self->can('_items_payments') )
1077 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1080 my $credittotal = 0;
1081 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1084 $total->{'total_item'} = &$escape_function($credit->{'description'});
1085 $credittotal += $credit->{'amount'};
1086 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1087 $adjusttotal += $credit->{'amount'};
1088 if ( $multisection ) {
1089 my $money = $old_latex ? '' : $money_char;
1090 push @detail_items, {
1091 ext_description => [],
1094 description => &$escape_function($credit->{'description'}),
1095 amount => $money. $credit->{'amount'},
1097 section => $adjust_section,
1100 push @total_items, $total;
1104 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1107 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1108 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1112 my $paymenttotal = 0;
1113 foreach my $payment ( $self->_items_payments ) {
1115 $total->{'total_item'} = &$escape_function($payment->{'description'});
1116 $paymenttotal += $payment->{'amount'};
1117 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1118 $adjusttotal += $payment->{'amount'};
1119 if ( $multisection ) {
1120 my $money = $old_latex ? '' : $money_char;
1121 push @detail_items, {
1122 ext_description => [],
1125 description => &$escape_function($payment->{'description'}),
1126 amount => $money. $payment->{'amount'},
1128 section => $adjust_section,
1131 push @total_items, $total;
1133 push @buf, [ $payment->{'description'},
1134 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1137 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1139 if ( $multisection ) {
1140 $adjust_section->{'subtotal'} = $other_money_char.
1141 sprintf('%.2f', $adjusttotal);
1142 push @sections, $adjust_section
1143 unless $adjust_section->{sort_weight};
1146 # create Balance Due message
1149 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1150 $total->{'total_amount'} =
1151 &$embolden_function(
1152 $other_money_char. sprintf('%.2f', $summarypage
1154 $self->billing_balance
1155 : $self->owed + $pr_total
1158 if ( $multisection && !$adjust_section->{sort_weight} ) {
1159 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1160 $total->{'total_amount'};
1162 push @total_items, $total;
1164 push @buf,['','-----------'];
1165 push @buf,[$self->balance_due_msg, $money_char.
1166 sprintf("%10.2f", $balance_due ) ];
1169 if ( $conf->exists('previous_balance-show_credit')
1170 and $cust_main->balance < 0 ) {
1171 my $credit_total = {
1172 'total_item' => &$embolden_function($self->credit_balance_msg),
1173 'total_amount' => &$embolden_function(
1174 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1177 if ( $multisection ) {
1178 $adjust_section->{'posttotal'} .= $newline_token .
1179 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1182 push @total_items, $credit_total;
1184 push @buf,['','-----------'];
1185 push @buf,[$self->credit_balance_msg, $money_char.
1186 sprintf("%10.2f", -$cust_main->balance ) ];
1190 if ( $multisection ) {
1191 if ( $conf->exists('svc_phone_sections')
1192 && $self->can('_items_svc_phone_sections')
1196 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1197 $total->{'total_amount'} =
1198 &$embolden_function(
1199 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1201 my $last_section = pop @sections;
1202 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1203 $total->{'total_amount'};
1204 push @sections, $last_section;
1206 push @sections, @$late_sections
1210 # make a discounts-available section, even without multisection
1211 if ( $conf->exists('discount-show_available')
1212 and my @discounts_avail = $self->_items_discounts_avail ) {
1213 my $discount_section = {
1214 'description' => $self->mt('Discounts Available'),
1219 push @sections, $discount_section;
1220 push @detail_items, map { +{
1221 'ref' => '', #should this be something else?
1222 'section' => $discount_section,
1223 'description' => &$escape_function( $_->{description} ),
1224 'amount' => $money_char . &$escape_function( $_->{amount} ),
1225 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1226 } } @discounts_avail;
1229 # All sections and items are built; now fill in templates.
1230 my @includelist = ();
1231 push @includelist, 'summary' if $summarypage;
1232 foreach my $include ( @includelist ) {
1234 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1237 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1239 @inc_src = $conf->config($inc_file, $agentnum);
1243 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1245 my $convert_map = $convert_maps{$format}{$include};
1247 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1248 s/--\@\]/$delimiters{$format}[1]/g;
1251 &$convert_map( $conf->config($inc_file, $agentnum) );
1255 my $inc_tt = new Text::Template (
1257 SOURCE => [ map "$_\n", @inc_src ],
1258 DELIMITERS => $delimiters{$format},
1259 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1261 unless ( $inc_tt->compile() ) {
1262 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1263 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1267 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1269 $invoice_data{$include} =~ s/\n+$//
1270 if ($format eq 'latex');
1275 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1276 /invoice_lines\((\d*)\)/;
1277 $invoice_lines += $1 || scalar(@buf);
1280 die "no invoice_lines() functions in template?"
1281 if ( $format eq 'template' && !$wasfunc );
1283 if ($format eq 'template') {
1285 if ( $invoice_lines ) {
1286 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1287 $invoice_data{'total_pages'}++
1288 if scalar(@buf) % $invoice_lines;
1291 #setup subroutine for the template
1292 $invoice_data{invoice_lines} = sub {
1293 my $lines = shift || scalar(@buf);
1305 push @collect, split("\n",
1306 $text_template->fill_in( HASH => \%invoice_data )
1308 $invoice_data{'page'}++;
1310 map "$_\n", @collect;
1312 } else { # this is where we actually create the invoice
1314 warn "filling in template for invoice ". $self->invnum. "\n"
1316 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1319 $text_template->fill_in(HASH => \%invoice_data);
1323 sub notice_name { '('.shift->table.')'; }
1325 sub template_conf { 'invoice_'; }
1327 # helper routine for generating date ranges
1328 sub _prior_month30s {
1331 [ 1, 2592000 ], # 0-30 days ago
1332 [ 2592000, 5184000 ], # 30-60 days ago
1333 [ 5184000, 7776000 ], # 60-90 days ago
1334 [ 7776000, 0 ], # 90+ days ago
1337 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1338 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1343 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1345 Returns an postscript invoice, as a scalar.
1347 Options can be passed as a hashref (recommended) or as a list of time, template
1348 and then any key/value pairs for any other options.
1350 I<time> an optional value used to control the printing of overdue messages. The
1351 default is now. It isn't the date of the invoice; that's the `_date' field.
1352 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1353 L<Time::Local> and L<Date::Parse> for conversion functions.
1355 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1362 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1363 my $ps = generate_ps($file);
1365 unlink($barcodefile) if $barcodefile;
1370 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1372 Returns an PDF invoice, as a scalar.
1374 Options can be passed as a hashref (recommended) or as a list of time, template
1375 and then any key/value pairs for any other options.
1377 I<time> an optional value used to control the printing of overdue messages. The
1378 default is now. It isn't the date of the invoice; that's the `_date' field.
1379 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1380 L<Time::Local> and L<Date::Parse> for conversion functions.
1382 I<template>, if specified, is the name of a suffix for alternate invoices.
1384 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1391 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1392 my $pdf = generate_pdf($file);
1394 unlink($barcodefile) if $barcodefile;
1399 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1401 Returns an HTML invoice, as a scalar.
1403 I<time> an optional value used to control the printing of overdue messages. The
1404 default is now. It isn't the date of the invoice; that's the `_date' field.
1405 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1406 L<Time::Local> and L<Date::Parse> for conversion functions.
1408 I<template>, if specified, is the name of a suffix for alternate invoices.
1410 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1412 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1413 when emailing the invoice as part of a multipart/related MIME email.
1421 %params = %{ shift() };
1423 $params{'time'} = shift;
1424 $params{'template'} = shift;
1425 $params{'cid'} = shift;
1428 $params{'format'} = 'html';
1430 $self->print_generic( %params );
1433 # quick subroutine for print_latex
1435 # There are ten characters that LaTeX treats as special characters, which
1436 # means that they do not simply typeset themselves:
1437 # # $ % & ~ _ ^ \ { }
1439 # TeX ignores blanks following an escaped character; if you want a blank (as
1440 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1444 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1445 $value =~ s/([<>])/\$$1\$/g;
1451 encode_entities($value);
1455 sub _html_escape_nbsp {
1456 my $value = _html_escape(shift);
1457 $value =~ s/ +/ /g;
1461 #utility methods for print_*
1463 sub _translate_old_latex_format {
1464 warn "_translate_old_latex_format called\n"
1471 if ( $line =~ /^%%Detail\s*$/ ) {
1473 push @template, q![@--!,
1474 q! foreach my $_tr_line (@detail_items) {!,
1475 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1476 q! $_tr_line->{'description'} .= !,
1477 q! "\\tabularnewline\n~~".!,
1478 q! join( "\\tabularnewline\n~~",!,
1479 q! @{$_tr_line->{'ext_description'}}!,
1483 while ( ( my $line_item_line = shift )
1484 !~ /^%%EndDetail\s*$/ ) {
1485 $line_item_line =~ s/'/\\'/g; # nice LTS
1486 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1487 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1488 push @template, " \$OUT .= '$line_item_line';";
1491 push @template, '}',
1494 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1496 push @template, '[@--',
1497 ' foreach my $_tr_line (@total_items) {';
1499 while ( ( my $total_item_line = shift )
1500 !~ /^%%EndTotalDetails\s*$/ ) {
1501 $total_item_line =~ s/'/\\'/g; # nice LTS
1502 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1503 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1504 push @template, " \$OUT .= '$total_item_line';";
1507 push @template, '}',
1511 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1512 push @template, $line;
1518 warn "$_\n" foreach @template;
1526 my $conf = $self->conf;
1528 #check for an invoice-specific override
1529 return $self->invoice_terms if $self->invoice_terms;
1531 #check for a customer- specific override
1532 my $cust_main = $self->cust_main;
1533 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1535 #use configured default
1536 $conf->config('invoice_default_terms') || '';
1542 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1543 $duedate = $self->_date() + ( $1 * 86400 );
1550 $self->due_date ? time2str(shift, $self->due_date) : '';
1553 sub balance_due_msg {
1555 my $msg = $self->mt('Balance Due');
1556 return $msg unless $self->terms;
1557 if ( $self->due_date ) {
1558 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1559 $self->due_date2str($date_format);
1560 } elsif ( $self->terms ) {
1561 $msg .= ' - '. $self->terms;
1566 sub balance_due_date {
1568 my $conf = $self->conf;
1570 if ( $conf->exists('invoice_default_terms')
1571 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1572 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1577 sub credit_balance_msg {
1579 $self->mt('Credit Balance Remaining')
1584 Returns a string with the date, for example: "3/20/2008"
1590 time2str($date_format, $self->_date);
1593 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1595 Generate section information for all items appearing on this invoice.
1596 This will only be called for multi-section invoices.
1598 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1599 related display records (L<FS::cust_bill_pkg_display>) and organize
1600 them into two groups ("early" and "late" according to whether they come
1601 before or after the total), then into sections. A subtotal is calculated
1604 Section descriptions are returned in sort weight order. Each consists
1605 of a hash containing:
1607 description: the package category name, escaped
1608 subtotal: the total charges in that section
1609 tax_section: a flag indicating that the section contains only tax charges
1610 summarized: same as tax_section, for some reason
1611 sort_weight: the package category's sort weight
1613 If 'condense' is set on the display record, it also contains everything
1614 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1615 coderefs to generate parts of the invoice. This is not advised.
1619 LATE: an arrayref to push the "late" section hashes onto. The "early"
1620 group is simply returned from the method.
1622 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1623 Turning this on has the following effects:
1624 - Ignores display items with the 'summary' flag.
1625 - Combines all items into the "early" group.
1626 - Creates sections for all non-disabled package categories, even if they
1627 have no charges on this invoice, as well as a section with no name.
1629 ESCAPE: an escape function to use for section titles.
1631 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1632 sorted list. If there are any of these, section subtotals exclude
1635 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1636 passed through to C<_condense_section()>.
1640 use vars qw(%pkg_category_cache);
1641 sub _items_sections {
1644 my $summarypage = shift;
1646 my $extra_sections = shift;
1650 my %late_subtotal = ();
1653 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1656 my $usage = $cust_bill_pkg->usage;
1658 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1659 next if ( $display->summary && $summarypage );
1661 my $section = $display->section;
1662 my $type = $display->type;
1664 $not_tax{$section} = 1
1665 unless $cust_bill_pkg->pkgnum == 0;
1667 if ( $display->post_total && !$summarypage ) {
1668 if (! $type || $type eq 'S') {
1669 $late_subtotal{$section} += $cust_bill_pkg->setup
1670 if $cust_bill_pkg->setup != 0
1671 || $cust_bill_pkg->setup_show_zero;
1675 $late_subtotal{$section} += $cust_bill_pkg->recur
1676 if $cust_bill_pkg->recur != 0
1677 || $cust_bill_pkg->recur_show_zero;
1680 if ($type && $type eq 'R') {
1681 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1682 if $cust_bill_pkg->recur != 0
1683 || $cust_bill_pkg->recur_show_zero;
1686 if ($type && $type eq 'U') {
1687 $late_subtotal{$section} += $usage
1688 unless scalar(@$extra_sections);
1693 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1695 if (! $type || $type eq 'S') {
1696 $subtotal{$section} += $cust_bill_pkg->setup
1697 if $cust_bill_pkg->setup != 0
1698 || $cust_bill_pkg->setup_show_zero;
1702 $subtotal{$section} += $cust_bill_pkg->recur
1703 if $cust_bill_pkg->recur != 0
1704 || $cust_bill_pkg->recur_show_zero;
1707 if ($type && $type eq 'R') {
1708 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1709 if $cust_bill_pkg->recur != 0
1710 || $cust_bill_pkg->recur_show_zero;
1713 if ($type && $type eq 'U') {
1714 $subtotal{$section} += $usage
1715 unless scalar(@$extra_sections);
1724 %pkg_category_cache = ();
1726 push @$late, map { { 'description' => &{$escape}($_),
1727 'subtotal' => $late_subtotal{$_},
1729 'sort_weight' => ( _pkg_category($_)
1730 ? _pkg_category($_)->weight
1733 ((_pkg_category($_) && _pkg_category($_)->condense)
1734 ? $self->_condense_section($format)
1738 sort _sectionsort keys %late_subtotal;
1741 if ( $summarypage ) {
1742 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1743 map { $_->categoryname } qsearch('pkg_category', {});
1744 push @sections, '' if exists($subtotal{''});
1746 @sections = keys %subtotal;
1749 my @early = map { { 'description' => &{$escape}($_),
1750 'subtotal' => $subtotal{$_},
1751 'summarized' => $not_tax{$_} ? '' : 'Y',
1752 'tax_section' => $not_tax{$_} ? '' : 'Y',
1753 'sort_weight' => ( _pkg_category($_)
1754 ? _pkg_category($_)->weight
1757 ((_pkg_category($_) && _pkg_category($_)->condense)
1758 ? $self->_condense_section($format)
1763 push @early, @$extra_sections if $extra_sections;
1765 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1769 #helper subs for above
1772 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1776 my $categoryname = shift;
1777 $pkg_category_cache{$categoryname} ||=
1778 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1781 my %condensed_format = (
1782 'label' => [ qw( Description Qty Amount ) ],
1784 sub { shift->{description} },
1785 sub { shift->{quantity} },
1786 sub { my($href, %opt) = @_;
1787 ($opt{dollar} || ''). $href->{amount};
1790 'align' => [ qw( l r r ) ],
1791 'span' => [ qw( 5 1 1 ) ], # unitprices?
1792 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1795 sub _condense_section {
1796 my ( $self, $format ) = ( shift, shift );
1798 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1799 qw( description_generator
1802 total_line_generator
1807 sub _condensed_generator_defaults {
1808 my ( $self, $format ) = ( shift, shift );
1809 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1818 sub _condensed_header_generator {
1819 my ( $self, $format ) = ( shift, shift );
1821 my ( $f, $prefix, $suffix, $separator, $column ) =
1822 _condensed_generator_defaults($format);
1824 if ($format eq 'latex') {
1825 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1826 $suffix = "\\\\\n\\hline";
1829 sub { my ($d,$a,$s,$w) = @_;
1830 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1832 } elsif ( $format eq 'html' ) {
1833 $prefix = '<th></th>';
1837 sub { my ($d,$a,$s,$w) = @_;
1838 return qq!<th align="$html_align{$a}">$d</th>!;
1846 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1848 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1851 $prefix. join($separator, @result). $suffix;
1856 sub _condensed_description_generator {
1857 my ( $self, $format ) = ( shift, shift );
1859 my ( $f, $prefix, $suffix, $separator, $column ) =
1860 _condensed_generator_defaults($format);
1862 my $money_char = '$';
1863 if ($format eq 'latex') {
1864 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1866 $separator = " & \n";
1868 sub { my ($d,$a,$s,$w) = @_;
1869 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1871 $money_char = '\\dollar';
1872 }elsif ( $format eq 'html' ) {
1873 $prefix = '"><td align="center"></td>';
1877 sub { my ($d,$a,$s,$w) = @_;
1878 return qq!<td align="$html_align{$a}">$d</td>!;
1880 #$money_char = $conf->config('money_char') || '$';
1881 $money_char = ''; # this is madness
1889 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1891 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1893 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1894 map { $f->{$_}->[$i] } qw(align span width)
1898 $prefix. join( $separator, @result ). $suffix;
1903 sub _condensed_total_generator {
1904 my ( $self, $format ) = ( shift, shift );
1906 my ( $f, $prefix, $suffix, $separator, $column ) =
1907 _condensed_generator_defaults($format);
1910 if ($format eq 'latex') {
1913 $separator = " & \n";
1915 sub { my ($d,$a,$s,$w) = @_;
1916 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1918 }elsif ( $format eq 'html' ) {
1922 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1924 sub { my ($d,$a,$s,$w) = @_;
1925 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1934 # my $r = &{$f->{fields}->[$i]}(@args);
1935 # $r .= ' Total' unless $i;
1937 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1939 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1940 map { $f->{$_}->[$i] } qw(align span width)
1944 $prefix. join( $separator, @result ). $suffix;
1949 =item total_line_generator FORMAT
1951 Returns a coderef used for generation of invoice total line items for this
1952 usage_class. FORMAT is either html or latex
1956 # should not be used: will have issues with hash element names (description vs
1957 # total_item and amount vs total_amount -- another array of functions?
1959 sub _condensed_total_line_generator {
1960 my ( $self, $format ) = ( shift, shift );
1962 my ( $f, $prefix, $suffix, $separator, $column ) =
1963 _condensed_generator_defaults($format);
1966 if ($format eq 'latex') {
1969 $separator = " & \n";
1971 sub { my ($d,$a,$s,$w) = @_;
1972 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1974 }elsif ( $format eq 'html' ) {
1978 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1980 sub { my ($d,$a,$s,$w) = @_;
1981 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1990 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1992 &{$column}( &{$f->{fields}->[$i]}(@args),
1993 map { $f->{$_}->[$i] } qw(align span width)
1997 $prefix. join( $separator, @result ). $suffix;
2002 # sub _items { # seems to be unused
2005 # #my @display = scalar(@_)
2007 # # : qw( _items_previous _items_pkg );
2008 # # #: qw( _items_pkg );
2009 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2010 # my @display = qw( _items_previous _items_pkg );
2013 # foreach my $display ( @display ) {
2014 # push @b, $self->$display(@_);
2019 =item _items_pkg [ OPTIONS ]
2021 Return line item hashes for each package item on this invoice. Nearly
2024 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2026 The only OPTIONS accepted is 'section', which may point to a hashref
2027 with a key named 'condensed', which may have a true value. If it
2028 does, this method tries to merge identical items into items with
2029 'quantity' equal to the number of items (not the sum of their
2030 separate quantities, for some reason).
2038 warn "$me _items_pkg searching for all package line items\n"
2041 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2043 warn "$me _items_pkg filtering line items\n"
2045 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2047 if ($options{section} && $options{section}->{condensed}) {
2049 warn "$me _items_pkg condensing section\n"
2053 local $Storable::canonical = 1;
2054 foreach ( @items ) {
2056 delete $item->{ref};
2057 delete $item->{ext_description};
2058 my $key = freeze($item);
2059 $itemshash{$key} ||= 0;
2060 $itemshash{$key} ++; # += $item->{quantity};
2062 @items = sort { $a->{description} cmp $b->{description} }
2063 map { my $i = thaw($_);
2064 $i->{quantity} = $itemshash{$_};
2066 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2072 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2079 return 0 unless $a->itemdesc cmp $b->itemdesc;
2080 return -1 if $b->itemdesc eq 'Tax';
2081 return 1 if $a->itemdesc eq 'Tax';
2082 return -1 if $b->itemdesc eq 'Other surcharges';
2083 return 1 if $a->itemdesc eq 'Other surcharges';
2084 $a->itemdesc cmp $b->itemdesc;
2089 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2090 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2093 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2095 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2096 list of hashrefs describing the line items they generate on the invoice.
2098 OPTIONS may include:
2100 format: the invoice format.
2102 escape_function: the function used to escape strings.
2104 DEPRECATED? (expensive, mostly unused?)
2105 format_function: the function used to format CDRs.
2107 section: a hashref containing 'description'; if this is present,
2108 cust_bill_pkg_display records not belonging to this section are
2111 multisection: a flag indicating that this is a multisection invoice,
2112 which does something complicated.
2114 multilocation: a flag to display the location label for the package.
2116 Returns a list of hashrefs, each of which may contain:
2118 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
2119 ext_description, which is an arrayref of detail lines to show below
2124 sub _items_cust_bill_pkg {
2126 my $conf = $self->conf;
2127 my $cust_bill_pkgs = shift;
2130 my $format = $opt{format} || '';
2131 my $escape_function = $opt{escape_function} || sub { shift };
2132 my $format_function = $opt{format_function} || '';
2133 my $no_usage = $opt{no_usage} || '';
2134 my $unsquelched = $opt{unsquelched} || ''; #unused
2135 my $section = $opt{section}->{description} if $opt{section};
2136 my $summary_page = $opt{summary_page} || ''; #unused
2137 my $multilocation = $opt{multilocation} || '';
2138 my $multisection = $opt{multisection} || '';
2139 my $discount_show_always = 0;
2141 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2143 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2146 my ($s, $r, $u) = ( undef, undef, undef );
2147 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2150 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2151 if ( $_ && !$cust_bill_pkg->hidden ) {
2152 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2153 $_->{amount} =~ s/^\-0\.00$/0.00/;
2154 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2156 if $_->{amount} != 0
2157 || $discount_show_always
2158 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2159 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2165 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2166 ? $cust_bill_pkg->cust_bill_pkg_display
2167 : ( $cust_bill_pkg );
2169 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2170 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2173 foreach my $display ( grep { defined($section)
2174 ? $_->section eq $section
2177 #grep { !$_->summary || !$summary_page } # bunk!
2178 grep { !$_->summary || $multisection }
2179 @cust_bill_pkg_display
2183 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2184 $display->billpkgdisplaynum. "\n"
2187 my $type = $display->type;
2189 my $desc = $cust_bill_pkg->desc;
2190 $desc = substr($desc, 0, $maxlength). '...'
2191 if $format eq 'latex' && length($desc) > $maxlength;
2193 my %details_opt = ( 'format' => $format,
2194 'escape_function' => $escape_function,
2195 'format_function' => $format_function,
2196 'no_usage' => $opt{'no_usage'},
2199 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2201 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2204 if ( $cust_bill_pkg->setup != 0 ) {
2205 my $description = $desc;
2206 $description .= ' Setup'
2207 if $cust_bill_pkg->recur != 0
2208 || $discount_show_always
2209 || $cust_bill_pkg->recur_show_zero;
2211 'description' => $description,
2212 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2215 if ( $cust_bill_pkg->recur != 0 ) {
2217 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2218 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2222 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2224 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2227 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2229 # start/end dates for invoice formats that do nonstandard
2231 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2233 if ( (!$type || $type eq 'S')
2234 && ( $cust_bill_pkg->setup != 0
2235 || $cust_bill_pkg->setup_show_zero
2240 warn "$me _items_cust_bill_pkg adding setup\n"
2243 my $description = $desc;
2244 $description .= ' Setup'
2245 if $cust_bill_pkg->recur != 0
2246 || $discount_show_always
2247 || $cust_bill_pkg->recur_show_zero;
2250 unless ( $cust_pkg->part_pkg->hide_svc_detail
2251 || $cust_bill_pkg->hidden )
2254 push @d, map &{$escape_function}($_),
2255 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2256 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2258 if ( $multilocation ) {
2259 my $loc = $cust_pkg->location_label;
2260 $loc = substr($loc, 0, $maxlength). '...'
2261 if $format eq 'latex' && length($loc) > $maxlength;
2262 push @d, &{$escape_function}($loc);
2265 } #unless hiding service details
2267 push @d, $cust_bill_pkg->details(%details_opt)
2268 if $cust_bill_pkg->recur == 0;
2270 if ( $cust_bill_pkg->hidden ) {
2271 $s->{amount} += $cust_bill_pkg->setup;
2272 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2273 push @{ $s->{ext_description} }, @d;
2277 description => $description,
2278 #pkgpart => $part_pkg->pkgpart,
2279 pkgnum => $cust_bill_pkg->pkgnum,
2280 amount => $cust_bill_pkg->setup,
2281 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2282 unit_amount => $cust_bill_pkg->unitsetup,
2283 quantity => $cust_bill_pkg->quantity,
2284 ext_description => \@d,
2290 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2292 $cust_bill_pkg->recur != 0
2293 || $cust_bill_pkg->setup == 0
2294 || $discount_show_always
2295 || $cust_bill_pkg->recur_show_zero
2300 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2303 my $is_summary = $display->summary;
2304 my $description = ($is_summary && $type && $type eq 'U')
2305 ? "Usage charges" : $desc;
2307 #pry be a bit more efficient to look some of this conf stuff up
2310 $conf->exists('disable_line_item_date_ranges')
2311 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2314 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2315 $cust_main->agentnum
2317 if ( defined($date_style) && $date_style eq 'month_of' ) {
2318 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2319 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2320 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2321 $cust_main->agentnum
2323 $desc .= ' ' unless $desc =~ /\s$/;
2324 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2326 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2327 " - ". time2str($date_format, $cust_bill_pkg->edate);
2329 $description .= " ($time_period)";
2333 my @seconds = (); # for display of usage info
2335 #at least until cust_bill_pkg has "past" ranges in addition to
2336 #the "future" sdate/edate ones... see #3032
2337 my @dates = ( $self->_date );
2338 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2339 push @dates, $prev->sdate if $prev;
2340 push @dates, undef if !$prev;
2342 unless ( $cust_pkg->part_pkg->hide_svc_detail
2343 || $cust_bill_pkg->itemdesc
2344 || $cust_bill_pkg->hidden
2345 || $is_summary && $type && $type eq 'U' )
2348 warn "$me _items_cust_bill_pkg adding service details\n"
2351 push @d, map &{$escape_function}($_),
2352 $cust_pkg->h_labels_short(@dates, 'I')
2353 #$cust_bill_pkg->edate,
2354 #$cust_bill_pkg->sdate)
2355 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2357 warn "$me _items_cust_bill_pkg done adding service details\n"
2360 if ( $multilocation ) {
2361 my $loc = $cust_pkg->location_label;
2362 $loc = substr($loc, 0, $maxlength). '...'
2363 if $format eq 'latex' && length($loc) > $maxlength;
2364 push @d, &{$escape_function}($loc);
2367 # Display of seconds_since_sqlradacct:
2368 # On the invoice, when processing @detail_items, look for a field
2369 # named 'seconds'. This will contain total seconds for each
2370 # service, in the same order as @ext_description. For services
2371 # that don't support this it will show undef.
2372 if ( $conf->exists('svc_acct-usage_seconds')
2373 and ! $cust_bill_pkg->pkgpart_override ) {
2374 foreach my $cust_svc (
2375 $cust_pkg->h_cust_svc(@dates, 'I')
2378 # eval because not having any part_export_usage exports
2379 # is a fatal error, last_bill/_date because that's how
2380 # sqlradius_hour billing does it
2382 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2384 push @seconds, $sec;
2386 } #if svc_acct-usage_seconds
2390 unless ( $is_summary ) {
2391 warn "$me _items_cust_bill_pkg adding details\n"
2394 #instead of omitting details entirely in this case (unwanted side
2395 # effects), just omit CDRs
2396 $details_opt{'no_usage'} = 1
2397 if $type && $type eq 'R';
2399 push @d, $cust_bill_pkg->details(%details_opt);
2402 warn "$me _items_cust_bill_pkg calculating amount\n"
2407 $amount = $cust_bill_pkg->recur;
2408 } elsif ($type eq 'R') {
2409 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2410 } elsif ($type eq 'U') {
2411 $amount = $cust_bill_pkg->usage;
2414 if ( !$type || $type eq 'R' ) {
2416 warn "$me _items_cust_bill_pkg adding recur\n"
2419 if ( $cust_bill_pkg->hidden ) {
2420 $r->{amount} += $amount;
2421 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
2422 push @{ $r->{ext_description} }, @d;
2425 description => $description,
2426 #pkgpart => $part_pkg->pkgpart,
2427 pkgnum => $cust_bill_pkg->pkgnum,
2429 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2430 unit_amount => $cust_bill_pkg->unitrecur,
2431 quantity => $cust_bill_pkg->quantity,
2433 ext_description => \@d,
2435 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
2438 } else { # $type eq 'U'
2440 warn "$me _items_cust_bill_pkg adding usage\n"
2443 if ( $cust_bill_pkg->hidden ) {
2444 $u->{amount} += $amount;
2445 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
2446 push @{ $u->{ext_description} }, @d;
2449 description => $description,
2450 #pkgpart => $part_pkg->pkgpart,
2451 pkgnum => $cust_bill_pkg->pkgnum,
2453 recur_show_zero => $cust_bill_pkg->recur_show_zero,
2454 unit_amount => $cust_bill_pkg->unitrecur,
2455 quantity => $cust_bill_pkg->quantity,
2457 ext_description => \@d,
2462 } # recurring or usage with recurring charge
2464 } else { #pkgnum tax or one-shot line item (??)
2466 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
2469 if ( $cust_bill_pkg->setup != 0 ) {
2471 'description' => $desc,
2472 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2475 if ( $cust_bill_pkg->recur != 0 ) {
2477 'description' => "$desc (".
2478 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
2479 time2str($date_format, $cust_bill_pkg->edate). ')',
2480 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2488 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
2489 && $conf->exists('discount-show-always'));
2493 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2495 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2496 $_->{amount} =~ s/^\-0\.00$/0.00/;
2497 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2499 if $_->{amount} != 0
2500 || $discount_show_always
2501 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2502 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2506 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
2513 =item _items_discounts_avail
2515 Returns an array of line item hashrefs representing available term discounts
2516 for this invoice. This makes the same assumptions that apply to term
2517 discounts in general: that the package is billed monthly, at a flat rate,
2518 with no usage charges. A prorated first month will be handled, as will
2519 a setup fee if the discount is allowed to apply to setup fees.
2523 sub _items_discounts_avail {
2526 #maybe move this method from cust_bill when quotations support discount_plans
2527 return () unless $self->can('discount_plans');
2528 my %plans = $self->discount_plans;
2530 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
2531 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
2535 my $plan = $plans{$months};
2537 my $term_total = sprintf('%.2f', $plan->discounted_total);
2538 my $percent = sprintf('%.0f',
2539 100 * (1 - $term_total / $plan->base_total) );
2540 my $permonth = sprintf('%.2f', $term_total / $months);
2541 my $detail = $self->mt('discount on item'). ' '.
2542 join(', ', map { "#$_" } $plan->pkgnums)
2545 # discounts for non-integer months don't work anyway
2546 $months = sprintf("%d", $months);
2549 description => $self->mt('Save [_1]% by paying for [_2] months',
2551 amount => $self->mt('[_1] ([_2] per month)',
2552 $term_total, $money_char.$permonth),
2553 ext_description => ($detail || ''),
2556 sort { $b <=> $a } keys %plans;