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"
898 $options{'section'} = $section if $multisection;
899 $options{'format'} = $format;
900 $options{'escape_function'} = $escape_function;
901 $options{'no_usage'} = 1 unless $unsquelched;
902 $options{'unsquelched'} = $unsquelched;
903 $options{'summary_page'} = $summarypage;
904 $options{'skip_usage'} =
905 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
906 $options{'multisection'} = $multisection;
908 warn "$me searching for line items\n"
911 foreach my $line_item ( $self->_items_pkg(%options) ) {
913 warn "$me adding line item $line_item\n"
917 ext_description => [],
919 $detail->{'ref'} = $line_item->{'pkgnum'};
920 $detail->{'quantity'} = $line_item->{'quantity'};
921 $detail->{'section'} = $section;
922 $detail->{'description'} = &$escape_function($line_item->{'description'});
923 if ( exists $line_item->{'ext_description'} ) {
924 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
926 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
927 $line_item->{'amount'};
928 if ( exists $line_item->{'unit_amount'} ) {
929 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
930 $line_item->{'unit_amount'};
932 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
934 $detail->{'sdate'} = $line_item->{'sdate'};
935 $detail->{'edate'} = $line_item->{'edate'};
936 $detail->{'seconds'} = $line_item->{'seconds'};
938 push @detail_items, $detail;
939 push @buf, ( [ $detail->{'description'},
940 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
942 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
946 if ( $section->{'description'} ) {
947 push @buf, ( ['','-----------'],
948 [ $section->{'description'}. ' sub-total',
949 $section->{'subtotal'} # already formatted this
958 $invoice_data{current_less_finance} =
959 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
961 # create a major section for previous balance if we have major sections,
962 # or if previous_section is in summary form
963 if ( ( $multisection && $self->enable_previous )
964 || $conf->exists('previous_balance-summary_only') )
966 unshift @sections, $previous_section if $pr_total;
969 warn "$me adding taxes\n"
972 foreach my $tax ( $self->_items_tax ) {
974 $taxtotal += $tax->{'amount'};
976 my $description = &$escape_function( $tax->{'description'} );
977 my $amount = sprintf( '%.2f', $tax->{'amount'} );
979 if ( $multisection ) {
981 my $money = $old_latex ? '' : $money_char;
982 push @detail_items, {
983 ext_description => [],
986 description => $description,
987 amount => $money. $amount,
989 section => $tax_section,
995 'total_item' => $description,
996 'total_amount' => $other_money_char. $amount,
1001 push @buf,[ $description,
1002 $money_char. $amount,
1009 $total->{'total_item'} = $self->mt('Sub-total');
1010 $total->{'total_amount'} =
1011 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
1013 if ( $multisection ) {
1014 $tax_section->{'subtotal'} = $other_money_char.
1015 sprintf('%.2f', $taxtotal);
1016 $tax_section->{'pretotal'} = 'New charges sub-total '.
1017 $total->{'total_amount'};
1018 push @sections, $tax_section if $taxtotal;
1020 unshift @total_items, $total;
1023 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
1025 push @buf,['','-----------'];
1026 push @buf,[$self->mt(
1027 (!$self->enable_previous)
1029 : 'Total New Charges'
1031 $money_char. sprintf("%10.2f",$self->charged) ];
1034 # calculate total, possibly including total owed on previous
1039 $item = $conf->config('previous_balance-exclude_from_total')
1040 || 'Total New Charges'
1041 if $conf->exists('previous_balance-exclude_from_total');
1042 my $amount = $self->charged;
1043 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
1044 $amount += $pr_total;
1047 $total->{'total_item'} = &$embolden_function($self->mt($item));
1048 $total->{'total_amount'} =
1049 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
1050 if ( $multisection ) {
1051 if ( $adjust_section->{'sort_weight'} ) {
1052 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
1053 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
1055 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
1056 $other_money_char. sprintf('%.2f', $self->charged );
1059 push @total_items, $total;
1061 push @buf,['','-----------'];
1064 sprintf( '%10.2f', $amount )
1069 # if we're showing previous invoices, also show previous
1070 # credits and payments
1071 if ( $self->enable_previous
1072 and $self->can('_items_credits')
1073 and $self->can('_items_payments') )
1075 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1078 my $credittotal = 0;
1079 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
1082 $total->{'total_item'} = &$escape_function($credit->{'description'});
1083 $credittotal += $credit->{'amount'};
1084 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
1085 $adjusttotal += $credit->{'amount'};
1086 if ( $multisection ) {
1087 my $money = $old_latex ? '' : $money_char;
1088 push @detail_items, {
1089 ext_description => [],
1092 description => &$escape_function($credit->{'description'}),
1093 amount => $money. $credit->{'amount'},
1095 section => $adjust_section,
1098 push @total_items, $total;
1102 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
1105 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
1106 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
1110 my $paymenttotal = 0;
1111 foreach my $payment ( $self->_items_payments ) {
1113 $total->{'total_item'} = &$escape_function($payment->{'description'});
1114 $paymenttotal += $payment->{'amount'};
1115 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
1116 $adjusttotal += $payment->{'amount'};
1117 if ( $multisection ) {
1118 my $money = $old_latex ? '' : $money_char;
1119 push @detail_items, {
1120 ext_description => [],
1123 description => &$escape_function($payment->{'description'}),
1124 amount => $money. $payment->{'amount'},
1126 section => $adjust_section,
1129 push @total_items, $total;
1131 push @buf, [ $payment->{'description'},
1132 $money_char. sprintf("%10.2f", $payment->{'amount'}),
1135 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
1137 if ( $multisection ) {
1138 $adjust_section->{'subtotal'} = $other_money_char.
1139 sprintf('%.2f', $adjusttotal);
1140 push @sections, $adjust_section
1141 unless $adjust_section->{sort_weight};
1144 # create Balance Due message
1147 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1148 $total->{'total_amount'} =
1149 &$embolden_function(
1150 $other_money_char. sprintf('%.2f', $summarypage
1152 $self->billing_balance
1153 : $self->owed + $pr_total
1156 if ( $multisection && !$adjust_section->{sort_weight} ) {
1157 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
1158 $total->{'total_amount'};
1160 push @total_items, $total;
1162 push @buf,['','-----------'];
1163 push @buf,[$self->balance_due_msg, $money_char.
1164 sprintf("%10.2f", $balance_due ) ];
1167 if ( $conf->exists('previous_balance-show_credit')
1168 and $cust_main->balance < 0 ) {
1169 my $credit_total = {
1170 'total_item' => &$embolden_function($self->credit_balance_msg),
1171 'total_amount' => &$embolden_function(
1172 $other_money_char. sprintf('%.2f', -$cust_main->balance)
1175 if ( $multisection ) {
1176 $adjust_section->{'posttotal'} .= $newline_token .
1177 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
1180 push @total_items, $credit_total;
1182 push @buf,['','-----------'];
1183 push @buf,[$self->credit_balance_msg, $money_char.
1184 sprintf("%10.2f", -$cust_main->balance ) ];
1188 if ( $multisection ) {
1189 if ( $conf->exists('svc_phone_sections')
1190 && $self->can('_items_svc_phone_sections')
1194 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
1195 $total->{'total_amount'} =
1196 &$embolden_function(
1197 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
1199 my $last_section = pop @sections;
1200 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
1201 $total->{'total_amount'};
1202 push @sections, $last_section;
1204 push @sections, @$late_sections
1208 # make a discounts-available section, even without multisection
1209 if ( $conf->exists('discount-show_available')
1210 and my @discounts_avail = $self->_items_discounts_avail ) {
1211 my $discount_section = {
1212 'description' => $self->mt('Discounts Available'),
1217 push @sections, $discount_section;
1218 push @detail_items, map { +{
1219 'ref' => '', #should this be something else?
1220 'section' => $discount_section,
1221 'description' => &$escape_function( $_->{description} ),
1222 'amount' => $money_char . &$escape_function( $_->{amount} ),
1223 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
1224 } } @discounts_avail;
1227 # All sections and items are built; now fill in templates.
1228 my @includelist = ();
1229 push @includelist, 'summary' if $summarypage;
1230 foreach my $include ( @includelist ) {
1232 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1235 if ( length( $conf->config($inc_file, $agentnum) ) ) {
1237 @inc_src = $conf->config($inc_file, $agentnum);
1241 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1243 my $convert_map = $convert_maps{$format}{$include};
1245 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1246 s/--\@\]/$delimiters{$format}[1]/g;
1249 &$convert_map( $conf->config($inc_file, $agentnum) );
1253 my $inc_tt = new Text::Template (
1255 SOURCE => [ map "$_\n", @inc_src ],
1256 DELIMITERS => $delimiters{$format},
1257 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1259 unless ( $inc_tt->compile() ) {
1260 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1261 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1265 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1267 $invoice_data{$include} =~ s/\n+$//
1268 if ($format eq 'latex');
1273 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1274 /invoice_lines\((\d*)\)/;
1275 $invoice_lines += $1 || scalar(@buf);
1278 die "no invoice_lines() functions in template?"
1279 if ( $format eq 'template' && !$wasfunc );
1281 if ($format eq 'template') {
1283 if ( $invoice_lines ) {
1284 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
1285 $invoice_data{'total_pages'}++
1286 if scalar(@buf) % $invoice_lines;
1289 #setup subroutine for the template
1290 $invoice_data{invoice_lines} = sub {
1291 my $lines = shift || scalar(@buf);
1303 push @collect, split("\n",
1304 $text_template->fill_in( HASH => \%invoice_data )
1306 $invoice_data{'page'}++;
1308 map "$_\n", @collect;
1310 } else { # this is where we actually create the invoice
1312 warn "filling in template for invoice ". $self->invnum. "\n"
1314 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
1317 $text_template->fill_in(HASH => \%invoice_data);
1321 sub notice_name { '('.shift->table.')'; }
1323 sub template_conf { 'invoice_'; }
1325 # helper routine for generating date ranges
1326 sub _prior_month30s {
1329 [ 1, 2592000 ], # 0-30 days ago
1330 [ 2592000, 5184000 ], # 30-60 days ago
1331 [ 5184000, 7776000 ], # 60-90 days ago
1332 [ 7776000, 0 ], # 90+ days ago
1335 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
1336 $_->[1] ? $self->_date - $_->[1] - 1 : '',
1341 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
1343 Returns an postscript invoice, as a scalar.
1345 Options can be passed as a hashref (recommended) or as a list of time, template
1346 and then any key/value pairs for any other options.
1348 I<time> an optional value used to control the printing of overdue messages. The
1349 default is now. It isn't the date of the invoice; that's the `_date' field.
1350 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1351 L<Time::Local> and L<Date::Parse> for conversion functions.
1353 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1360 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1361 my $ps = generate_ps($file);
1363 unlink($barcodefile) if $barcodefile;
1368 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
1370 Returns an PDF invoice, as a scalar.
1372 Options can be passed as a hashref (recommended) or as a list of time, template
1373 and then any key/value pairs for any other options.
1375 I<time> an optional value used to control the printing of overdue messages. The
1376 default is now. It isn't the date of the invoice; that's the `_date' field.
1377 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1378 L<Time::Local> and L<Date::Parse> for conversion functions.
1380 I<template>, if specified, is the name of a suffix for alternate invoices.
1382 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1389 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
1390 my $pdf = generate_pdf($file);
1392 unlink($barcodefile) if $barcodefile;
1397 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
1399 Returns an HTML invoice, as a scalar.
1401 I<time> an optional value used to control the printing of overdue messages. The
1402 default is now. It isn't the date of the invoice; that's the `_date' field.
1403 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1404 L<Time::Local> and L<Date::Parse> for conversion functions.
1406 I<template>, if specified, is the name of a suffix for alternate invoices.
1408 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1410 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1411 when emailing the invoice as part of a multipart/related MIME email.
1419 %params = %{ shift() };
1421 $params{'time'} = shift;
1422 $params{'template'} = shift;
1423 $params{'cid'} = shift;
1426 $params{'format'} = 'html';
1428 $self->print_generic( %params );
1431 # quick subroutine for print_latex
1433 # There are ten characters that LaTeX treats as special characters, which
1434 # means that they do not simply typeset themselves:
1435 # # $ % & ~ _ ^ \ { }
1437 # TeX ignores blanks following an escaped character; if you want a blank (as
1438 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1442 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1443 $value =~ s/([<>])/\$$1\$/g;
1449 encode_entities($value);
1453 sub _html_escape_nbsp {
1454 my $value = _html_escape(shift);
1455 $value =~ s/ +/ /g;
1459 #utility methods for print_*
1461 sub _translate_old_latex_format {
1462 warn "_translate_old_latex_format called\n"
1469 if ( $line =~ /^%%Detail\s*$/ ) {
1471 push @template, q![@--!,
1472 q! foreach my $_tr_line (@detail_items) {!,
1473 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
1474 q! $_tr_line->{'description'} .= !,
1475 q! "\\tabularnewline\n~~".!,
1476 q! join( "\\tabularnewline\n~~",!,
1477 q! @{$_tr_line->{'ext_description'}}!,
1481 while ( ( my $line_item_line = shift )
1482 !~ /^%%EndDetail\s*$/ ) {
1483 $line_item_line =~ s/'/\\'/g; # nice LTS
1484 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1485 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1486 push @template, " \$OUT .= '$line_item_line';";
1489 push @template, '}',
1492 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1494 push @template, '[@--',
1495 ' foreach my $_tr_line (@total_items) {';
1497 while ( ( my $total_item_line = shift )
1498 !~ /^%%EndTotalDetails\s*$/ ) {
1499 $total_item_line =~ s/'/\\'/g; # nice LTS
1500 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
1501 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
1502 push @template, " \$OUT .= '$total_item_line';";
1505 push @template, '}',
1509 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
1510 push @template, $line;
1516 warn "$_\n" foreach @template;
1524 my $conf = $self->conf;
1526 #check for an invoice-specific override
1527 return $self->invoice_terms if $self->invoice_terms;
1529 #check for a customer- specific override
1530 my $cust_main = $self->cust_main;
1531 return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
1533 #use configured default
1534 $conf->config('invoice_default_terms') || '';
1540 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
1541 $duedate = $self->_date() + ( $1 * 86400 );
1548 $self->due_date ? time2str(shift, $self->due_date) : '';
1551 sub balance_due_msg {
1553 my $msg = $self->mt('Balance Due');
1554 return $msg unless $self->terms;
1555 if ( $self->due_date ) {
1556 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
1557 $self->due_date2str($date_format);
1558 } elsif ( $self->terms ) {
1559 $msg .= ' - '. $self->terms;
1564 sub balance_due_date {
1566 my $conf = $self->conf;
1568 if ( $conf->exists('invoice_default_terms')
1569 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1570 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
1575 sub credit_balance_msg {
1577 $self->mt('Credit Balance Remaining')
1582 Returns a string with the date, for example: "3/20/2008"
1588 time2str($date_format, $self->_date);
1591 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
1593 Generate section information for all items appearing on this invoice.
1594 This will only be called for multi-section invoices.
1596 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
1597 related display records (L<FS::cust_bill_pkg_display>) and organize
1598 them into two groups ("early" and "late" according to whether they come
1599 before or after the total), then into sections. A subtotal is calculated
1602 Section descriptions are returned in sort weight order. Each consists
1603 of a hash containing:
1605 description: the package category name, escaped
1606 subtotal: the total charges in that section
1607 tax_section: a flag indicating that the section contains only tax charges
1608 summarized: same as tax_section, for some reason
1609 sort_weight: the package category's sort weight
1611 If 'condense' is set on the display record, it also contains everything
1612 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
1613 coderefs to generate parts of the invoice. This is not advised.
1617 LATE: an arrayref to push the "late" section hashes onto. The "early"
1618 group is simply returned from the method.
1620 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
1621 Turning this on has the following effects:
1622 - Ignores display items with the 'summary' flag.
1623 - Combines all items into the "early" group.
1624 - Creates sections for all non-disabled package categories, even if they
1625 have no charges on this invoice, as well as a section with no name.
1627 ESCAPE: an escape function to use for section titles.
1629 EXTRA_SECTIONS: an arrayref of additional sections to return after the
1630 sorted list. If there are any of these, section subtotals exclude
1633 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
1634 passed through to C<_condense_section()>.
1638 use vars qw(%pkg_category_cache);
1639 sub _items_sections {
1642 my $summarypage = shift;
1644 my $extra_sections = shift;
1648 my %late_subtotal = ();
1651 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
1654 my $usage = $cust_bill_pkg->usage;
1656 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
1657 next if ( $display->summary && $summarypage );
1659 my $section = $display->section;
1660 my $type = $display->type;
1662 $not_tax{$section} = 1
1663 unless $cust_bill_pkg->pkgnum == 0;
1665 if ( $display->post_total && !$summarypage ) {
1666 if (! $type || $type eq 'S') {
1667 $late_subtotal{$section} += $cust_bill_pkg->setup
1668 if $cust_bill_pkg->setup != 0
1669 || $cust_bill_pkg->setup_show_zero;
1673 $late_subtotal{$section} += $cust_bill_pkg->recur
1674 if $cust_bill_pkg->recur != 0
1675 || $cust_bill_pkg->recur_show_zero;
1678 if ($type && $type eq 'R') {
1679 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
1680 if $cust_bill_pkg->recur != 0
1681 || $cust_bill_pkg->recur_show_zero;
1684 if ($type && $type eq 'U') {
1685 $late_subtotal{$section} += $usage
1686 unless scalar(@$extra_sections);
1691 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
1693 if (! $type || $type eq 'S') {
1694 $subtotal{$section} += $cust_bill_pkg->setup
1695 if $cust_bill_pkg->setup != 0
1696 || $cust_bill_pkg->setup_show_zero;
1700 $subtotal{$section} += $cust_bill_pkg->recur
1701 if $cust_bill_pkg->recur != 0
1702 || $cust_bill_pkg->recur_show_zero;
1705 if ($type && $type eq 'R') {
1706 $subtotal{$section} += $cust_bill_pkg->recur - $usage
1707 if $cust_bill_pkg->recur != 0
1708 || $cust_bill_pkg->recur_show_zero;
1711 if ($type && $type eq 'U') {
1712 $subtotal{$section} += $usage
1713 unless scalar(@$extra_sections);
1722 %pkg_category_cache = ();
1724 push @$late, map { { 'description' => &{$escape}($_),
1725 'subtotal' => $late_subtotal{$_},
1727 'sort_weight' => ( _pkg_category($_)
1728 ? _pkg_category($_)->weight
1731 ((_pkg_category($_) && _pkg_category($_)->condense)
1732 ? $self->_condense_section($format)
1736 sort _sectionsort keys %late_subtotal;
1739 if ( $summarypage ) {
1740 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
1741 map { $_->categoryname } qsearch('pkg_category', {});
1742 push @sections, '' if exists($subtotal{''});
1744 @sections = keys %subtotal;
1747 my @early = map { { 'description' => &{$escape}($_),
1748 'subtotal' => $subtotal{$_},
1749 'summarized' => $not_tax{$_} ? '' : 'Y',
1750 'tax_section' => $not_tax{$_} ? '' : 'Y',
1751 'sort_weight' => ( _pkg_category($_)
1752 ? _pkg_category($_)->weight
1755 ((_pkg_category($_) && _pkg_category($_)->condense)
1756 ? $self->_condense_section($format)
1761 push @early, @$extra_sections if $extra_sections;
1763 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
1767 #helper subs for above
1770 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
1774 my $categoryname = shift;
1775 $pkg_category_cache{$categoryname} ||=
1776 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
1779 my %condensed_format = (
1780 'label' => [ qw( Description Qty Amount ) ],
1782 sub { shift->{description} },
1783 sub { shift->{quantity} },
1784 sub { my($href, %opt) = @_;
1785 ($opt{dollar} || ''). $href->{amount};
1788 'align' => [ qw( l r r ) ],
1789 'span' => [ qw( 5 1 1 ) ], # unitprices?
1790 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
1793 sub _condense_section {
1794 my ( $self, $format ) = ( shift, shift );
1796 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
1797 qw( description_generator
1800 total_line_generator
1805 sub _condensed_generator_defaults {
1806 my ( $self, $format ) = ( shift, shift );
1807 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
1816 sub _condensed_header_generator {
1817 my ( $self, $format ) = ( shift, shift );
1819 my ( $f, $prefix, $suffix, $separator, $column ) =
1820 _condensed_generator_defaults($format);
1822 if ($format eq 'latex') {
1823 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
1824 $suffix = "\\\\\n\\hline";
1827 sub { my ($d,$a,$s,$w) = @_;
1828 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1830 } elsif ( $format eq 'html' ) {
1831 $prefix = '<th></th>';
1835 sub { my ($d,$a,$s,$w) = @_;
1836 return qq!<th align="$html_align{$a}">$d</th>!;
1844 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1846 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
1849 $prefix. join($separator, @result). $suffix;
1854 sub _condensed_description_generator {
1855 my ( $self, $format ) = ( shift, shift );
1857 my ( $f, $prefix, $suffix, $separator, $column ) =
1858 _condensed_generator_defaults($format);
1860 my $money_char = '$';
1861 if ($format eq 'latex') {
1862 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
1864 $separator = " & \n";
1866 sub { my ($d,$a,$s,$w) = @_;
1867 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
1869 $money_char = '\\dollar';
1870 }elsif ( $format eq 'html' ) {
1871 $prefix = '"><td align="center"></td>';
1875 sub { my ($d,$a,$s,$w) = @_;
1876 return qq!<td align="$html_align{$a}">$d</td>!;
1878 #$money_char = $conf->config('money_char') || '$';
1879 $money_char = ''; # this is madness
1887 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1889 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
1891 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
1892 map { $f->{$_}->[$i] } qw(align span width)
1896 $prefix. join( $separator, @result ). $suffix;
1901 sub _condensed_total_generator {
1902 my ( $self, $format ) = ( shift, shift );
1904 my ( $f, $prefix, $suffix, $separator, $column ) =
1905 _condensed_generator_defaults($format);
1908 if ($format eq 'latex') {
1911 $separator = " & \n";
1913 sub { my ($d,$a,$s,$w) = @_;
1914 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1916 }elsif ( $format eq 'html' ) {
1920 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1922 sub { my ($d,$a,$s,$w) = @_;
1923 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1932 # my $r = &{$f->{fields}->[$i]}(@args);
1933 # $r .= ' Total' unless $i;
1935 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1937 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
1938 map { $f->{$_}->[$i] } qw(align span width)
1942 $prefix. join( $separator, @result ). $suffix;
1947 =item total_line_generator FORMAT
1949 Returns a coderef used for generation of invoice total line items for this
1950 usage_class. FORMAT is either html or latex
1954 # should not be used: will have issues with hash element names (description vs
1955 # total_item and amount vs total_amount -- another array of functions?
1957 sub _condensed_total_line_generator {
1958 my ( $self, $format ) = ( shift, shift );
1960 my ( $f, $prefix, $suffix, $separator, $column ) =
1961 _condensed_generator_defaults($format);
1964 if ($format eq 'latex') {
1967 $separator = " & \n";
1969 sub { my ($d,$a,$s,$w) = @_;
1970 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
1972 }elsif ( $format eq 'html' ) {
1976 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
1978 sub { my ($d,$a,$s,$w) = @_;
1979 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
1988 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
1990 &{$column}( &{$f->{fields}->[$i]}(@args),
1991 map { $f->{$_}->[$i] } qw(align span width)
1995 $prefix. join( $separator, @result ). $suffix;
2000 # sub _items { # seems to be unused
2003 # #my @display = scalar(@_)
2005 # # : qw( _items_previous _items_pkg );
2006 # # #: qw( _items_pkg );
2007 # # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2008 # my @display = qw( _items_previous _items_pkg );
2011 # foreach my $display ( @display ) {
2012 # push @b, $self->$display(@_);
2017 =item _items_pkg [ OPTIONS ]
2019 Return line item hashes for each package item on this invoice. Nearly
2022 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
2024 The only OPTIONS accepted is 'section', which may point to a hashref
2025 with a key named 'condensed', which may have a true value. If it
2026 does, this method tries to merge identical items into items with
2027 'quantity' equal to the number of items (not the sum of their
2028 separate quantities, for some reason).
2036 warn "$me _items_pkg searching for all package line items\n"
2039 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2041 warn "$me _items_pkg filtering line items\n"
2043 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2045 if ($options{section} && $options{section}->{condensed}) {
2047 warn "$me _items_pkg condensing section\n"
2051 local $Storable::canonical = 1;
2052 foreach ( @items ) {
2054 delete $item->{ref};
2055 delete $item->{ext_description};
2056 my $key = freeze($item);
2057 $itemshash{$key} ||= 0;
2058 $itemshash{$key} ++; # += $item->{quantity};
2060 @items = sort { $a->{description} cmp $b->{description} }
2061 map { my $i = thaw($_);
2062 $i->{quantity} = $itemshash{$_};
2064 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
2070 warn "$me _items_pkg returning ". scalar(@items). " items\n"
2077 return 0 unless $a->itemdesc cmp $b->itemdesc;
2078 return -1 if $b->itemdesc eq 'Tax';
2079 return 1 if $a->itemdesc eq 'Tax';
2080 return -1 if $b->itemdesc eq 'Other surcharges';
2081 return 1 if $a->itemdesc eq 'Other surcharges';
2082 $a->itemdesc cmp $b->itemdesc;
2087 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2088 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2091 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
2093 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
2094 list of hashrefs describing the line items they generate on the invoice.
2096 OPTIONS may include:
2098 format: the invoice format.
2100 escape_function: the function used to escape strings.
2102 DEPRECATED? (expensive, mostly unused?)
2103 format_function: the function used to format CDRs.
2105 section: a hashref containing 'description'; if this is present,
2106 cust_bill_pkg_display records not belonging to this section are
2109 multisection: a flag indicating that this is a multisection invoice,
2110 which does something complicated.
2112 Returns a list of hashrefs, each of which may contain:
2114 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
2115 ext_description, which is an arrayref of detail lines to show below
2120 sub _items_cust_bill_pkg {
2122 my $conf = $self->conf;
2123 my $cust_bill_pkgs = shift;
2126 my $format = $opt{format} || '';
2127 my $escape_function = $opt{escape_function} || sub { shift };
2128 my $format_function = $opt{format_function} || '';
2129 my $no_usage = $opt{no_usage} || '';
2130 my $unsquelched = $opt{unsquelched} || ''; #unused
2131 my $section = $opt{section}->{description} if $opt{section};
2132 my $summary_page = $opt{summary_page} || ''; #unused
2133 my $multisection = $opt{multisection} || '';
2134 my $discount_show_always = 0;
2136 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2138 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
2139 # and location labels
2142 my ($s, $r, $u) = ( undef, undef, undef );
2143 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
2146 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
2147 if ( $_ && !$cust_bill_pkg->hidden ) {
2148 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2149 $_->{amount} =~ s/^\-0\.00$/0.00/;
2150 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2152 if $_->{amount} != 0
2153 || $discount_show_always
2154 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
2155 || ( $_->{_is_setup} && $_->{setup_show_zero} )
2161 my @cust_bill_pkg_display = $cust_bill_pkg->can('cust_bill_pkg_display')
2162 ? $cust_bill_pkg->cust_bill_pkg_display
2163 : ( $cust_bill_pkg );
2165 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
2166 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
2169 foreach my $display ( grep { defined($section)
2170 ? $_->section eq $section
2173 #grep { !$_->summary || !$summary_page } # bunk!
2174 grep { !$_->summary || $multisection }
2175 @cust_bill_pkg_display
2179 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
2180 $display->billpkgdisplaynum. "\n"
2183 my $type = $display->type;
2185 my $desc = $cust_bill_pkg->desc;
2186 $desc = substr($desc, 0, $maxlength). '...'
2187 if $format eq 'latex' && length($desc) > $maxlength;
2189 my %details_opt = ( 'format' => $format,
2190 'escape_function' => $escape_function,
2191 'format_function' => $format_function,
2192 'no_usage' => $opt{'no_usage'},
2195 if ( ref($cust_bill_pkg) eq 'FS::quotation_pkg' ) {
2197 warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
2200 if ( $cust_bill_pkg->setup != 0 ) {
2201 my $description = $desc;
2202 $description .= ' Setup'
2203 if $cust_bill_pkg->recur != 0
2204 || $discount_show_always
2205 || $cust_bill_pkg->recur_show_zero;
2207 'description' => $description,
2208 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2211 if ( $cust_bill_pkg->recur != 0 ) {
2213 'description' => "$desc (". $cust_bill_pkg->part_pkg->freq_pretty.")",
2214 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2218 } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
2220 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
2223 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2225 # start/end dates for invoice formats that do nonstandard
2227 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
2229 if ( (!$type || $type eq 'S')
2230 && ( $cust_bill_pkg->setup != 0
2231 || $cust_bill_pkg->setup_show_zero
2236 warn "$me _items_cust_bill_pkg adding setup\n"
2239 my $description = $desc;
2240 $description .= ' Setup'
2241 if $cust_bill_pkg->recur != 0
2242 || $discount_show_always
2243 || $cust_bill_pkg->recur_show_zero;
2246 unless ( $cust_pkg->part_pkg->hide_svc_detail
2247 || $cust_bill_pkg->hidden )
2250 push @d, map &{$escape_function}($_),
2251 $cust_pkg->h_labels_short($self->_date, undef, 'I')
2252 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2254 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2255 my $loc = $cust_pkg->location_label;
2256 $loc = substr($loc, 0, $maxlength). '...'
2257 if $format eq 'latex' && length($loc) > $maxlength;
2258 push @d, &{$escape_function}($loc);
2261 } #unless hiding service details
2263 push @d, $cust_bill_pkg->details(%details_opt)
2264 if $cust_bill_pkg->recur == 0;
2266 if ( $cust_bill_pkg->hidden ) {
2267 $s->{amount} += $cust_bill_pkg->setup;
2268 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2269 push @{ $s->{ext_description} }, @d;
2273 description => $description,
2274 #pkgpart => $part_pkg->pkgpart,
2275 pkgnum => $cust_bill_pkg->pkgnum,
2276 amount => $cust_bill_pkg->setup,
2277 setup_show_zero => $cust_bill_pkg->setup_show_zero,
2278 unit_amount => $cust_bill_pkg->unitsetup,
2279 quantity => $cust_bill_pkg->quantity,
2280 ext_description => \@d,
2286 if ( ( !$type || $type eq 'R' || $type eq 'U' )
2288 $cust_bill_pkg->recur != 0
2289 || $cust_bill_pkg->setup == 0
2290 || $discount_show_always
2291 || $cust_bill_pkg->recur_show_zero
2296 warn "$me _items_cust_bill_pkg adding recur/usage\n"
2299 my $is_summary = $display->summary;
2300 my $description = ($is_summary && $type && $type eq 'U')
2301 ? "Usage charges" : $desc;
2303 #pry be a bit more efficient to look some of this conf stuff up
2306 $conf->exists('disable_line_item_date_ranges')
2307 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
2310 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
2311 $cust_main->agentnum
2313 if ( defined($date_style) && $date_style eq 'month_of' ) {
2314 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
2315 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
2316 my $desc = $conf->config( 'cust_bill-line_item-date_description',
2317 $cust_main->agentnum
2319 $desc .= ' ' unless $desc =~ /\s$/;
2320 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
2322 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
2323 " - ". time2str($date_format, $cust_bill_pkg->edate);
2325 $description .= " ($time_period)";
2329 my @seconds = (); # for display of usage info
2331 #at least until cust_bill_pkg has "past" ranges in addition to
2332 #the "future" sdate/edate ones... see #3032
2333 my @dates = ( $self->_date );
2334 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2335 push @dates, $prev->sdate if $prev;
2336 push @dates, undef if !$prev;
2338 unless ( $cust_pkg->part_pkg->hide_svc_detail
2339 || $cust_bill_pkg->itemdesc
2340 || $cust_bill_pkg->hidden
2341 || $is_summary && $type && $type eq 'U' )
2344 warn "$me _items_cust_bill_pkg adding service details\n"
2347 push @d, map &{$escape_function}($_),
2348 $cust_pkg->h_labels_short(@dates, 'I')
2349 #$cust_bill_pkg->edate,
2350 #$cust_bill_pkg->sdate)
2351 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
2353 warn "$me _items_cust_bill_pkg done adding service details\n"
2356 if ( $cust_pkg->locationnum != $cust_main->ship_locationnum ) {
2357 my $loc = $cust_pkg->location_label;
2358 $loc = substr($loc, 0, $maxlength). '...'
2359 if $format eq 'latex' && length($loc) > $maxlength;
2360 push @d, &{$escape_function}($loc);
2363 # Display of seconds_since_sqlradacct:
2364 # On the invoice, when processing @detail_items, look for a field
2365 # named 'seconds'. This will contain total seconds for each
2366 # service, in the same order as @ext_description. For services
2367 # that don't support this it will show undef.
2368 if ( $conf->exists('svc_acct-usage_seconds')
2369 and ! $cust_bill_pkg->pkgpart_override ) {
2370 foreach my $cust_svc (
2371 $cust_pkg->h_cust_svc(@dates, 'I')
2374 # eval because not having any part_export_usage exports
2375 # is a fatal error, last_bill/_date because that's how
2376 # sqlradius_hour billing does it
2378 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
2380 push @seconds, $sec;
2382 } #if svc_acct-usage_seconds
2386 unless ( $is_summary ) {
2387 warn "$me _items_cust_bill_pkg adding details\n"
2390 #instead of omitting details entirely in this case (unwanted side
2391 # effects), just omit CDRs
2392 $details_opt{'no_usage'} = 1
2393 if $type && $type eq 'R';
2395 push @d, $cust_bill_pkg->details(%details_opt);
2398 warn "$me _items_cust_bill_pkg calculating amount\n"
2403 $amount = $cust_bill_pkg->recur;
2404 } elsif ($type eq 'R') {
2405 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2406 } elsif ($type eq 'U') {
2407 $amount = $cust_bill_pkg->usage;
2411 ( $cust_bill_pkg->unitrecur > 0 ) ? $cust_bill_pkg->unitrecur
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} += $unit_amount;
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 => $unit_amount,
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} += $unit_amount,
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 => $unit_amount,
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;