quotations, RT#16996
authorIvan Kohler <ivan@freeside.biz>
Wed, 4 Jul 2012 01:51:51 +0000 (18:51 -0700)
committerIvan Kohler <ivan@freeside.biz>
Wed, 4 Jul 2012 01:51:51 +0000 (18:51 -0700)
21 files changed:
FS/FS/AccessRight.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm [new file with mode: 0644]
FS/FS/Upgrade.pm
FS/FS/access_right.pm
FS/FS/cust_bill.pm
FS/FS/cust_main.pm
FS/FS/prospect_main.pm
FS/FS/quotation.pm
FS/MANIFEST
conf/quotation_html [new file with mode: 0644]
conf/quotation_latex [new file with mode: 0644]
conf/quotation_latexnotes [new file with mode: 0644]
httemplate/edit/process/quotation.html
httemplate/edit/quotation.html
httemplate/elements/tr-fixed-cust_main.html [new file with mode: 0644]
httemplate/elements/tr-fixed-date.html [new file with mode: 0644]
httemplate/elements/tr-fixed-prospect_main.html [new file with mode: 0644]
httemplate/view/prospect_main.html
httemplate/view/quotation.html

index eb9974a..4de2948 100644 (file)
@@ -98,6 +98,7 @@ tie my %rights, 'Tie::IxHash',
     #'New contact',
     #'View customer contacts',
     #'List contacts',
+    'Generate quotation',
   ],
   
   ###
index 8c9d56f..7e64130 100644 (file)
@@ -1197,7 +1197,15 @@ sub reason_type_options {
   {
     'key'         => 'invoice_html',
     'section'     => 'invoicing',
-    'description' => 'Optional HTML template for invoices.  See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:Administration#HTML_invoice_templates">billing documentation</a> for details.',
+    'description' => 'HTML template for invoices.  See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:2.1:Documentation:Administration#HTML_invoice_templates">billing documentation</a> for details.',
+
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'quotation_html',
+    'section'     => '',
+    'description' => 'HTML template for quotations.',
 
     'type'        => 'textarea',
   },
@@ -1245,6 +1253,13 @@ sub reason_type_options {
   },
 
   {
+    'key'         => 'quotation_latex',
+    'section'     => '',
+    'description' => 'LaTeX template for typeset PostScript quotations.',
+    'type'        => 'textarea',
+  },
+
+  {
     'key'         => 'invoice_latextopmargin',
     'section'     => 'invoicing',
     'description' => 'Optional LaTeX invoice topmargin setting. Include units.',
@@ -1303,6 +1318,15 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'quotation_latexnotes',
+    'section'     => '',
+    'description' => 'Notes section for LaTeX typeset PostScript quotations.',
+    'type'        => 'textarea',
+    'per_agent'   => 1,
+    'per_locale'  => 1,
+  },
+
+  {
     'key'         => 'invoice_latexfooter',
     'section'     => 'invoicing',
     'description' => 'Footer for LaTeX typeset PostScript invoices.',
index a90c73a..797b705 100644 (file)
@@ -1054,8 +1054,50 @@ sub tables_hashref {
       'index'       => [ [ 'company' ], [ 'agentnum' ], [ 'disabled' ] ],
     },
 
-    #eventually use for billing & ship from cust_main too
-    #for now, just cust_pkg locations
+    'quotation' => {
+      'columns' => [
+        #regular fields
+        'quotationnum',   'serial',     '', '', '', '', 
+        'prospectnum',       'int', 'NULL', '', '', '',
+        'custnum',           'int', 'NULL', '', '', '',
+        '_date',        @date_type,             '', '', 
+        'disabled',         'char', 'NULL',  1, '', '', 
+        'usernum',           'int', 'NULL', '', '', '',
+        #'total',      @money_type,       '', '', 
+        #'quotation_term', 'varchar', 'NULL', $char_d, '', '',
+      ],
+      'primary_key' => 'quotationnum',
+      'unique' => [],
+      'index' => [ [ 'prospectnum' ], ['custnum'], ],
+    },
+
+    'quotation_pkg' => {
+      'columns' => [
+        'quotationpkgnum',   'serial',     '', '', '', '', 
+        'pkgpart',              'int',     '', '', '', '', 
+        'locationnum',          'int', 'NULL', '', '', '',
+        'start_date',      @date_type,             '', '', 
+        'contract_end',    @date_type,             '', '',
+        'quantity',             'int', 'NULL', '', '', '',
+        'waive_setup',         'char', 'NULL',  1, '', '', 
+      ],
+      'primary_key' => 'quotationpkgnum',
+      'unique' => [],
+      'index' => [ ['pkgpart'], ],
+    },
+
+    'quotation_pkg_discount' => {
+      'columns' => [
+        'quotationpkgdiscountnum', 'serial', '', '', '', '',
+        'quotationpkgnum',            'int', '', '', '', '', 
+        'discountnum',                'int', '', '', '', '',
+        #'end_date',              @date_type,         '', '',
+      ],
+      'primary_key' => 'quotationpkgdiscountnum',
+      'unique' => [],
+      'index'  => [ [ 'quotationpkgnum' ], ], #[ 'discountnum' ] ],
+    },
+
     'cust_location' => { #'location' now that its prospects too, but...
       'columns' => [
         'locationnum',      'serial',     '',      '', '', '',
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
new file mode 100644 (file)
index 0000000..19b452e
--- /dev/null
@@ -0,0 +1,2525 @@
+package FS::Template_Mixin;
+
+use strict;
+use vars qw( $DEBUG $me
+             $money_char $date_format $rdate_format $date_format_long );
+             # but NOT $conf
+use vars qw( $invoice_lines @buf ); #yuck
+use List::Util qw(sum);
+use Date::Format;
+use Date::Language;
+use Text::Template 1.20;
+use File::Temp 0.14;
+use HTML::Entities;
+use Locale::Country;
+use FS::UID;
+use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( generate_ps generate_pdf );
+use FS::pkg_category;
+use FS::pkg_class;
+use FS::L10N;
+
+$DEBUG = 0;
+$me = '[FS::Template_Mixin]';
+FS::UID->install_callback( sub { 
+  my $conf = new FS::Conf; #global
+  $money_char       = $conf->config('money_char')       || '$';  
+  $date_format      = $conf->config('date_format')      || '%x'; #/YY
+  $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
+  $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
+} );
+
+=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+
+Returns an text invoice, as a list of lines.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_text {
+  my $self = shift;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
+
+  my %params = ( 'format' => 'template' );
+  $params{'time'} = $today if $today;
+  $params{'template'} = $template if $template;
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+
+  $self->print_generic( %params );
+}
+
+=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+
+Internal method - returns a filename of a filled-in LaTeX template for this
+invoice (Note: add ".tex" to get the actual filename), and a filename of
+an associated logo (with the .eps extension included).
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_latex {
+  my $self = shift;
+  my $conf = $self->conf;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
+
+  my %params = ( 'format' => 'latex' );
+  $params{'time'} = $today if $today;
+  $params{'template'} = $template if $template;
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+
+  $template ||= $self->_agent_template
+    if $self->can('_agent_template');
+
+  my $pkey = $self->primary_key;
+  my $tmp_template = $self->table. '.'. $self->$pkey. '.XXXXXXXX';
+
+  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
+  my $lh = new File::Temp(
+    TEMPLATE => $tmp_template,
+    DIR      => $dir,
+    SUFFIX   => '.eps',
+    UNLINK   => 0,
+  ) or die "can't open temp file: $!\n";
+
+  my $agentnum = $self->cust_main->agentnum;
+
+  if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
+    print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
+      or die "can't write temp file: $!\n";
+  } else {
+    print $lh $conf->config_binary('logo.eps', $agentnum)
+      or die "can't write temp file: $!\n";
+  }
+  close $lh;
+  $params{'logo_file'} = $lh->filename;
+
+  if( $conf->exists('invoice-barcode') && $self->can('invoice_barcode') ) {
+      my $png_file = $self->invoice_barcode($dir);
+      my $eps_file = $png_file;
+      $eps_file =~ s/\.png$/.eps/g;
+      $png_file =~ /(barcode.*png)/;
+      $png_file = $1;
+      $eps_file =~ /(barcode.*eps)/;
+      $eps_file = $1;
+
+      my $curr_dir = cwd();
+      chdir($dir); 
+      # after painfuly long experimentation, it was determined that sam2p won't
+      #        accept : and other chars in the path, no matter how hard I tried to
+      # escape them, hence the chdir (and chdir back, just to be safe)
+      system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
+       or die "sam2p failed: $!\n";
+      unlink($png_file);
+      chdir($curr_dir);
+
+      $params{'barcode_file'} = $eps_file;
+  }
+
+  my @filled_in = $self->print_generic( %params );
+  
+  my $fh = new File::Temp( TEMPLATE => $tmp_template,
+                           DIR      => $dir,
+                           SUFFIX   => '.tex',
+                           UNLINK   => 0,
+                         ) or die "can't open temp file: $!\n";
+  binmode($fh, ':utf8'); # language support
+  print $fh join('', @filled_in );
+  close $fh;
+
+  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
+  return ($1, $params{'logo_file'}, $params{'barcode_file'});
+
+}
+
+=item print_generic OPTION => VALUE ...
+
+Internal method - returns a filled-in template for this invoice as a scalar.
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+Non optional options include 
+  format - latex, html, template
+
+Optional options include
+
+template - a value used as a suffix for a configuration template
+
+time - a value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+cid - 
+
+unsquelch_cdr - overrides any per customer cdr squelching when true
+
+notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+locale - override customer's locale
+
+=cut
+
+#what's with all the sprintf('%10.2f')'s in here?  will it cause any
+# (alignment in text invoice?) problems to change them all to '%.2f' ?
+# yes: fixed width/plain text printing will be borked
+sub print_generic {
+  my( $self, %params ) = @_;
+  my $conf = $self->conf;
+  my $today = $params{today} ? $params{today} : time;
+  warn "$me print_generic called on $self with suffix $params{template}\n"
+    if $DEBUG;
+
+  my $format = $params{format};
+  die "Unknown format: $format"
+    unless $format =~ /^(latex|html|template)$/;
+
+  my $cust_main = $self->cust_main || $self->prospect_main;
+  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
+    unless $cust_main->payname
+        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
+
+  my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
+                     'html'     => [ '<%=', '%>' ],
+                     'template' => [ '{', '}' ],
+                   );
+
+  warn "$me print_generic creating template\n"
+    if $DEBUG > 1;
+
+  #create the template
+  my $template = $params{template} ? $params{template} : $self->_agent_template;
+  my $templatefile = $self->template_conf. $format;
+  $templatefile .= "_$template"
+    if length($template) && $conf->exists($templatefile."_$template");
+  my @invoice_template = map "$_\n", $conf->config($templatefile)
+    or die "cannot load config data $templatefile";
+
+  my $old_latex = '';
+  if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
+    #change this to a die when the old code is removed
+    warn "old-style invoice template $templatefile; ".
+         "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
+    $old_latex = 'true';
+    @invoice_template = _translate_old_latex_format(@invoice_template);
+  } 
+
+  warn "$me print_generic creating T:T object\n"
+    if $DEBUG > 1;
+
+  my $text_template = new Text::Template(
+    TYPE => 'ARRAY',
+    SOURCE => \@invoice_template,
+    DELIMITERS => $delimiters{$format},
+  );
+
+  warn "$me print_generic compiling T:T object\n"
+    if $DEBUG > 1;
+
+  $text_template->compile()
+    or die "Can't compile $templatefile: $Text::Template::ERROR\n";
+
+
+  # additional substitution could possibly cause breakage in existing templates
+  my %convert_maps = ( 
+    'latex' => {
+                 'notes'         => sub { map "$_", @_ },
+                 'footer'        => sub { map "$_", @_ },
+                 'smallfooter'   => sub { map "$_", @_ },
+                 'returnaddress' => sub { map "$_", @_ },
+                 'coupon'        => sub { map "$_", @_ },
+                 'summary'       => sub { map "$_", @_ },
+               },
+    'html'  => {
+                 'notes' =>
+                   sub {
+                     map { 
+                       s/%%(.*)$/<!-- $1 -->/g;
+                       s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
+                       s/\\begin\{enumerate\}/<ol>/g;
+                       s/\\item /  <li>/g;
+                       s/\\end\{enumerate\}/<\/ol>/g;
+                       s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
+                       s/\\\\\*/<br>/g;
+                       s/\\dollar ?/\$/g;
+                       s/\\#/#/g;
+                       s/~/&nbsp;/g;
+                       $_;
+                     }  @_
+                   },
+                 'footer' =>
+                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
+                 'smallfooter' =>
+                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
+                 'returnaddress' =>
+                   sub {
+                     map { 
+                       s/~/&nbsp;/g;
+                       s/\\\\\*?\s*$/<BR>/;
+                       s/\\hyphenation\{[\w\s\-]+}//;
+                       s/\\([&])/$1/g;
+                       $_;
+                     }  @_
+                   },
+                 'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
+               },
+    'template' => {
+                 'notes' =>
+                   sub {
+                     map { 
+                       s/%%.*$//g;
+                       s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
+                       s/\\begin\{enumerate\}//g;
+                       s/\\item /  * /g;
+                       s/\\end\{enumerate\}//g;
+                       s/\\textbf\{(.*)\}/$1/g;
+                       s/\\\\\*/ /;
+                       s/\\dollar ?/\$/g;
+                       $_;
+                     }  @_
+                   },
+                 'footer' =>
+                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
+                 'smallfooter' =>
+                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
+                 'returnaddress' =>
+                   sub {
+                     map { 
+                       s/~/ /g;
+                       s/\\\\\*?\s*$/\n/;             # dubious
+                       s/\\hyphenation\{[\w\s\-]+}//;
+                       $_;
+                     }  @_
+                   },
+                 'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
+               },
+  );
+
+
+  # hashes for differing output formats
+  my %nbsps = ( 'latex'    => '~',
+                'html'     => '',    # '&nbps;' would be nice
+                'template' => '',    # not used
+              );
+  my $nbsp = $nbsps{$format};
+
+  my %escape_functions = ( 'latex'    => \&_latex_escape,
+                           'html'     => \&_html_escape_nbsp,#\&encode_entities,
+                           'template' => sub { shift },
+                         );
+  my $escape_function = $escape_functions{$format};
+  my $escape_function_nonbsp = ($format eq 'html')
+                                 ? \&_html_escape : $escape_function;
+
+  my %date_formats = ( 'latex'    => $date_format_long,
+                       'html'     => $date_format_long,
+                       'template' => '%s',
+                     );
+  $date_formats{'html'} =~ s/ /&nbsp;/g;
+
+  my $date_format = $date_formats{$format};
+
+  my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
+                                               },
+                             'html'     => sub { return '<b>'. shift(). '</b>'
+                                               },
+                             'template' => sub { shift },
+                           );
+  my $embolden_function = $embolden_functions{$format};
+
+  my %newline_tokens = (  'latex'     => '\\\\',
+                          'html'      => '<br>',
+                          'template'  => "\n",
+                        );
+  my $newline_token = $newline_tokens{$format};
+
+  warn "$me generating template variables\n"
+    if $DEBUG > 1;
+
+  # generate template variables
+  my $returnaddress;
+  if (
+         defined( $conf->config_orbase( "invoice_${format}returnaddress",
+                                        $template
+                                      )
+                )
+       && length( $conf->config_orbase( "invoice_${format}returnaddress",
+                                        $template
+                                      )
+                )
+  ) {
+
+    $returnaddress = join("\n",
+      $conf->config_orbase("invoice_${format}returnaddress", $template)
+    );
+
+  } elsif ( grep /\S/,
+            $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
+
+    my $convert_map = $convert_maps{$format}{'returnaddress'};
+    $returnaddress =
+      join( "\n",
+            &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
+                                                 $template
+                                               )
+                         )
+          );
+  } elsif ( grep /\S/, $conf->config('company_address', $cust_main->agentnum) ) {
+
+    my $convert_map = $convert_maps{$format}{'returnaddress'};
+    $returnaddress = join( "\n", &$convert_map(
+                                   map { s/( {2,})/'~' x length($1)/eg;
+                                         s/$/\\\\\*/;
+                                         $_
+                                       }
+                                     ( $conf->config('company_name', $cust_main->agentnum),
+                                       $conf->config('company_address', $cust_main->agentnum),
+                                     )
+                                 )
+                     );
+
+  } else {
+
+    my $warning = "Couldn't find a return address; ".
+                  "do you need to set the company_address configuration value?";
+    warn "$warning\n";
+    $returnaddress = $nbsp;
+    #$returnaddress = $warning;
+
+  }
+
+  warn "$me generating invoice data\n"
+    if $DEBUG > 1;
+
+  my $agentnum = $cust_main->agentnum;
+
+  my %invoice_data = (
+
+    #invoice from info
+    'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
+    'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
+    'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
+    'returnaddress'   => $returnaddress,
+    'agent'           => &$escape_function($cust_main->agent->agent),
+
+    #invoice/quotation info
+    'invnum'          => $self->invnum,
+    'quotationnum'    => $self->quotationnum,
+    'date'            => time2str($date_format, $self->_date),
+    'today'           => time2str($date_format_long, $today),
+    'terms'           => $self->terms,
+    'template'        => $template, #params{'template'},
+    'notice_name'     => ($params{'notice_name'} || $self->notice_name),#escape_function?
+    'current_charges' => sprintf("%.2f", $self->charged),
+    'duedate'         => $self->due_date2str($rdate_format), #date_format?
+
+    #customer info
+    'custnum'         => $cust_main->display_custnum,
+    'prospectnum'     => $cust_main->prospectnum,
+    'agent_custid'    => &$escape_function($cust_main->agent_custid),
+    ( map { $_ => &$escape_function($cust_main->$_()) } qw(
+      payname company address1 address2 city state zip fax
+    )),
+
+    #global config
+    'ship_enable'     => $conf->exists('invoice-ship_address'),
+    'unitprices'      => $conf->exists('invoice-unitprice'),
+    'smallernotes'    => $conf->exists('invoice-smallernotes'),
+    'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
+    'balance_due_below_line' => $conf->exists('balance_due_below_line'),
+   
+    #layout info -- would be fancy to calc some of this and bury the template
+    #               here in the code
+    'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
+    'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
+    'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
+    'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
+    'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
+    'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
+    'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
+    'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
+    'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
+    'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+
+    # better hang on to conf_dir for a while (for old templates)
+    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+
+    #these are only used when doing paged plaintext
+    'page'            => 1,
+    'total_pages'     => 1,
+
+  );
+  #localization
+  my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
+  $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
+  my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
+  # eval to avoid death for unimplemented languages
+  my $dh = eval { Date::Language->new($info{'name'}) } ||
+           Date::Language->new(); # fall back to English
+  # prototype here to silence warnings
+  $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
+  # eventually use this date handle everywhere in here, too
+
+  my $min_sdate = 999999999999;
+  my $max_edate = 0;
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+    $min_sdate = $cust_bill_pkg->sdate
+      if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
+    $max_edate = $cust_bill_pkg->edate
+      if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
+  }
+
+  $invoice_data{'bill_period'} = '';
+  $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
+    . " to " . time2str('%e %h', $max_edate)
+    if ($max_edate != 0 && $min_sdate != 999999999999);
+
+  $invoice_data{finance_section} = '';
+  if ( $conf->config('finance_pkgclass') ) {
+    my $pkg_class =
+      qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
+    $invoice_data{finance_section} = $pkg_class->categoryname;
+  } 
+  $invoice_data{finance_amount} = '0.00';
+  $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
+
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  foreach ( qw( address1 address2 city state zip country fax) ){
+    my $method = 'ship_'.$_;
+    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+  }
+  foreach ( qw( contact company ) ) { #compatibility
+    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
+  }
+  $invoice_data{'ship_country'} = ''
+    if ( $invoice_data{'ship_country'} eq $countrydefault );
+  
+  $invoice_data{'cid'} = $params{'cid'}
+    if $params{'cid'};
+
+  if ( $cust_main->country eq $countrydefault ) {
+    $invoice_data{'country'} = '';
+  } else {
+    $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
+  }
+
+  my @address = ();
+  $invoice_data{'address'} = \@address;
+  push @address,
+    $cust_main->payname.
+      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
+        ? " (P.O. #". $cust_main->payinfo. ")"
+        : ''
+      )
+  ;
+  push @address, $cust_main->company
+    if $cust_main->company;
+  push @address, $cust_main->address1;
+  push @address, $cust_main->address2
+    if $cust_main->address2;
+  push @address,
+    $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
+  push @address, $invoice_data{'country'}
+    if $invoice_data{'country'};
+  push @address, ''
+    while (scalar(@address) < 5);
+
+  $invoice_data{'logo_file'} = $params{'logo_file'}
+    if $params{'logo_file'};
+  $invoice_data{'barcode_file'} = $params{'barcode_file'}
+    if $params{'barcode_file'};
+  $invoice_data{'barcode_img'} = $params{'barcode_img'}
+    if $params{'barcode_img'};
+  $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
+    if $params{'barcode_cid'};
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
+
+  # the customer's current balance as shown on the invoice before this one
+  $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+
+  # the change in balance from that invoice to this one
+  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
+
+  # the sum of amount owed on all previous invoices
+  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+
+  # the sum of amount owed on all invoices
+  $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+
+  # info from customer's last invoice before this one, for some 
+  # summary formats
+  $invoice_data{'last_bill'} = {};
+  my $last_bill = $pr_cust_bill[-1];
+  if ( $last_bill ) {
+    $invoice_data{'last_bill'} = {
+      '_date'     => $last_bill->_date, #unformatted
+      # all we need for now
+    };
+  }
+
+  my $summarypage = '';
+  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+    $summarypage = 1;
+  }
+  $invoice_data{'summarypage'} = $summarypage;
+
+  warn "$me substituting variables in notes, footer, smallfooter\n"
+    if $DEBUG > 1;
+
+  my $tc = $self->template_conf;
+  my @include = ( [ $tc,        'notes' ],
+                  [ 'invoice_', 'footer' ],
+                  [ 'invoice_', 'smallfooter', ],
+                );
+  push @include, [ $tc,        'coupon', ]
+    unless $params{'no_coupon'};
+
+  foreach my $i (@include) {
+
+    my($base, $include) = @$i;
+
+    my $inc_file = $conf->key_orbase("$base$format$include", $template);
+    my @inc_src;
+
+    if ( $conf->exists($inc_file, $agentnum)
+         && length( $conf->config($inc_file, $agentnum) ) ) {
+
+      @inc_src = $conf->config($inc_file, $agentnum);
+
+    } else {
+
+      $inc_file = $conf->key_orbase("${base}latex$include", $template);
+
+      my $convert_map = $convert_maps{$format}{$include};
+
+      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+                       s/--\@\]/$delimiters{$format}[1]/g;
+                       $_;
+                     } 
+                 &$convert_map( $conf->config($inc_file, $agentnum) );
+
+    }
+
+    my $inc_tt = new Text::Template (
+      TYPE       => 'ARRAY',
+      SOURCE     => [ map "$_\n", @inc_src ],
+      DELIMITERS => $delimiters{$format},
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+    unless ( $inc_tt->compile() ) {
+      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+      die $error;
+    }
+
+    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+    $invoice_data{$include} =~ s/\n+$//
+      if ($format eq 'latex');
+  }
+
+  # let invoices use either of these as needed
+  $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
+    ? $cust_main->payinfo : '';
+  $invoice_data{'po_line'} = 
+    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+      ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
+      : $nbsp;
+
+  my %money_chars = ( 'latex'    => '',
+                      'html'     => $conf->config('money_char') || '$',
+                      'template' => '',
+                    );
+  my $money_char = $money_chars{$format};
+
+  my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
+                            'html'     => $conf->config('money_char') || '$',
+                            'template' => '',
+                          );
+  my $other_money_char = $other_money_chars{$format};
+  $invoice_data{'dollar'} = $other_money_char;
+
+  my @detail_items = ();
+  my @total_items = ();
+  my @buf = ();
+  my @sections = ();
+
+  $invoice_data{'detail_items'} = \@detail_items;
+  $invoice_data{'total_items'} = \@total_items;
+  $invoice_data{'buf'} = \@buf;
+  $invoice_data{'sections'} = \@sections;
+
+  warn "$me generating sections\n"
+    if $DEBUG > 1;
+
+  my $previous_section = { 'description' => $self->mt('Previous Charges'),
+                           'subtotal'    => $other_money_char.
+                                            sprintf('%.2f', $pr_total),
+                           'summarized'  => '', #why? $summarypage ? 'Y' : '',
+                         };
+  $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
+    join(' / ', map { $cust_main->balance_date_range(@$_) }
+                $self->_prior_month30s
+        )
+    if $conf->exists('invoice_include_aging');
+
+  my $taxtotal = 0;
+  my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
+                      'subtotal'    => $taxtotal,   # adjusted below
+                    };
+  my $tax_weight = _pkg_category($tax_section->{description})
+                        ? _pkg_category($tax_section->{description})->weight
+                        : 0;
+  $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
+  $tax_section->{'sort_weight'} = $tax_weight;
+
+
+  my $adjusttotal = 0;
+  my $adjust_section = { 'description' => 
+    $self->mt('Credits, Payments, and Adjustments'),
+                         'subtotal'    => 0,   # adjusted below
+                       };
+  my $adjust_weight = _pkg_category($adjust_section->{description})
+                        ? _pkg_category($adjust_section->{description})->weight
+                        : 0;
+  $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
+  $adjust_section->{'sort_weight'} = $adjust_weight;
+
+  my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
+  my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  $invoice_data{'multisection'} = $multisection;
+  my $late_sections = [];
+  my $extra_sections = [];
+  my $extra_lines = ();
+
+  my $default_section = { 'description' => '',
+                          'subtotal'    => '', 
+                          'no_subtotal' => 1,
+                        };
+
+  if ( $multisection ) {
+    ($extra_sections, $extra_lines) =
+      $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
+      if $conf->exists('usage_class_as_a_section', $cust_main->agentnum)
+      && $self->can('_items_extra_usage_sections');
+
+    push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
+
+    push @detail_items, @$extra_lines if $extra_lines;
+    push @sections,
+      $self->_items_sections( $late_sections,      # this could stand a refactor
+                              $summarypage,
+                              $escape_function_nonbsp,
+                              $extra_sections,
+                              $format,             #bah
+                            );
+    if (    $conf->exists('svc_phone_sections')
+         && $self->can('_items_svc_phone_sections')
+       )
+    {
+      my ($phone_sections, $phone_lines) =
+        $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
+      push @{$late_sections}, @$phone_sections;
+      push @detail_items, @$phone_lines;
+    }
+    if ( $conf->exists('voip-cust_accountcode_cdr')
+         && $cust_main->accountcode_cdr
+         && $self->can('_items_accountcode_cdr')
+       )
+    {
+      my ($accountcode_section, $accountcode_lines) =
+        $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
+      if ( scalar(@$accountcode_lines) ) {
+          push @{$late_sections}, $accountcode_section;
+          push @detail_items, @$accountcode_lines;
+      }
+    }
+  } else {# not multisection
+    # make a default section
+    push @sections, $default_section;
+    # and calculate the finance charge total, since it won't get done otherwise.
+    # XXX possibly other totals?
+    # XXX possibly finance_pkgclass should not be used in this manner?
+    if ( $conf->exists('finance_pkgclass') ) {
+      my @finance_charges;
+      foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+        if ( grep { $_->section eq $invoice_data{finance_section} }
+             $cust_bill_pkg->cust_bill_pkg_display ) {
+          # I think these are always setup fees, but just to be sure...
+          push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
+        }
+      }
+      $invoice_data{finance_amount} = 
+        sprintf('%.2f', sum( @finance_charges ) || 0);
+    }
+  }
+
+  unless (    $conf->exists('disable_previous_balance', $agentnum)
+           || $conf->exists('previous_balance-summary_only')
+           || ! $self->can('_items_previous')
+         )
+  {
+
+    warn "$me adding previous balances\n"
+      if $DEBUG > 1;
+
+    foreach my $line_item ( $self->_items_previous ) {
+
+      my $detail = {
+        ext_description => [],
+      };
+      $detail->{'ref'} = $line_item->{'pkgnum'};
+      $detail->{'quantity'} = 1;
+      $detail->{'section'} = $multisection ? $previous_section
+                                           : $default_section;
+      $detail->{'description'} = &$escape_function($line_item->{'description'});
+      if ( exists $line_item->{'ext_description'} ) {
+        @{$detail->{'ext_description'}} = map {
+          &$escape_function($_);
+        } @{$line_item->{'ext_description'}};
+      }
+      $detail->{'amount'} = ( $old_latex ? '' : $money_char).
+                            $line_item->{'amount'};
+      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+      push @detail_items, $detail;
+      push @buf, [ $detail->{'description'},
+                   $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+                 ];
+    }
+
+  }
+  
+  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) 
+    {
+    push @buf, ['','-----------'];
+    push @buf, [ $self->mt('Total Previous Balance'),
+                 $money_char. sprintf("%10.2f", $pr_total) ];
+    push @buf, ['',''];
+  }
+  if ( $conf->exists('svc_phone-did-summary') && $self->can('_did_summary') ) {
+      warn "$me adding DID summary\n"
+        if $DEBUG > 1;
+
+      my ($didsummary,$minutes) = $self->_did_summary;
+      my $didsummary_desc = 'DID Activity Summary (since last invoice)';
+      push @detail_items, 
+       { 'description' => $didsummary_desc,
+           'ext_description' => [ $didsummary, $minutes ],
+       };
+  }
+
+  foreach my $section (@sections, @$late_sections) {
+
+    warn "$me adding section \n". Dumper($section)
+      if $DEBUG > 1;
+
+    # begin some normalization
+    $section->{'subtotal'} = $section->{'amount'}
+      if $multisection
+         && !exists($section->{subtotal})
+         && exists($section->{amount});
+
+    $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
+      if ( $invoice_data{finance_section} &&
+           $section->{'description'} eq $invoice_data{finance_section} );
+
+    $section->{'subtotal'} = $other_money_char.
+                             sprintf('%.2f', $section->{'subtotal'})
+      if $multisection;
+
+    # continue some normalization
+    $section->{'amount'}   = $section->{'subtotal'}
+      if $multisection;
+
+
+    if ( $section->{'description'} ) {
+      push @buf, ( [ &$escape_function($section->{'description'}), '' ],
+                   [ '', '' ],
+                 );
+    }
+
+    warn "$me   setting options\n"
+      if $DEBUG > 1;
+
+    my $multilocation = scalar($cust_main->cust_location); #too expensive?
+    my %options = ();
+    $options{'section'} = $section if $multisection;
+    $options{'format'} = $format;
+    $options{'escape_function'} = $escape_function;
+    $options{'no_usage'} = 1 unless $unsquelched;
+    $options{'unsquelched'} = $unsquelched;
+    $options{'summary_page'} = $summarypage;
+    $options{'skip_usage'} =
+      scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
+    $options{'multilocation'} = $multilocation;
+    $options{'multisection'} = $multisection;
+
+    warn "$me   searching for line items\n"
+      if $DEBUG > 1;
+
+    foreach my $line_item ( $self->_items_pkg(%options) ) {
+
+      warn "$me     adding line item $line_item\n"
+        if $DEBUG > 1;
+
+      my $detail = {
+        ext_description => [],
+      };
+      $detail->{'ref'} = $line_item->{'pkgnum'};
+      $detail->{'quantity'} = $line_item->{'quantity'};
+      $detail->{'section'} = $section;
+      $detail->{'description'} = &$escape_function($line_item->{'description'});
+      if ( exists $line_item->{'ext_description'} ) {
+        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
+      }
+      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
+                              $line_item->{'amount'};
+      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
+                                 $line_item->{'unit_amount'};
+      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+      $detail->{'sdate'} = $line_item->{'sdate'};
+      $detail->{'edate'} = $line_item->{'edate'};
+      $detail->{'seconds'} = $line_item->{'seconds'};
+  
+      push @detail_items, $detail;
+      push @buf, ( [ $detail->{'description'},
+                     $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+                   ],
+                   map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
+                 );
+    }
+
+    if ( $section->{'description'} ) {
+      push @buf, ( ['','-----------'],
+                   [ $section->{'description'}. ' sub-total',
+                      $section->{'subtotal'} # already formatted this 
+                   ],
+                   [ '', '' ],
+                   [ '', '' ],
+                 );
+    }
+  
+  }
+
+  $invoice_data{current_less_finance} =
+    sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
+
+  if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
+    || $conf->exists('previous_balance-summary_only') )
+  {
+    unshift @sections, $previous_section if $pr_total;
+  }
+
+  warn "$me adding taxes\n"
+    if $DEBUG > 1;
+
+  foreach my $tax ( $self->_items_tax ) {
+
+    $taxtotal += $tax->{'amount'};
+
+    my $description = &$escape_function( $tax->{'description'} );
+    my $amount      = sprintf( '%.2f', $tax->{'amount'} );
+
+    if ( $multisection ) {
+
+      my $money = $old_latex ? '' : $money_char;
+      push @detail_items, {
+        ext_description => [],
+        ref          => '',
+        quantity     => '',
+        description  => $description,
+        amount       => $money. $amount,
+        product_code => '',
+        section      => $tax_section,
+      };
+
+    } else {
+
+      push @total_items, {
+        'total_item'   => $description,
+        'total_amount' => $other_money_char. $amount,
+      };
+
+    }
+
+    push @buf,[ $description,
+                $money_char. $amount,
+              ];
+
+  }
+  
+  if ( $taxtotal ) {
+    my $total = {};
+    $total->{'total_item'} = $self->mt('Sub-total');
+    $total->{'total_amount'} =
+      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
+
+    if ( $multisection ) {
+      $tax_section->{'subtotal'} = $other_money_char.
+                                   sprintf('%.2f', $taxtotal);
+      $tax_section->{'pretotal'} = 'New charges sub-total '.
+                                   $total->{'total_amount'};
+      push @sections, $tax_section if $taxtotal;
+    }else{
+      unshift @total_items, $total;
+    }
+  }
+  $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
+
+  push @buf,['','-----------'];
+  push @buf,[$self->mt( 
+              $conf->exists('disable_previous_balance', $agentnum) 
+               ? 'Total Charges'
+               : 'Total New Charges'
+             ),
+             $money_char. sprintf("%10.2f",$self->charged) ];
+  push @buf,['',''];
+
+  {
+    my $total = {};
+    my $item = 'Total';
+    $item = $conf->config('previous_balance-exclude_from_total')
+         || 'Total New Charges'
+      if $conf->exists('previous_balance-exclude_from_total');
+    my $amount = $self->charged +
+                   ( $conf->exists('disable_previous_balance', $agentnum) ||
+                     $conf->exists('previous_balance-exclude_from_total')
+                     ? 0
+                     : $pr_total
+                   );
+    $total->{'total_item'} = &$embolden_function($self->mt($item));
+    $total->{'total_amount'} =
+      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
+    if ( $multisection ) {
+      if ( $adjust_section->{'sort_weight'} ) {
+        $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
+          $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
+      } else {
+        $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
+          $other_money_char.  sprintf('%.2f', $self->charged );
+      } 
+    }else{
+      push @total_items, $total;
+    }
+    push @buf,['','-----------'];
+    push @buf,[$item,
+               $money_char.
+               sprintf( '%10.2f', $amount )
+              ];
+    push @buf,['',''];
+  }
+  
+  unless (    $conf->exists('disable_previous_balance', $agentnum) 
+           || ! $self->can('_items_credits')
+           || ! $self->can('_items_payments')
+         )
+  {
+    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+  
+    # credits
+    my $credittotal = 0;
+    foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
+      my $total;
+      $total->{'total_item'} = &$escape_function($credit->{'description'});
+      $credittotal += $credit->{'amount'};
+      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+      $adjusttotal += $credit->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($credit->{'description'}),
+          amount       => $money. $credit->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      } else {
+        push @total_items, $total;
+      }
+
+    }
+    $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
+
+    #credits (again)
+    foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+      push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+    }
+
+    # payments
+    my $paymenttotal = 0;
+    foreach my $payment ( $self->_items_payments ) {
+      my $total = {};
+      $total->{'total_item'} = &$escape_function($payment->{'description'});
+      $paymenttotal += $payment->{'amount'};
+      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+      $adjusttotal += $payment->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($payment->{'description'}),
+          amount       => $money. $payment->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      }else{
+        push @total_items, $total;
+      }
+      push @buf, [ $payment->{'description'},
+                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
+                 ];
+    }
+    $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
+  
+    if ( $multisection ) {
+      $adjust_section->{'subtotal'} = $other_money_char.
+                                      sprintf('%.2f', $adjusttotal);
+      push @sections, $adjust_section
+        unless $adjust_section->{sort_weight};
+    }
+
+    # create Balance Due message
+    { 
+      my $total;
+      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+      $total->{'total_amount'} =
+        &$embolden_function(
+          $other_money_char. sprintf('%.2f', $summarypage 
+                                               ? $self->charged +
+                                                 $self->billing_balance
+                                               : $self->owed + $pr_total
+                                    )
+        );
+      if ( $multisection && !$adjust_section->{sort_weight} ) {
+        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                         $total->{'total_amount'};
+      }else{
+        push @total_items, $total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->balance_due_msg, $money_char. 
+        sprintf("%10.2f", $balance_due ) ];
+    }
+
+    if ( $conf->exists('previous_balance-show_credit')
+        and $cust_main->balance < 0 ) {
+      my $credit_total = {
+        'total_item'    => &$embolden_function($self->credit_balance_msg),
+        'total_amount'  => &$embolden_function(
+          $other_money_char. sprintf('%.2f', -$cust_main->balance)
+        ),
+      };
+      if ( $multisection ) {
+        $adjust_section->{'posttotal'} .= $newline_token .
+          $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+      }
+      else {
+        push @total_items, $credit_total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->credit_balance_msg, $money_char. 
+        sprintf("%10.2f", -$cust_main->balance ) ];
+    }
+  }
+
+  if ( $multisection ) {
+    if (    $conf->exists('svc_phone_sections')
+         && $self->can('_items_svc_phone_sections')
+       )
+    {
+      my $total;
+      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+      $total->{'total_amount'} =
+        &$embolden_function(
+          $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+        );
+      my $last_section = pop @sections;
+      $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                     $total->{'total_amount'};
+      push @sections, $last_section;
+    }
+    push @sections, @$late_sections
+      if $unsquelched;
+  }
+
+  # make a discounts-available section, even without multisection
+  if ( $conf->exists('discount-show_available') 
+       and my @discounts_avail = $self->_items_discounts_avail ) {
+    my $discount_section = {
+      'description' => $self->mt('Discounts Available'),
+      'subtotal'    => '',
+      'no_subtotal' => 1,
+    };
+
+    push @sections, $discount_section;
+    push @detail_items, map { +{
+        'ref'         => '', #should this be something else?
+        'section'     => $discount_section,
+        'description' => &$escape_function( $_->{description} ),
+        'amount'      => $money_char . &$escape_function( $_->{amount} ),
+        'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
+    } } @discounts_avail;
+  }
+
+  # All sections and items are built; now fill in templates.
+  my @includelist = ();
+  push @includelist, 'summary' if $summarypage;
+  foreach my $include ( @includelist ) {
+
+    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
+    my @inc_src;
+
+    if ( length( $conf->config($inc_file, $agentnum) ) ) {
+
+      @inc_src = $conf->config($inc_file, $agentnum);
+
+    } else {
+
+      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+
+      my $convert_map = $convert_maps{$format}{$include};
+
+      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+                       s/--\@\]/$delimiters{$format}[1]/g;
+                       $_;
+                     } 
+                 &$convert_map( $conf->config($inc_file, $agentnum) );
+
+    }
+
+    my $inc_tt = new Text::Template (
+      TYPE       => 'ARRAY',
+      SOURCE     => [ map "$_\n", @inc_src ],
+      DELIMITERS => $delimiters{$format},
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+    unless ( $inc_tt->compile() ) {
+      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+      die $error;
+    }
+
+    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+    $invoice_data{$include} =~ s/\n+$//
+      if ($format eq 'latex');
+  }
+
+  $invoice_lines = 0;
+  my $wasfunc = 0;
+  foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
+    /invoice_lines\((\d*)\)/;
+    $invoice_lines += $1 || scalar(@buf);
+    $wasfunc=1;
+  }
+  die "no invoice_lines() functions in template?"
+    if ( $format eq 'template' && !$wasfunc );
+
+  if ($format eq 'template') {
+
+    if ( $invoice_lines ) {
+      $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
+      $invoice_data{'total_pages'}++
+        if scalar(@buf) % $invoice_lines;
+    }
+
+    #setup subroutine for the template
+    $invoice_data{invoice_lines} = sub {
+      my $lines = shift || scalar(@buf);
+      map { 
+        scalar(@buf)
+          ? shift @buf
+          : [ '', '' ];
+      }
+      ( 1 .. $lines );
+    };
+
+    my $lines;
+    my @collect;
+    while (@buf) {
+      push @collect, split("\n",
+        $text_template->fill_in( HASH => \%invoice_data )
+      );
+      $invoice_data{'page'}++;
+    }
+    map "$_\n", @collect;
+
+  } else { # this is where we actually create the invoice
+
+    warn "filling in template for invoice ". $self->invnum. "\n"
+      if $DEBUG;
+    warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
+      if $DEBUG > 1;
+
+    $text_template->fill_in(HASH => \%invoice_data);
+  }
+}
+
+sub notice_name { '('.shift->table.')'; }
+
+sub template_conf { 'invoice_'; }
+
+# helper routine for generating date ranges
+sub _prior_month30s {
+  my $self = shift;
+  my @ranges = (
+   [ 1,       2592000 ], # 0-30 days ago
+   [ 2592000, 5184000 ], # 30-60 days ago
+   [ 5184000, 7776000 ], # 60-90 days ago
+   [ 7776000, 0       ], # 90+   days ago
+  );
+
+  map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
+          $_->[1] ? $self->_date - $_->[1] - 1 : '',
+      ] }
+  @ranges;
+}
+
+=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
+
+Returns an postscript invoice, as a scalar.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_ps {
+  my $self = shift;
+
+  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
+  my $ps = generate_ps($file);
+  unlink($logofile);
+  unlink($barcodefile) if $barcodefile;
+
+  $ps;
+}
+
+=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
+
+Returns an PDF invoice, as a scalar.
+
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+=cut
+
+sub print_pdf {
+  my $self = shift;
+
+  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
+  my $pdf = generate_pdf($file);
+  unlink($logofile);
+  unlink($barcodefile) if $barcodefile;
+
+  $pdf;
+}
+
+=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
+
+Returns an HTML invoice, as a scalar.
+
+I<time> an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
+when emailing the invoice as part of a multipart/related MIME email.
+
+=cut
+
+sub print_html {
+  my $self = shift;
+  my %params;
+  if ( ref($_[0]) ) {
+    %params = %{ shift() }; 
+  }else{
+    $params{'time'} = shift;
+    $params{'template'} = shift;
+    $params{'cid'} = shift;
+  }
+
+  $params{'format'} = 'html';
+  
+  $self->print_generic( %params );
+}
+
+# quick subroutine for print_latex
+#
+# There are ten characters that LaTeX treats as special characters, which
+# means that they do not simply typeset themselves: 
+#      # $ % & ~ _ ^ \ { }
+#
+# TeX ignores blanks following an escaped character; if you want a blank (as
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
+
+sub _latex_escape {
+  my $value = shift;
+  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
+  $value =~ s/([<>])/\$$1\$/g;
+  $value;
+}
+
+sub _html_escape {
+  my $value = shift;
+  encode_entities($value);
+  $value;
+}
+
+sub _html_escape_nbsp {
+  my $value = _html_escape(shift);
+  $value =~ s/ +/&nbsp;/g;
+  $value;
+}
+
+#utility methods for print_*
+
+sub _translate_old_latex_format {
+  warn "_translate_old_latex_format called\n"
+    if $DEBUG; 
+
+  my @template = ();
+  while ( @_ ) {
+    my $line = shift;
+  
+    if ( $line =~ /^%%Detail\s*$/ ) {
+  
+      push @template, q![@--!,
+                      q!  foreach my $_tr_line (@detail_items) {!,
+                      q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
+                      q!      $_tr_line->{'description'} .= !, 
+                      q!        "\\tabularnewline\n~~".!,
+                      q!        join( "\\tabularnewline\n~~",!,
+                      q!          @{$_tr_line->{'ext_description'}}!,
+                      q!        );!,
+                      q!    }!;
+
+      while ( ( my $line_item_line = shift )
+              !~ /^%%EndDetail\s*$/                            ) {
+        $line_item_line =~ s/'/\\'/g;    # nice LTS
+        $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
+        $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
+        push @template, "    \$OUT .= '$line_item_line';";
+      }
+
+      push @template, '}',
+                      '--@]';
+      #' doh, gvim
+    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+
+      push @template, '[@--',
+                      '  foreach my $_tr_line (@total_items) {';
+
+      while ( ( my $total_item_line = shift )
+              !~ /^%%EndTotalDetails\s*$/                      ) {
+        $total_item_line =~ s/'/\\'/g;    # nice LTS
+        $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
+        $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
+        push @template, "    \$OUT .= '$total_item_line';";
+      }
+
+      push @template, '}',
+                      '--@]';
+
+    } else {
+      $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
+      push @template, $line;  
+    }
+  
+  }
+
+  if ($DEBUG) {
+    warn "$_\n" foreach @template;
+  }
+
+  (@template);
+}
+
+sub terms {
+  my $self = shift;
+  my $conf = $self->conf;
+
+  #check for an invoice-specific override
+  return $self->invoice_terms if $self->invoice_terms;
+  
+  #check for a customer- specific override
+  my $cust_main = $self->cust_main;
+  return $cust_main->invoice_terms if $cust_main && $cust_main->invoice_terms;
+
+  #use configured default
+  $conf->config('invoice_default_terms') || '';
+}
+
+sub due_date {
+  my $self = shift;
+  my $duedate = '';
+  if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $duedate = $self->_date() + ( $1 * 86400 );
+  }
+  $duedate;
+}
+
+sub due_date2str {
+  my $self = shift;
+  $self->due_date ? time2str(shift, $self->due_date) : '';
+}
+
+sub balance_due_msg {
+  my $self = shift;
+  my $msg = $self->mt('Balance Due');
+  return $msg unless $self->terms;
+  if ( $self->due_date ) {
+    $msg .= ' - ' . $self->mt('Please pay by'). ' '.
+      $self->due_date2str($date_format);
+  } elsif ( $self->terms ) {
+    $msg .= ' - '. $self->terms;
+  }
+  $msg;
+}
+
+sub balance_due_date {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $duedate = '';
+  if (    $conf->exists('invoice_default_terms') 
+       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
+  }
+  $duedate;
+}
+
+sub credit_balance_msg { 
+  my $self = shift;
+  $self->mt('Credit Balance Remaining')
+}
+
+=item _date_pretty
+
+Returns a string with the date, for example: "3/20/2008"
+
+=cut
+
+sub _date_pretty {
+  my $self = shift;
+  time2str($date_format, $self->_date);
+}
+
+=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
+
+Generate section information for all items appearing on this invoice.
+This will only be called for multi-section invoices.
+
+For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
+related display records (L<FS::cust_bill_pkg_display>) and organize 
+them into two groups ("early" and "late" according to whether they come 
+before or after the total), then into sections.  A subtotal is calculated 
+for each section.
+
+Section descriptions are returned in sort weight order.  Each consists 
+of a hash containing:
+
+description: the package category name, escaped
+subtotal: the total charges in that section
+tax_section: a flag indicating that the section contains only tax charges
+summarized: same as tax_section, for some reason
+sort_weight: the package category's sort weight
+
+If 'condense' is set on the display record, it also contains everything 
+returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
+coderefs to generate parts of the invoice.  This is not advised.
+
+Arguments:
+
+LATE: an arrayref to push the "late" section hashes onto.  The "early"
+group is simply returned from the method.
+
+SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
+Turning this on has the following effects:
+- Ignores display items with the 'summary' flag.
+- Combines all items into the "early" group.
+- Creates sections for all non-disabled package categories, even if they 
+have no charges on this invoice, as well as a section with no name.
+
+ESCAPE: an escape function to use for section titles.
+
+EXTRA_SECTIONS: an arrayref of additional sections to return after the 
+sorted list.  If there are any of these, section subtotals exclude 
+usage charges.
+
+FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
+passed through to C<_condense_section()>.
+
+=cut
+
+use vars qw(%pkg_category_cache);
+sub _items_sections {
+  my $self = shift;
+  my $late = shift;
+  my $summarypage = shift;
+  my $escape = shift;
+  my $extra_sections = shift;
+  my $format = shift;
+
+  my %subtotal = ();
+  my %late_subtotal = ();
+  my %not_tax = ();
+
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+  {
+
+      my $usage = $cust_bill_pkg->usage;
+
+      foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
+        next if ( $display->summary && $summarypage );
+
+        my $section = $display->section;
+        my $type    = $display->type;
+
+        $not_tax{$section} = 1
+          unless $cust_bill_pkg->pkgnum == 0;
+
+        if ( $display->post_total && !$summarypage ) {
+          if (! $type || $type eq 'S') {
+            $late_subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0
+              || $cust_bill_pkg->setup_show_zero;
+          }
+
+          if (! $type) {
+            $late_subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0
+              || $cust_bill_pkg->recur_show_zero;
+          }
+
+          if ($type && $type eq 'R') {
+            $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0
+              || $cust_bill_pkg->recur_show_zero;
+          }
+          
+          if ($type && $type eq 'U') {
+            $late_subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
+          }
+
+        } else {
+
+          next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
+          if (! $type || $type eq 'S') {
+            $subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0
+              || $cust_bill_pkg->setup_show_zero;
+          }
+
+          if (! $type) {
+            $subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0
+              || $cust_bill_pkg->recur_show_zero;
+          }
+
+          if ($type && $type eq 'R') {
+            $subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0
+              || $cust_bill_pkg->recur_show_zero;
+          }
+          
+          if ($type && $type eq 'U') {
+            $subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
+          }
+
+        }
+
+      }
+
+  }
+
+  %pkg_category_cache = ();
+
+  push @$late, map { { 'description' => &{$escape}($_),
+                       'subtotal'    => $late_subtotal{$_},
+                       'post_total'  => 1,
+                       'sort_weight' => ( _pkg_category($_)
+                                            ? _pkg_category($_)->weight
+                                            : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
+                   } }
+                 sort _sectionsort keys %late_subtotal;
+
+  my @sections;
+  if ( $summarypage ) {
+    @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
+                map { $_->categoryname } qsearch('pkg_category', {});
+    push @sections, '' if exists($subtotal{''});
+  } else {
+    @sections = keys %subtotal;
+  }
+
+  my @early = map { { 'description' => &{$escape}($_),
+                      'subtotal'    => $subtotal{$_},
+                      'summarized'  => $not_tax{$_} ? '' : 'Y',
+                      'tax_section' => $not_tax{$_} ? '' : 'Y',
+                      'sort_weight' => ( _pkg_category($_)
+                                           ? _pkg_category($_)->weight
+                                           : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
+                    }
+                  } @sections;
+  push @early, @$extra_sections if $extra_sections;
+
+  sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
+
+}
+
+#helper subs for above
+
+sub _sectionsort {
+  _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+}
+
+sub _pkg_category {
+  my $categoryname = shift;
+  $pkg_category_cache{$categoryname} ||=
+    qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
+}
+
+my %condensed_format = (
+  'label' => [ qw( Description Qty Amount ) ],
+  'fields' => [
+                sub { shift->{description} },
+                sub { shift->{quantity} },
+                sub { my($href, %opt) = @_;
+                      ($opt{dollar} || ''). $href->{amount};
+                    },
+              ],
+  'align'  => [ qw( l r r ) ],
+  'span'   => [ qw( 5 1 1 ) ],            # unitprices?
+  'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
+);
+
+sub _condense_section {
+  my ( $self, $format ) = ( shift, shift );
+  ( 'condensed' => 1,
+    map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
+      qw( description_generator
+          header_generator
+          total_generator
+          total_line_generator
+        )
+  );
+}
+
+sub _condensed_generator_defaults {
+  my ( $self, $format ) = ( shift, shift );
+  return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
+}
+
+my %html_align = (
+  'c' => 'center',
+  'l' => 'left',
+  'r' => 'right',
+);
+
+sub _condensed_header_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+
+  if ($format eq 'latex') {
+    $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
+    $suffix = "\\\\\n\\hline";
+    $separator = "&\n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+          };
+  } elsif ( $format eq 'html' ) {
+    $prefix = '<th></th>';
+    $suffix = '';
+    $separator = '';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<th align="$html_align{$a}">$d</th>!;
+      };
+  }
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
+    }
+
+    $prefix. join($separator, @result). $suffix;
+  };
+
+}
+
+sub _condensed_description_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+
+  my $money_char = '$';
+  if ($format eq 'latex') {
+    $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
+    $suffix = '\\\\';
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+          };
+    $money_char = '\\dollar';
+  }elsif ( $format eq 'html' ) {
+    $prefix = '"><td align="center"></td>';
+    $suffix = '';
+    $separator = '';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}">$d</td>!;
+      };
+    #$money_char = $conf->config('money_char') || '$';
+    $money_char = '';  # this is madness
+  }
+
+  sub {
+    #my @args = @_;
+    my $href = shift;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      my $dollar = '';
+      $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
+
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+sub _condensed_total_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+  my $style = '';
+
+  if ($format eq 'latex') {
+    $prefix = "& ";
+    $suffix = "\\\\\n";
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+          };
+  }elsif ( $format eq 'html' ) {
+    $prefix = '';
+    $suffix = '';
+    $separator = '';
+    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+      };
+  }
+
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    #  my $r = &{$f->{fields}->[$i]}(@args);
+    #  $r .= ' Total' unless $i;
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
+
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+=item total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class.  FORMAT is either html or latex
+
+=cut
+
+# should not be used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub _condensed_total_line_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+  my $style = '';
+
+  if ($format eq 'latex') {
+    $prefix = "& ";
+    $suffix = "\\\\\n";
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+          };
+  }elsif ( $format eq 'html' ) {
+    $prefix = '';
+    $suffix = '';
+    $separator = '';
+    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+      };
+  }
+
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}(@args),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
+
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+#  sub _items { # seems to be unused
+#    my $self = shift;
+#  
+#    #my @display = scalar(@_)
+#    #              ? @_
+#    #              : qw( _items_previous _items_pkg );
+#    #              #: qw( _items_pkg );
+#    #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+#    my @display = qw( _items_previous _items_pkg );
+#  
+#    my @b = ();
+#    foreach my $display ( @display ) {
+#      push @b, $self->$display(@_);
+#    }
+#    @b;
+#  }
+
+=item _items_pkg [ OPTIONS ]
+
+Return line item hashes for each package item on this invoice. Nearly 
+equivalent to 
+
+$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
+
+The only OPTIONS accepted is 'section', which may point to a hashref 
+with a key named 'condensed', which may have a true value.  If it 
+does, this method tries to merge identical items into items with 
+'quantity' equal to the number of items (not the sum of their 
+separate quantities, for some reason).
+
+=cut
+
+sub _items_pkg {
+  my $self = shift;
+  my %options = @_;
+
+  warn "$me _items_pkg searching for all package line items\n"
+    if $DEBUG > 1;
+
+  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+
+  warn "$me _items_pkg filtering line items\n"
+    if $DEBUG > 1;
+  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+  if ($options{section} && $options{section}->{condensed}) {
+
+    warn "$me _items_pkg condensing section\n"
+      if $DEBUG > 1;
+
+    my %itemshash = ();
+    local $Storable::canonical = 1;
+    foreach ( @items ) {
+      my $item = { %$_ };
+      delete $item->{ref};
+      delete $item->{ext_description};
+      my $key = freeze($item);
+      $itemshash{$key} ||= 0;
+      $itemshash{$key} ++; # += $item->{quantity};
+    }
+    @items = sort { $a->{description} cmp $b->{description} }
+             map { my $i = thaw($_);
+                   $i->{quantity} = $itemshash{$_};
+                   $i->{amount} =
+                     sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
+                   $i;
+                 }
+             keys %itemshash;
+  }
+
+  warn "$me _items_pkg returning ". scalar(@items). " items\n"
+    if $DEBUG > 1;
+
+  @items;
+}
+
+sub _taxsort {
+  return 0 unless $a->itemdesc cmp $b->itemdesc;
+  return -1 if $b->itemdesc eq 'Tax';
+  return 1 if $a->itemdesc eq 'Tax';
+  return -1 if $b->itemdesc eq 'Other surcharges';
+  return 1 if $a->itemdesc eq 'Other surcharges';
+  $a->itemdesc cmp $b->itemdesc;
+}
+
+sub _items_tax {
+  my $self = shift;
+  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
+
+Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
+list of hashrefs describing the line items they generate on the invoice.
+
+OPTIONS may include:
+
+format: the invoice format.
+
+escape_function: the function used to escape strings.
+
+DEPRECATED? (expensive, mostly unused?)
+format_function: the function used to format CDRs.
+
+section: a hashref containing 'description'; if this is present, 
+cust_bill_pkg_display records not belonging to this section are 
+ignored.
+
+multisection: a flag indicating that this is a multisection invoice,
+which does something complicated.
+
+multilocation: a flag to display the location label for the package.
+
+Returns a list of hashrefs, each of which may contain:
+
+pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
+ext_description, which is an arrayref of detail lines to show below 
+the package line.
+
+=cut
+
+sub _items_cust_bill_pkg {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $cust_bill_pkgs = shift;
+  my %opt = @_;
+
+  my $format = $opt{format} || '';
+  my $escape_function = $opt{escape_function} || sub { shift };
+  my $format_function = $opt{format_function} || '';
+  my $no_usage = $opt{no_usage} || '';
+  my $unsquelched = $opt{unsquelched} || ''; #unused
+  my $section = $opt{section}->{description} if $opt{section};
+  my $summary_page = $opt{summary_page} || ''; #unused
+  my $multilocation = $opt{multilocation} || '';
+  my $multisection = $opt{multisection} || '';
+  my $discount_show_always = 0;
+
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+  my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
+
+  my @b = ();
+  my ($s, $r, $u) = ( undef, undef, undef );
+  foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
+  {
+
+    foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+      if ( $_ && !$cust_bill_pkg->hidden ) {
+        $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+        $_->{amount}      =~ s/^\-0\.00$/0.00/;
+        $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+        push @b, { %$_ }
+          if $_->{amount} != 0
+          || $discount_show_always
+          || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+          || (   $_->{_is_setup} && $_->{setup_show_zero} )
+        ;
+        $_ = undef;
+      }
+    }
+
+    my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
+
+    warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
+         $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
+      if $DEBUG > 1;
+
+    foreach my $display ( grep { defined($section)
+                                 ? $_->section eq $section
+                                 : 1
+                               }
+                          #grep { !$_->summary || !$summary_page } # bunk!
+                          grep { !$_->summary || $multisection }
+                          @cust_bill_pkg_display
+                        )
+    {
+
+      warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
+           $display->billpkgdisplaynum. "\n"
+        if $DEBUG > 1;
+
+      my $type = $display->type;
+
+      my $desc = $cust_bill_pkg->desc;
+      $desc = substr($desc, 0, $maxlength). '...'
+        if $format eq 'latex' && length($desc) > $maxlength;
+
+      my %details_opt = ( 'format'          => $format,
+                          'escape_function' => $escape_function,
+                          'format_function' => $format_function,
+                          'no_usage'        => $opt{'no_usage'},
+                        );
+
+      if ( $cust_bill_pkg->pkgnum > 0 ) {
+
+        warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
+          if $DEBUG > 1;
+        my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+        # start/end dates for invoice formats that do nonstandard 
+        # things with them
+        my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
+
+        if (    (!$type || $type eq 'S')
+             && (    $cust_bill_pkg->setup != 0
+                  || $cust_bill_pkg->setup_show_zero
+                )
+           )
+         {
+
+          warn "$me _items_cust_bill_pkg adding setup\n"
+            if $DEBUG > 1;
+
+          my $description = $desc;
+          $description .= ' Setup'
+            if $cust_bill_pkg->recur != 0
+            || $discount_show_always
+            || $cust_bill_pkg->recur_show_zero;
+
+          my @d = ();
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->hidden )
+          {
+
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short($self->_date, undef, 'I')
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($loc, 0, $maxlength). '...'
+                if $format eq 'latex' && length($loc) > $maxlength;
+              push @d, &{$escape_function}($loc);
+            }
+
+          } #unless hiding service details
+
+          push @d, $cust_bill_pkg->details(%details_opt)
+            if $cust_bill_pkg->recur == 0;
+
+          if ( $cust_bill_pkg->hidden ) {
+            $s->{amount}      += $cust_bill_pkg->setup;
+            $s->{unit_amount} += $cust_bill_pkg->unitsetup;
+            push @{ $s->{ext_description} }, @d;
+          } else {
+            $s = {
+              _is_setup       => 1,
+              description     => $description,
+              #pkgpart         => $part_pkg->pkgpart,
+              pkgnum          => $cust_bill_pkg->pkgnum,
+              amount          => $cust_bill_pkg->setup,
+              setup_show_zero => $cust_bill_pkg->setup_show_zero,
+              unit_amount     => $cust_bill_pkg->unitsetup,
+              quantity        => $cust_bill_pkg->quantity,
+              ext_description => \@d,
+            };
+          };
+
+        }
+
+        if (    ( !$type || $type eq 'R' || $type eq 'U' )
+             && (
+                     $cust_bill_pkg->recur != 0
+                  || $cust_bill_pkg->setup == 0
+                  || $discount_show_always
+                  || $cust_bill_pkg->recur_show_zero
+                )
+           )
+        {
+
+          warn "$me _items_cust_bill_pkg adding recur/usage\n"
+            if $DEBUG > 1;
+
+          my $is_summary = $display->summary;
+          my $description = ($is_summary && $type && $type eq 'U')
+                            ? "Usage charges" : $desc;
+
+          #pry be a bit more efficient to look some of this conf stuff up
+          # outside the loop
+          unless (
+            $conf->exists('disable_line_item_date_ranges')
+              || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
+          ) {
+            my $time_period;
+            my $date_style = $conf->config( 'cust_bill-line_item-date_style',
+                                            $cust_main->agentnum
+                                          );
+            if ( defined($date_style) && $date_style eq 'month_of' ) {
+              $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
+            } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
+              my $desc = $conf->config( 'cust_bill-line_item-date_description',
+                                         $cust_main->agentnum
+                                      );
+              $desc .= ' ' unless $desc =~ /\s$/;
+              $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
+            } else {
+              $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
+                           " - ". time2str($date_format, $cust_bill_pkg->edate);
+            }
+            $description .= " ($time_period)";
+          }
+
+          my @d = ();
+          my @seconds = (); # for display of usage info
+
+          #at least until cust_bill_pkg has "past" ranges in addition to
+          #the "future" sdate/edate ones... see #3032
+          my @dates = ( $self->_date );
+          my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
+          push @dates, $prev->sdate if $prev;
+          push @dates, undef if !$prev;
+
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->itemdesc
+                || $cust_bill_pkg->hidden
+                || $is_summary && $type && $type eq 'U' )
+          {
+
+            warn "$me _items_cust_bill_pkg adding service details\n"
+              if $DEBUG > 1;
+
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short(@dates, 'I')
+                                                   #$cust_bill_pkg->edate,
+                                                   #$cust_bill_pkg->sdate)
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+            warn "$me _items_cust_bill_pkg done adding service details\n"
+              if $DEBUG > 1;
+
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($loc, 0, $maxlength). '...'
+                if $format eq 'latex' && length($loc) > $maxlength;
+              push @d, &{$escape_function}($loc);
+            }
+
+            # Display of seconds_since_sqlradacct:
+            # On the invoice, when processing @detail_items, look for a field
+            # named 'seconds'.  This will contain total seconds for each 
+            # service, in the same order as @ext_description.  For services 
+            # that don't support this it will show undef.
+            if ( $conf->exists('svc_acct-usage_seconds') 
+                 and ! $cust_bill_pkg->pkgpart_override ) {
+              foreach my $cust_svc ( 
+                  $cust_pkg->h_cust_svc(@dates, 'I') 
+                ) {
+
+                # eval because not having any part_export_usage exports 
+                # is a fatal error, last_bill/_date because that's how 
+                # sqlradius_hour billing does it
+                my $sec = eval {
+                  $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
+                };
+                push @seconds, $sec;
+              }
+            } #if svc_acct-usage_seconds
+
+          }
+
+          unless ( $is_summary ) {
+            warn "$me _items_cust_bill_pkg adding details\n"
+              if $DEBUG > 1;
+
+            #instead of omitting details entirely in this case (unwanted side
+            # effects), just omit CDRs
+            $details_opt{'no_usage'} = 1
+              if $type && $type eq 'R';
+
+            push @d, $cust_bill_pkg->details(%details_opt);
+          }
+
+          warn "$me _items_cust_bill_pkg calculating amount\n"
+            if $DEBUG > 1;
+  
+          my $amount = 0;
+          if (!$type) {
+            $amount = $cust_bill_pkg->recur;
+          } elsif ($type eq 'R') {
+            $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+          } elsif ($type eq 'U') {
+            $amount = $cust_bill_pkg->usage;
+          }
+  
+          if ( !$type || $type eq 'R' ) {
+
+            warn "$me _items_cust_bill_pkg adding recur\n"
+              if $DEBUG > 1;
+
+            if ( $cust_bill_pkg->hidden ) {
+              $r->{amount}      += $amount;
+              $r->{unit_amount} += $cust_bill_pkg->unitrecur;
+              push @{ $r->{ext_description} }, @d;
+            } else {
+              $r = {
+                description     => $description,
+                #pkgpart         => $part_pkg->pkgpart,
+                pkgnum          => $cust_bill_pkg->pkgnum,
+                amount          => $amount,
+                recur_show_zero => $cust_bill_pkg->recur_show_zero,
+                unit_amount     => $cust_bill_pkg->unitrecur,
+                quantity        => $cust_bill_pkg->quantity,
+                %item_dates,
+                ext_description => \@d,
+              };
+              $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
+            }
+
+          } else {  # $type eq 'U'
+
+            warn "$me _items_cust_bill_pkg adding usage\n"
+              if $DEBUG > 1;
+
+            if ( $cust_bill_pkg->hidden ) {
+              $u->{amount}      += $amount;
+              $u->{unit_amount} += $cust_bill_pkg->unitrecur;
+              push @{ $u->{ext_description} }, @d;
+            } else {
+              $u = {
+                description     => $description,
+                #pkgpart         => $part_pkg->pkgpart,
+                pkgnum          => $cust_bill_pkg->pkgnum,
+                amount          => $amount,
+                recur_show_zero => $cust_bill_pkg->recur_show_zero,
+                unit_amount     => $cust_bill_pkg->unitrecur,
+                quantity        => $cust_bill_pkg->quantity,
+                %item_dates,
+                ext_description => \@d,
+              };
+            }
+          }
+
+        } # recurring or usage with recurring charge
+
+      } else { #pkgnum tax or one-shot line item (??)
+
+        warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
+          if $DEBUG > 1;
+
+        if ( $cust_bill_pkg->setup != 0 ) {
+          push @b, {
+            'description' => $desc,
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
+          };
+        }
+        if ( $cust_bill_pkg->recur != 0 ) {
+          push @b, {
+            'description' => "$desc (".
+                             time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+                             time2str($date_format, $cust_bill_pkg->edate). ')',
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
+          };
+        }
+
+      }
+
+    }
+
+    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
+                                && $conf->exists('discount-show-always'));
+
+  }
+
+  foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+    if ( $_  ) {
+      $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+      $_->{amount}      =~ s/^\-0\.00$/0.00/;
+      $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+      push @b, { %$_ }
+        if $_->{amount} != 0
+        || $discount_show_always
+        || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
+        || (   $_->{_is_setup} && $_->{setup_show_zero} )
+    }
+  }
+
+  warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
+    if $DEBUG > 1;
+
+  @b;
+
+}
+
+=item _items_discounts_avail
+
+Returns an array of line item hashrefs representing available term discounts
+for this invoice.  This makes the same assumptions that apply to term 
+discounts in general: that the package is billed monthly, at a flat rate, 
+with no usage charges.  A prorated first month will be handled, as will 
+a setup fee if the discount is allowed to apply to setup fees.
+
+=cut
+
+sub _items_discounts_avail {
+  my $self = shift;
+
+  #maybe move this method from cust_bill when quotations support discount_plans 
+  return () unless $self->can('discount_plans');
+  my %plans = $self->discount_plans;
+
+  my $list_pkgnums = 0; # if any packages are not eligible for all discounts
+  $list_pkgnums = grep { $_->list_pkgnums } values %plans;
+
+  map {
+    my $months = $_;
+    my $plan = $plans{$months};
+
+    my $term_total = sprintf('%.2f', $plan->discounted_total);
+    my $percent = sprintf('%.0f', 
+                          100 * (1 - $term_total / $plan->base_total) );
+    my $permonth = sprintf('%.2f', $term_total / $months);
+    my $detail = $self->mt('discount on item'). ' '.
+                 join(', ', map { "#$_" } $plan->pkgnums)
+      if $list_pkgnums;
+
+    # discounts for non-integer months don't work anyway
+    $months = sprintf("%d", $months);
+
+    +{
+      description => $self->mt('Save [_1]% by paying for [_2] months',
+                                $percent, $months),
+      amount      => $self->mt('[_1] ([_2] per month)', 
+                                $term_total, $money_char.$permonth),
+      ext_description => ($detail || ''),
+    }
+  } #map
+  sort { $b <=> $a } keys %plans;
+
+}
+
+1;
index 8d4b346..417b202 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use vars qw( @ISA @EXPORT_OK $DEBUG );
 use Exporter;
 use Tie::IxHash;
+use File::Slurp;
 use FS::UID qw( dbh driver_name );
 use FS::Conf;
 use FS::Record qw(qsearchs qsearch str2time_sql);
@@ -63,7 +64,12 @@ sub upgrade_config {
 
   upgrade_overlimit_groups($conf);
   map { upgrade_overlimit_groups($conf,$_->agentnum) } qsearch('agent', {});
-  
+
+  my $DIST_CONF = '/usr/local/etc/freeside/default_conf/';#DIST_CONF in Makefile
+  $conf->set($_, scalar(read_file( "$DIST_CONF/$_" )) )
+    foreach grep { ! $conf->exists($_) && -s "$DIST_CONF/$_" }
+      qw( quotation_html quotation_latex quotation_latexnotes );
+
 }
 
 sub upgrade_overlimit_groups {
index 1e65ca3..e6266b4 100644 (file)
@@ -192,6 +192,7 @@ sub _upgrade_data { # class method
     'Cancel customer package immediately' => 'Un-cancel customer package',
     'Suspend customer package'            => 'Suspend customer',
     'Unsuspend customer package'          => 'Unsuspend customer',
+    'New prospect'                        => 'Generate quotation',
 
     'List services'    => [ 'Services: Accounts',
                             'Services: Domains',
index 498025f..83748be 100644 (file)
@@ -1,26 +1,20 @@
 package FS::cust_bill;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
 
 use strict;
-use vars qw( @ISA $DEBUG $me 
-             $money_char $date_format $rdate_format $date_format_long );
+use vars qw( $DEBUG $me $date_format );
              # but NOT $conf
-use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
-use List::Util qw(min max sum);
+use List::Util qw(min max);
 use Date::Format;
-use Date::Language;
-use Text::Template 1.20;
 use File::Temp 0.14;
-use String::ShellQuote;
 use HTML::Entities;
-use Locale::Country;
 use Storable qw( freeze thaw );
 use GD::Barcode;
 use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
+use FS::Misc qw( send_email send_fax do_print );
 use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main_Mixin;
 use FS::cust_main;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
@@ -46,18 +40,13 @@ use FS::cust_credit_bill_pkg;
 use FS::discount_plan;
 use FS::L10N;
 
-@ISA = qw( FS::cust_main_Mixin FS::Record );
-
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
 
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   my $conf = new FS::Conf; #global
-  $money_char       = $conf->config('money_char')       || '$';  
   $date_format      = $conf->config('date_format')      || '%x'; #/YY
-  $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
-  $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
 } );
 
 =head1 NAME
@@ -161,6 +150,7 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
+sub notice_name { 'Invoice'; }
 
 sub cust_linked { $_[0]->cust_main_custnum; } 
 sub cust_unlinked_msg {
@@ -2325,143 +2315,6 @@ sub _agent_invoice_from {
   $self->cust_main->agent_invoice_from;
 }
 
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Returns an text invoice, as a list of lines.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time>, if specified, is used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_text {
-  my $self = shift;
-  my( $today, $template, %opt );
-  if ( ref($_[0]) ) {
-    %opt = %{ shift() };
-    $today = delete($opt{'time'}) || '';
-    $template = delete($opt{template}) || '';
-  } else {
-    ( $today, $template, %opt ) = @_;
-  }
-
-  my %params = ( 'format' => 'template' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
-
-  $self->print_generic( %params );
-}
-
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Internal method - returns a filename of a filled-in LaTeX template for this
-invoice (Note: add ".tex" to get the actual filename), and a filename of
-an associated logo (with the .eps extension included).
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time>, if specified, is used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_latex {
-  my $self = shift;
-  my $conf = $self->conf;
-  my( $today, $template, %opt );
-  if ( ref($_[0]) ) {
-    %opt = %{ shift() };
-    $today = delete($opt{'time'}) || '';
-    $template = delete($opt{template}) || '';
-  } else {
-    ( $today, $template, %opt ) = @_;
-  }
-
-  my %params = ( 'format' => 'latex' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
-
-  $template ||= $self->_agent_template;
-
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
-                           DIR      => $dir,
-                           SUFFIX   => '.eps',
-                           UNLINK   => 0,
-                         ) or die "can't open temp file: $!\n";
-
-  my $agentnum = $self->cust_main->agentnum;
-
-  if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
-    print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
-      or die "can't write temp file: $!\n";
-  } else {
-    print $lh $conf->config_binary('logo.eps', $agentnum)
-      or die "can't write temp file: $!\n";
-  }
-  close $lh;
-  $params{'logo_file'} = $lh->filename;
-
-  if($conf->exists('invoice-barcode')){
-      my $png_file = $self->invoice_barcode($dir);
-      my $eps_file = $png_file;
-      $eps_file =~ s/\.png$/.eps/g;
-      $png_file =~ /(barcode.*png)/;
-      $png_file = $1;
-      $eps_file =~ /(barcode.*eps)/;
-      $eps_file = $1;
-
-      my $curr_dir = cwd();
-      chdir($dir); 
-      # after painfuly long experimentation, it was determined that sam2p won't
-      #        accept : and other chars in the path, no matter how hard I tried to
-      # escape them, hence the chdir (and chdir back, just to be safe)
-      system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
-       or die "sam2p failed: $!\n";
-      unlink($png_file);
-      chdir($curr_dir);
-
-      $params{'barcode_file'} = $eps_file;
-  }
-
-  my @filled_in = $self->print_generic( %params );
-  
-  my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
-                           DIR      => $dir,
-                           SUFFIX   => '.tex',
-                           UNLINK   => 0,
-                         ) or die "can't open temp file: $!\n";
-  binmode($fh, ':utf8'); # language support
-  print $fh join('', @filled_in );
-  close $fh;
-
-  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
-  return ($1, $params{'logo_file'}, $params{'barcode_file'});
-
-}
-
 =item invoice_barcode DIR_OR_FALSE
 
 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
@@ -2491,1951 +2344,164 @@ sub invoice_barcode {
     return $gd->png;
 }
 
-=item print_generic OPTION => VALUE ...
-
-Internal method - returns a filled-in template for this invoice as a scalar.
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Non optional options include 
-  format - latex, html, template
-
-Optional options include
-
-template - a value used as a suffix for a configuration template
-
-time - a value used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-cid - 
-
-unsquelch_cdr - overrides any per customer cdr squelching when true
-
-notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+=item invnum_date_pretty
 
-locale - override customer's locale
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)"
 
 =cut
 
-#what's with all the sprintf('%10.2f')'s in here?  will it cause any
-# (alignment in text invoice?) problems to change them all to '%.2f' ?
-# yes: fixed width/plain text printing will be borked
-sub print_generic {
-  my( $self, %params ) = @_;
-  my $conf = $self->conf;
-  my $today = $params{today} ? $params{today} : time;
-  warn "$me print_generic called on $self with suffix $params{template}\n"
-    if $DEBUG;
-
-  my $format = $params{format};
-  die "Unknown format: $format"
-    unless $format =~ /^(latex|html|template)$/;
-
-  my $cust_main = $self->cust_main;
-  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname
-        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
-
-  my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
-                     'html'     => [ '<%=', '%>' ],
-                     'template' => [ '{', '}' ],
-                   );
-
-  warn "$me print_generic creating template\n"
-    if $DEBUG > 1;
-
-  #create the template
-  my $template = $params{template} ? $params{template} : $self->_agent_template;
-  my $templatefile = "invoice_$format";
-  $templatefile .= "_$template"
-    if length($template) && $conf->exists($templatefile."_$template");
-  my @invoice_template = map "$_\n", $conf->config($templatefile)
-    or die "cannot load config data $templatefile";
-
-  my $old_latex = '';
-  if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
-    #change this to a die when the old code is removed
-    warn "old-style invoice template $templatefile; ".
-         "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
-    $old_latex = 'true';
-    @invoice_template = _translate_old_latex_format(@invoice_template);
-  } 
-
-  warn "$me print_generic creating T:T object\n"
-    if $DEBUG > 1;
-
-  my $text_template = new Text::Template(
-    TYPE => 'ARRAY',
-    SOURCE => \@invoice_template,
-    DELIMITERS => $delimiters{$format},
-  );
-
-  warn "$me print_generic compiling T:T object\n"
-    if $DEBUG > 1;
-
-  $text_template->compile()
-    or die "Can't compile $templatefile: $Text::Template::ERROR\n";
-
-
-  # additional substitution could possibly cause breakage in existing templates
-  my %convert_maps = ( 
-    'latex' => {
-                 'notes'         => sub { map "$_", @_ },
-                 'footer'        => sub { map "$_", @_ },
-                 'smallfooter'   => sub { map "$_", @_ },
-                 'returnaddress' => sub { map "$_", @_ },
-                 'coupon'        => sub { map "$_", @_ },
-                 'summary'       => sub { map "$_", @_ },
-               },
-    'html'  => {
-                 'notes' =>
-                   sub {
-                     map { 
-                       s/%%(.*)$/<!-- $1 -->/g;
-                       s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
-                       s/\\begin\{enumerate\}/<ol>/g;
-                       s/\\item /  <li>/g;
-                       s/\\end\{enumerate\}/<\/ol>/g;
-                       s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
-                       s/\\\\\*/<br>/g;
-                       s/\\dollar ?/\$/g;
-                       s/\\#/#/g;
-                       s/~/&nbsp;/g;
-                       $_;
-                     }  @_
-                   },
-                 'footer' =>
-                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
-                 'smallfooter' =>
-                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
-                 'returnaddress' =>
-                   sub {
-                     map { 
-                       s/~/&nbsp;/g;
-                       s/\\\\\*?\s*$/<BR>/;
-                       s/\\hyphenation\{[\w\s\-]+}//;
-                       s/\\([&])/$1/g;
-                       $_;
-                     }  @_
-                   },
-                 'coupon'        => sub { "" },
-                 'summary'       => sub { "" },
-               },
-    'template' => {
-                 'notes' =>
-                   sub {
-                     map { 
-                       s/%%.*$//g;
-                       s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
-                       s/\\begin\{enumerate\}//g;
-                       s/\\item /  * /g;
-                       s/\\end\{enumerate\}//g;
-                       s/\\textbf\{(.*)\}/$1/g;
-                       s/\\\\\*/ /;
-                       s/\\dollar ?/\$/g;
-                       $_;
-                     }  @_
-                   },
-                 'footer' =>
-                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
-                 'smallfooter' =>
-                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
-                 'returnaddress' =>
-                   sub {
-                     map { 
-                       s/~/ /g;
-                       s/\\\\\*?\s*$/\n/;             # dubious
-                       s/\\hyphenation\{[\w\s\-]+}//;
-                       $_;
-                     }  @_
-                   },
-                 'coupon'        => sub { "" },
-                 'summary'       => sub { "" },
-               },
-  );
-
-
-  # hashes for differing output formats
-  my %nbsps = ( 'latex'    => '~',
-                'html'     => '',    # '&nbps;' would be nice
-                'template' => '',    # not used
-              );
-  my $nbsp = $nbsps{$format};
-
-  my %escape_functions = ( 'latex'    => \&_latex_escape,
-                           'html'     => \&_html_escape_nbsp,#\&encode_entities,
-                           'template' => sub { shift },
-                         );
-  my $escape_function = $escape_functions{$format};
-  my $escape_function_nonbsp = ($format eq 'html')
-                                 ? \&_html_escape : $escape_function;
-
-  my %date_formats = ( 'latex'    => $date_format_long,
-                       'html'     => $date_format_long,
-                       'template' => '%s',
-                     );
-  $date_formats{'html'} =~ s/ /&nbsp;/g;
-
-  my $date_format = $date_formats{$format};
-
-  my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
-                                               },
-                             'html'     => sub { return '<b>'. shift(). '</b>'
-                                               },
-                             'template' => sub { shift },
-                           );
-  my $embolden_function = $embolden_functions{$format};
-
-  my %newline_tokens = (  'latex'     => '\\\\',
-                          'html'      => '<br>',
-                          'template'  => "\n",
-                        );
-  my $newline_token = $newline_tokens{$format};
-
-  warn "$me generating template variables\n"
-    if $DEBUG > 1;
-
-  # generate template variables
-  my $returnaddress;
-  if (
-         defined( $conf->config_orbase( "invoice_${format}returnaddress",
-                                        $template
-                                      )
-                )
-       && length( $conf->config_orbase( "invoice_${format}returnaddress",
-                                        $template
-                                      )
-                )
-  ) {
-
-    $returnaddress = join("\n",
-      $conf->config_orbase("invoice_${format}returnaddress", $template)
-    );
-
-  } elsif ( grep /\S/,
-            $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
-
-    my $convert_map = $convert_maps{$format}{'returnaddress'};
-    $returnaddress =
-      join( "\n",
-            &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
-                                                 $template
-                                               )
-                         )
-          );
-  } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
-
-    my $convert_map = $convert_maps{$format}{'returnaddress'};
-    $returnaddress = join( "\n", &$convert_map(
-                                   map { s/( {2,})/'~' x length($1)/eg;
-                                         s/$/\\\\\*/;
-                                         $_
-                                       }
-                                     ( $conf->config('company_name', $self->cust_main->agentnum),
-                                       $conf->config('company_address', $self->cust_main->agentnum),
-                                     )
-                                 )
-                     );
+sub invnum_date_pretty {
+  my $self = shift;
+  $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
+}
 
-  } else {
+#sub _items_extra_usage_sections {
+#  my $self = shift;
+#  my $escape = shift;
+#
+#  my %sections = ();
+#
+#  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
+#  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+#  {
+#    next unless $cust_bill_pkg->pkgnum > 0;
+#
+#    foreach my $section ( keys %usage_class ) {
+#
+#      my $usage = $cust_bill_pkg->usage($section);
+#
+#      next unless $usage && $usage > 0;
+#
+#      $sections{$section} ||= 0;
+#      $sections{$section} += $usage;
+#
+#    }
+#
+#  }
+#
+#  map { { 'description' => &{$escape}($_),
+#          'subtotal'    => $sections{$_},
+#          'summarized'  => '',
+#          'tax_section' => '',
+#        }
+#      }
+#    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
 
-    my $warning = "Couldn't find a return address; ".
-                  "do you need to set the company_address configuration value?";
-    warn "$warning\n";
-    $returnaddress = $nbsp;
-    #$returnaddress = $warning;
+sub _items_extra_usage_sections {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $escape = shift;
+  my $format = shift;
 
-  }
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
 
-  warn "$me generating invoice data\n"
-    if $DEBUG > 1;
-
-  my $agentnum = $self->cust_main->agentnum;
-
-  my %invoice_data = (
-
-    #invoice from info
-    'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
-    'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
-    'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
-    'returnaddress'   => $returnaddress,
-    'agent'           => &$escape_function($cust_main->agent->agent),
-
-    #invoice info
-    'invnum'          => $self->invnum,
-    'date'            => time2str($date_format, $self->_date),
-    'today'           => time2str($date_format_long, $today),
-    'terms'           => $self->terms,
-    'template'        => $template, #params{'template'},
-    'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
-    'current_charges' => sprintf("%.2f", $self->charged),
-    'duedate'         => $self->due_date2str($rdate_format), #date_format?
-
-    #customer info
-    'custnum'         => $cust_main->display_custnum,
-    'agent_custid'    => &$escape_function($cust_main->agent_custid),
-    ( map { $_ => &$escape_function($cust_main->$_()) } qw(
-      payname company address1 address2 city state zip fax
-    )),
-
-    #global config
-    'ship_enable'     => $conf->exists('invoice-ship_address'),
-    'unitprices'      => $conf->exists('invoice-unitprice'),
-    'smallernotes'    => $conf->exists('invoice-smallernotes'),
-    'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
-    'balance_due_below_line' => $conf->exists('balance_due_below_line'),
-   
-    #layout info -- would be fancy to calc some of this and bury the template
-    #               here in the code
-    'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
-    'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
-    'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
-    'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
-    'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
-    'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
-    'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
-    'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
-    'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
-    'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
-
-    # better hang on to conf_dir for a while (for old templates)
-    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
-
-    #these are only used when doing paged plaintext
-    'page'            => 1,
-    'total_pages'     => 1,
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
 
-  );
-  #localization
-  my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
-  $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
-  my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
-  # eval to avoid death for unimplemented languages
-  my $dh = eval { Date::Language->new($info{'name'}) } ||
-           Date::Language->new(); # fall back to English
-  # prototype here to silence warnings
-  $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
-  # eventually use this date handle everywhere in here, too
-
-  my $min_sdate = 999999999999;
-  my $max_edate = 0;
+  my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
     next unless $cust_bill_pkg->pkgnum > 0;
-    $min_sdate = $cust_bill_pkg->sdate
-      if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
-    $max_edate = $cust_bill_pkg->edate
-      if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
-  }
-
-  $invoice_data{'bill_period'} = '';
-  $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
-    . " to " . time2str('%e %h', $max_edate)
-    if ($max_edate != 0 && $min_sdate != 999999999999);
-
-  $invoice_data{finance_section} = '';
-  if ( $conf->config('finance_pkgclass') ) {
-    my $pkg_class =
-      qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
-    $invoice_data{finance_section} = $pkg_class->categoryname;
-  } 
-  $invoice_data{finance_amount} = '0.00';
-  $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
-
-  my $countrydefault = $conf->config('countrydefault') || 'US';
-  foreach ( qw( address1 address2 city state zip country fax) ){
-    my $method = 'ship_'.$_;
-    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
-  }
-  foreach ( qw( contact company ) ) { #compatibility
-    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
-  }
-  $invoice_data{'ship_country'} = ''
-    if ( $invoice_data{'ship_country'} eq $countrydefault );
-  
-  $invoice_data{'cid'} = $params{'cid'}
-    if $params{'cid'};
-
-  if ( $cust_main->country eq $countrydefault ) {
-    $invoice_data{'country'} = '';
-  } else {
-    $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
-  }
-
-  my @address = ();
-  $invoice_data{'address'} = \@address;
-  push @address,
-    $cust_main->payname.
-      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
-        ? " (P.O. #". $cust_main->payinfo. ")"
-        : ''
-      )
-  ;
-  push @address, $cust_main->company
-    if $cust_main->company;
-  push @address, $cust_main->address1;
-  push @address, $cust_main->address2
-    if $cust_main->address2;
-  push @address,
-    $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
-  push @address, $invoice_data{'country'}
-    if $invoice_data{'country'};
-  push @address, ''
-    while (scalar(@address) < 5);
-
-  $invoice_data{'logo_file'} = $params{'logo_file'}
-    if $params{'logo_file'};
-  $invoice_data{'barcode_file'} = $params{'barcode_file'}
-    if $params{'barcode_file'};
-  $invoice_data{'barcode_img'} = $params{'barcode_img'}
-    if $params{'barcode_img'};
-  $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
-    if $params{'barcode_cid'};
-
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
-  #my $balance_due = $self->owed + $pr_total - $cr_total;
-  my $balance_due = $self->owed + $pr_total;
-
-  # the customer's current balance as shown on the invoice before this one
-  $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
-
-  # the change in balance from that invoice to this one
-  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
-
-  # the sum of amount owed on all previous invoices
-  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
-
-  # the sum of amount owed on all invoices
-  $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
-
-  # info from customer's last invoice before this one, for some 
-  # summary formats
-  $invoice_data{'last_bill'} = {};
-  my $last_bill = $pr_cust_bill[-1];
-  if ( $last_bill ) {
-    $invoice_data{'last_bill'} = {
-      '_date'     => $last_bill->_date, #unformatted
-      # all we need for now
-    };
-  }
-
-  my $summarypage = '';
-  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
-    $summarypage = 1;
-  }
-  $invoice_data{'summarypage'} = $summarypage;
 
-  warn "$me substituting variables in notes, footer, smallfooter\n"
-    if $DEBUG > 1;
-
-  my @include = (qw( notes footer smallfooter ));
-  push @include, 'coupon' unless $params{'no_coupon'};
-  foreach my $include (@include) {
-
-    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
-    my @inc_src;
-
-    if ( $conf->exists($inc_file, $agentnum)
-         && length( $conf->config($inc_file, $agentnum) ) ) {
-
-      @inc_src = $conf->config($inc_file, $agentnum);
-
-    } else {
-
-      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
-      my $convert_map = $convert_maps{$format}{$include};
-
-      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
-                       s/--\@\]/$delimiters{$format}[1]/g;
-                       $_;
-                     } 
-                 &$convert_map( $conf->config($inc_file, $agentnum) );
-
-    }
+    foreach my $classnum ( keys %usage_class ) {
+      my $section = $usage_class{$classnum}->classname;
+      $classnums{$section} = $classnum;
 
-    my $inc_tt = new Text::Template (
-      TYPE       => 'ARRAY',
-      SOURCE     => [ map "$_\n", @inc_src ],
-      DELIMITERS => $delimiters{$format},
-    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+      foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+        my $amount = $detail->amount;
+        next unless $amount && $amount > 0;
+        $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+        $sections{$section}{amount} += $amount;  #subtotal
+        $sections{$section}{calls}++;
+        $sections{$section}{duration} += $detail->duration;
 
-    unless ( $inc_tt->compile() ) {
-      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
-      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
-      die $error;
-    }
+        my $desc = $detail->regionname; 
+        my $description = $desc;
+        $description = substr($desc, 0, $maxlength). '...'
+          if $format eq 'latex' && length($desc) > $maxlength;
 
-    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+        $lines{$section}{$desc} ||= {
+          description     => &{$escape}($description),
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_bill_pkg->pkgnum,
+          ref             => '',
+          amount          => 0,
+          calls           => 0,
+          duration        => 0,
+          #unit_amount     => $cust_bill_pkg->unitrecur,
+          quantity        => $cust_bill_pkg->quantity,
+          product_code    => 'N/A',
+          ext_description => [],
+        };
 
-    $invoice_data{$include} =~ s/\n+$//
-      if ($format eq 'latex');
-  }
+        $lines{$section}{$desc}{amount} += $amount;
+        $lines{$section}{$desc}{calls}++;
+        $lines{$section}{$desc}{duration} += $detail->duration;
 
-  # let invoices use either of these as needed
-  $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
-    ? $cust_main->payinfo : '';
-  $invoice_data{'po_line'} = 
-    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
-      ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
-      : $nbsp;
-
-  my %money_chars = ( 'latex'    => '',
-                      'html'     => $conf->config('money_char') || '$',
-                      'template' => '',
-                    );
-  my $money_char = $money_chars{$format};
-
-  my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
-                            'html'     => $conf->config('money_char') || '$',
-                            'template' => '',
-                          );
-  my $other_money_char = $other_money_chars{$format};
-  $invoice_data{'dollar'} = $other_money_char;
-
-  my @detail_items = ();
-  my @total_items = ();
-  my @buf = ();
-  my @sections = ();
-
-  $invoice_data{'detail_items'} = \@detail_items;
-  $invoice_data{'total_items'} = \@total_items;
-  $invoice_data{'buf'} = \@buf;
-  $invoice_data{'sections'} = \@sections;
-
-  warn "$me generating sections\n"
-    if $DEBUG > 1;
-
-  my $previous_section = { 'description' => $self->mt('Previous Charges'),
-                           'subtotal'    => $other_money_char.
-                                            sprintf('%.2f', $pr_total),
-                           'summarized'  => '', #why? $summarypage ? 'Y' : '',
-                         };
-  $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
-    join(' / ', map { $cust_main->balance_date_range(@$_) }
-                $self->_prior_month30s
-        )
-    if $conf->exists('invoice_include_aging');
-
-  my $taxtotal = 0;
-  my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
-                      'subtotal'    => $taxtotal,   # adjusted below
-                    };
-  my $tax_weight = _pkg_category($tax_section->{description})
-                        ? _pkg_category($tax_section->{description})->weight
-                        : 0;
-  $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
-  $tax_section->{'sort_weight'} = $tax_weight;
-
-
-  my $adjusttotal = 0;
-  my $adjust_section = { 'description' => 
-    $self->mt('Credits, Payments, and Adjustments'),
-                         'subtotal'    => 0,   # adjusted below
-                       };
-  my $adjust_weight = _pkg_category($adjust_section->{description})
-                        ? _pkg_category($adjust_section->{description})->weight
-                        : 0;
-  $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
-  $adjust_section->{'sort_weight'} = $adjust_weight;
-
-  my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
-  my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
-  $invoice_data{'multisection'} = $multisection;
-  my $late_sections = [];
-  my $extra_sections = [];
-  my $extra_lines = ();
-
-  my $default_section = { 'description' => '',
-                          'subtotal'    => '', 
-                          'no_subtotal' => 1,
-                        };
-
-  if ( $multisection ) {
-    ($extra_sections, $extra_lines) =
-      $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
-      if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
-
-    push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
-
-    push @detail_items, @$extra_lines if $extra_lines;
-    push @sections,
-      $self->_items_sections( $late_sections,      # this could stand a refactor
-                              $summarypage,
-                              $escape_function_nonbsp,
-                              $extra_sections,
-                              $format,             #bah
-                            );
-    if ($conf->exists('svc_phone_sections')) {
-      my ($phone_sections, $phone_lines) =
-        $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
-      push @{$late_sections}, @$phone_sections;
-      push @detail_items, @$phone_lines;
-    }
-    if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
-      my ($accountcode_section, $accountcode_lines) =
-        $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
-      if ( scalar(@$accountcode_lines) ) {
-          push @{$late_sections}, $accountcode_section;
-          push @detail_items, @$accountcode_lines;
-      }
-    }
-  } else {# not multisection
-    # make a default section
-    push @sections, $default_section;
-    # and calculate the finance charge total, since it won't get done otherwise.
-    # XXX possibly other totals?
-    # XXX possibly finance_pkgclass should not be used in this manner?
-    if ( $conf->exists('finance_pkgclass') ) {
-      my @finance_charges;
-      foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-        if ( grep { $_->section eq $invoice_data{finance_section} }
-             $cust_bill_pkg->cust_bill_pkg_display ) {
-          # I think these are always setup fees, but just to be sure...
-          push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
-        }
       }
-      $invoice_data{finance_amount} = 
-        sprintf('%.2f', sum( @finance_charges ) || 0);
     }
   }
 
-  unless (    $conf->exists('disable_previous_balance', $agentnum)
-           || $conf->exists('previous_balance-summary_only')
-         )
-  {
-
-    warn "$me adding previous balances\n"
-      if $DEBUG > 1;
+  my %sectionmap = ();
+  foreach (keys %sections) {
+    my $usage_class = $usage_class{$classnums{$_}};
+    $sectionmap{$_} = { 'description' => &{$escape}($_),
+                        'amount'    => $sections{$_}{amount},    #subtotal
+                        'calls'       => $sections{$_}{calls},
+                        'duration'    => $sections{$_}{duration},
+                        'summarized'  => '',
+                        'tax_section' => '',
+                        'sort_weight' => $usage_class->weight,
+                        ( $usage_class->format
+                          ? ( map { $_ => $usage_class->$_($format) }
+                              qw( description_generator header_generator total_generator total_line_generator )
+                            )
+                          : ()
+                        ), 
+                      };
+  }
 
-    foreach my $line_item ( $self->_items_previous ) {
+  my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+                 values %sectionmap;
 
-      my $detail = {
-        ext_description => [],
-      };
-      $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = 1;
-      $detail->{'section'} = $multisection ? $previous_section
-                                           : $default_section;
-      $detail->{'description'} = &$escape_function($line_item->{'description'});
-      if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = map {
-          &$escape_function($_);
-        } @{$line_item->{'ext_description'}};
-      }
-      $detail->{'amount'} = ( $old_latex ? '' : $money_char).
-                            $line_item->{'amount'};
-      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
-      push @detail_items, $detail;
-      push @buf, [ $detail->{'description'},
-                   $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-                 ];
+  my @lines = ();
+  foreach my $section ( keys %lines ) {
+    foreach my $line ( keys %{$lines{$section}} ) {
+      my $l = $lines{$section}{$line};
+      $l->{section}     = $sectionmap{$section};
+      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
+      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+      push @lines, $l;
     }
-
-  }
-  
-  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) 
-    {
-    push @buf, ['','-----------'];
-    push @buf, [ $self->mt('Total Previous Balance'),
-                 $money_char. sprintf("%10.2f", $pr_total) ];
-    push @buf, ['',''];
   }
-  if ( $conf->exists('svc_phone-did-summary') ) {
-      warn "$me adding DID summary\n"
-        if $DEBUG > 1;
-
-      my ($didsummary,$minutes) = $self->_did_summary;
-      my $didsummary_desc = 'DID Activity Summary (since last invoice)';
-      push @detail_items, 
-       { 'description' => $didsummary_desc,
-           'ext_description' => [ $didsummary, $minutes ],
-       };
-  }
-
-  foreach my $section (@sections, @$late_sections) {
-
-    warn "$me adding section \n". Dumper($section)
-      if $DEBUG > 1;
-
-    # begin some normalization
-    $section->{'subtotal'} = $section->{'amount'}
-      if $multisection
-         && !exists($section->{subtotal})
-         && exists($section->{amount});
-
-    $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
-      if ( $invoice_data{finance_section} &&
-           $section->{'description'} eq $invoice_data{finance_section} );
-
-    $section->{'subtotal'} = $other_money_char.
-                             sprintf('%.2f', $section->{'subtotal'})
-      if $multisection;
-
-    # continue some normalization
-    $section->{'amount'}   = $section->{'subtotal'}
-      if $multisection;
 
+  return(\@sections, \@lines);
 
-    if ( $section->{'description'} ) {
-      push @buf, ( [ &$escape_function($section->{'description'}), '' ],
-                   [ '', '' ],
-                 );
-    }
+}
 
-    warn "$me   setting options\n"
-      if $DEBUG > 1;
+sub _did_summary {
+    my $self = shift;
+    my $end = $self->_date;
 
-    my $multilocation = scalar($cust_main->cust_location); #too expensive?
-    my %options = ();
-    $options{'section'} = $section if $multisection;
-    $options{'format'} = $format;
-    $options{'escape_function'} = $escape_function;
-    $options{'no_usage'} = 1 unless $unsquelched;
-    $options{'unsquelched'} = $unsquelched;
-    $options{'summary_page'} = $summarypage;
-    $options{'skip_usage'} =
-      scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
-    $options{'multilocation'} = $multilocation;
-    $options{'multisection'} = $multisection;
+    # start at date of previous invoice + 1 second or 0 if no previous invoice
+    my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
+    $start = 0 if !$start;
+    $start++;
 
-    warn "$me   searching for line items\n"
-      if $DEBUG > 1;
-
-    foreach my $line_item ( $self->_items_pkg(%options) ) {
-
-      warn "$me     adding line item $line_item\n"
-        if $DEBUG > 1;
-
-      my $detail = {
-        ext_description => [],
-      };
-      $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = $line_item->{'quantity'};
-      $detail->{'section'} = $section;
-      $detail->{'description'} = &$escape_function($line_item->{'description'});
-      if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
-      }
-      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
-                              $line_item->{'amount'};
-      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
-                                 $line_item->{'unit_amount'};
-      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
-      $detail->{'sdate'} = $line_item->{'sdate'};
-      $detail->{'edate'} = $line_item->{'edate'};
-      $detail->{'seconds'} = $line_item->{'seconds'};
-  
-      push @detail_items, $detail;
-      push @buf, ( [ $detail->{'description'},
-                     $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-                   ],
-                   map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
-                 );
-    }
-
-    if ( $section->{'description'} ) {
-      push @buf, ( ['','-----------'],
-                   [ $section->{'description'}. ' sub-total',
-                      $section->{'subtotal'} # already formatted this 
-                   ],
-                   [ '', '' ],
-                   [ '', '' ],
-                 );
-    }
-  
-  }
-
-  $invoice_data{current_less_finance} =
-    sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
-
-  if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
-    || $conf->exists('previous_balance-summary_only') )
-  {
-    unshift @sections, $previous_section if $pr_total;
-  }
-
-  warn "$me adding taxes\n"
-    if $DEBUG > 1;
-
-  foreach my $tax ( $self->_items_tax ) {
-
-    $taxtotal += $tax->{'amount'};
-
-    my $description = &$escape_function( $tax->{'description'} );
-    my $amount      = sprintf( '%.2f', $tax->{'amount'} );
-
-    if ( $multisection ) {
-
-      my $money = $old_latex ? '' : $money_char;
-      push @detail_items, {
-        ext_description => [],
-        ref          => '',
-        quantity     => '',
-        description  => $description,
-        amount       => $money. $amount,
-        product_code => '',
-        section      => $tax_section,
-      };
-
-    } else {
-
-      push @total_items, {
-        'total_item'   => $description,
-        'total_amount' => $other_money_char. $amount,
-      };
-
-    }
-
-    push @buf,[ $description,
-                $money_char. $amount,
-              ];
-
-  }
-  
-  if ( $taxtotal ) {
-    my $total = {};
-    $total->{'total_item'} = $self->mt('Sub-total');
-    $total->{'total_amount'} =
-      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-
-    if ( $multisection ) {
-      $tax_section->{'subtotal'} = $other_money_char.
-                                   sprintf('%.2f', $taxtotal);
-      $tax_section->{'pretotal'} = 'New charges sub-total '.
-                                   $total->{'total_amount'};
-      push @sections, $tax_section if $taxtotal;
-    }else{
-      unshift @total_items, $total;
-    }
-  }
-  $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
-
-  push @buf,['','-----------'];
-  push @buf,[$self->mt( 
-              $conf->exists('disable_previous_balance', $agentnum) 
-               ? 'Total Charges'
-               : 'Total New Charges'
-             ),
-             $money_char. sprintf("%10.2f",$self->charged) ];
-  push @buf,['',''];
-
-  {
-    my $total = {};
-    my $item = 'Total';
-    $item = $conf->config('previous_balance-exclude_from_total')
-         || 'Total New Charges'
-      if $conf->exists('previous_balance-exclude_from_total');
-    my $amount = $self->charged +
-                   ( $conf->exists('disable_previous_balance', $agentnum) ||
-                     $conf->exists('previous_balance-exclude_from_total')
-                     ? 0
-                     : $pr_total
-                   );
-    $total->{'total_item'} = &$embolden_function($self->mt($item));
-    $total->{'total_amount'} =
-      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
-    if ( $multisection ) {
-      if ( $adjust_section->{'sort_weight'} ) {
-        $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
-          $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
-      } else {
-        $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
-          $other_money_char.  sprintf('%.2f', $self->charged );
-      } 
-    }else{
-      push @total_items, $total;
-    }
-    push @buf,['','-----------'];
-    push @buf,[$item,
-               $money_char.
-               sprintf( '%10.2f', $amount )
-              ];
-    push @buf,['',''];
-  }
-  
-  unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
-    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-  
-    # credits
-    my $credittotal = 0;
-    foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
-
-      my $total;
-      $total->{'total_item'} = &$escape_function($credit->{'description'});
-      $credittotal += $credit->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
-      $adjusttotal += $credit->{'amount'};
-      if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($credit->{'description'}),
-          amount       => $money. $credit->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      } else {
-        push @total_items, $total;
-      }
-
-    }
-    $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
-    #credits (again)
-    foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
-      push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
-    }
-
-    # payments
-    my $paymenttotal = 0;
-    foreach my $payment ( $self->_items_payments ) {
-      my $total = {};
-      $total->{'total_item'} = &$escape_function($payment->{'description'});
-      $paymenttotal += $payment->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
-      $adjusttotal += $payment->{'amount'};
-      if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($payment->{'description'}),
-          amount       => $money. $payment->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      }else{
-        push @total_items, $total;
-      }
-      push @buf, [ $payment->{'description'},
-                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
-                 ];
-    }
-    $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-  
-    if ( $multisection ) {
-      $adjust_section->{'subtotal'} = $other_money_char.
-                                      sprintf('%.2f', $adjusttotal);
-      push @sections, $adjust_section
-        unless $adjust_section->{sort_weight};
-    }
-
-    # create Balance Due message
-    { 
-      my $total;
-      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-      $total->{'total_amount'} =
-        &$embolden_function(
-          $other_money_char. sprintf('%.2f', $summarypage 
-                                               ? $self->charged +
-                                                 $self->billing_balance
-                                               : $self->owed + $pr_total
-                                    )
-        );
-      if ( $multisection && !$adjust_section->{sort_weight} ) {
-        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
-                                         $total->{'total_amount'};
-      }else{
-        push @total_items, $total;
-      }
-      push @buf,['','-----------'];
-      push @buf,[$self->balance_due_msg, $money_char. 
-        sprintf("%10.2f", $balance_due ) ];
-    }
-
-    if ( $conf->exists('previous_balance-show_credit')
-        and $cust_main->balance < 0 ) {
-      my $credit_total = {
-        'total_item'    => &$embolden_function($self->credit_balance_msg),
-        'total_amount'  => &$embolden_function(
-          $other_money_char. sprintf('%.2f', -$cust_main->balance)
-        ),
-      };
-      if ( $multisection ) {
-        $adjust_section->{'posttotal'} .= $newline_token .
-          $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
-      }
-      else {
-        push @total_items, $credit_total;
-      }
-      push @buf,['','-----------'];
-      push @buf,[$self->credit_balance_msg, $money_char. 
-        sprintf("%10.2f", -$cust_main->balance ) ];
-    }
-  }
-
-  if ( $multisection ) {
-    if ($conf->exists('svc_phone_sections')) {
-      my $total;
-      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-      $total->{'total_amount'} =
-        &$embolden_function(
-          $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
-        );
-      my $last_section = pop @sections;
-      $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
-                                     $total->{'total_amount'};
-      push @sections, $last_section;
-    }
-    push @sections, @$late_sections
-      if $unsquelched;
-  }
-
-  # make a discounts-available section, even without multisection
-  if ( $conf->exists('discount-show_available') 
-       and my @discounts_avail = $self->_items_discounts_avail ) {
-    my $discount_section = {
-      'description' => $self->mt('Discounts Available'),
-      'subtotal'    => '',
-      'no_subtotal' => 1,
-    };
-
-    push @sections, $discount_section;
-    push @detail_items, map { +{
-        'ref'         => '', #should this be something else?
-        'section'     => $discount_section,
-        'description' => &$escape_function( $_->{description} ),
-        'amount'      => $money_char . &$escape_function( $_->{amount} ),
-        'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
-    } } @discounts_avail;
-  }
-
-  # All sections and items are built; now fill in templates.
-  my @includelist = ();
-  push @includelist, 'summary' if $summarypage;
-  foreach my $include ( @includelist ) {
-
-    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
-    my @inc_src;
-
-    if ( length( $conf->config($inc_file, $agentnum) ) ) {
-
-      @inc_src = $conf->config($inc_file, $agentnum);
-
-    } else {
-
-      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
-
-      my $convert_map = $convert_maps{$format}{$include};
-
-      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
-                       s/--\@\]/$delimiters{$format}[1]/g;
-                       $_;
-                     } 
-                 &$convert_map( $conf->config($inc_file, $agentnum) );
-
-    }
-
-    my $inc_tt = new Text::Template (
-      TYPE       => 'ARRAY',
-      SOURCE     => [ map "$_\n", @inc_src ],
-      DELIMITERS => $delimiters{$format},
-    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
-
-    unless ( $inc_tt->compile() ) {
-      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
-      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
-      die $error;
-    }
-
-    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
-
-    $invoice_data{$include} =~ s/\n+$//
-      if ($format eq 'latex');
-  }
-
-  $invoice_lines = 0;
-  my $wasfunc = 0;
-  foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
-    /invoice_lines\((\d*)\)/;
-    $invoice_lines += $1 || scalar(@buf);
-    $wasfunc=1;
-  }
-  die "no invoice_lines() functions in template?"
-    if ( $format eq 'template' && !$wasfunc );
-
-  if ($format eq 'template') {
-
-    if ( $invoice_lines ) {
-      $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
-      $invoice_data{'total_pages'}++
-        if scalar(@buf) % $invoice_lines;
-    }
-
-    #setup subroutine for the template
-    $invoice_data{invoice_lines} = sub {
-      my $lines = shift || scalar(@buf);
-      map { 
-        scalar(@buf)
-          ? shift @buf
-          : [ '', '' ];
-      }
-      ( 1 .. $lines );
-    };
-
-    my $lines;
-    my @collect;
-    while (@buf) {
-      push @collect, split("\n",
-        $text_template->fill_in( HASH => \%invoice_data )
-      );
-      $invoice_data{'page'}++;
-    }
-    map "$_\n", @collect;
-  }else{
-    # this is where we actually create the invoice
-    warn "filling in template for invoice ". $self->invnum. "\n"
-      if $DEBUG;
-    warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
-      if $DEBUG > 1;
-
-    $text_template->fill_in(HASH => \%invoice_data);
-  }
-}
-
-# helper routine for generating date ranges
-sub _prior_month30s {
-  my $self = shift;
-  my @ranges = (
-   [ 1,       2592000 ], # 0-30 days ago
-   [ 2592000, 5184000 ], # 30-60 days ago
-   [ 5184000, 7776000 ], # 60-90 days ago
-   [ 7776000, 0       ], # 90+   days ago
-  );
-
-  map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
-          $_->[1] ? $self->_date - $_->[1] - 1 : '',
-      ] }
-  @ranges;
-}
-
-=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an postscript invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time> an optional value used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_ps {
-  my $self = shift;
-
-  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
-  my $ps = generate_ps($file);
-  unlink($logofile);
-  unlink($barcodefile) if $barcodefile;
-
-  $ps;
-}
-
-=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an PDF invoice, as a scalar.
-
-Options can be passed as a hashref (recommended) or as a list of time, template
-and then any key/value pairs for any other options.
-
-I<time> an optional value used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-=cut
-
-sub print_pdf {
-  my $self = shift;
-
-  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
-  my $pdf = generate_pdf($file);
-  unlink($logofile);
-  unlink($barcodefile) if $barcodefile;
-
-  $pdf;
-}
-
-=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
-
-Returns an HTML invoice, as a scalar.
-
-I<time> an optional value used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
-
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
-
-I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
-when emailing the invoice as part of a multipart/related MIME email.
-
-=cut
-
-sub print_html {
-  my $self = shift;
-  my %params;
-  if ( ref($_[0]) ) {
-    %params = %{ shift() }; 
-  }else{
-    $params{'time'} = shift;
-    $params{'template'} = shift;
-    $params{'cid'} = shift;
-  }
-
-  $params{'format'} = 'html';
-  
-  $self->print_generic( %params );
-}
-
-# quick subroutine for print_latex
-#
-# There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves: 
-#      # $ % & ~ _ ^ \ { }
-#
-# TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
-
-sub _latex_escape {
-  my $value = shift;
-  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
-  $value =~ s/([<>])/\$$1\$/g;
-  $value;
-}
-
-sub _html_escape {
-  my $value = shift;
-  encode_entities($value);
-  $value;
-}
-
-sub _html_escape_nbsp {
-  my $value = _html_escape(shift);
-  $value =~ s/ +/&nbsp;/g;
-  $value;
-}
-
-#utility methods for print_*
-
-sub _translate_old_latex_format {
-  warn "_translate_old_latex_format called\n"
-    if $DEBUG; 
-
-  my @template = ();
-  while ( @_ ) {
-    my $line = shift;
-  
-    if ( $line =~ /^%%Detail\s*$/ ) {
-  
-      push @template, q![@--!,
-                      q!  foreach my $_tr_line (@detail_items) {!,
-                      q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
-                      q!      $_tr_line->{'description'} .= !, 
-                      q!        "\\tabularnewline\n~~".!,
-                      q!        join( "\\tabularnewline\n~~",!,
-                      q!          @{$_tr_line->{'ext_description'}}!,
-                      q!        );!,
-                      q!    }!;
-
-      while ( ( my $line_item_line = shift )
-              !~ /^%%EndDetail\s*$/                            ) {
-        $line_item_line =~ s/'/\\'/g;    # nice LTS
-        $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
-        $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
-        push @template, "    \$OUT .= '$line_item_line';";
-      }
-
-      push @template, '}',
-                      '--@]';
-      #' doh, gvim
-    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
-
-      push @template, '[@--',
-                      '  foreach my $_tr_line (@total_items) {';
-
-      while ( ( my $total_item_line = shift )
-              !~ /^%%EndTotalDetails\s*$/                      ) {
-        $total_item_line =~ s/'/\\'/g;    # nice LTS
-        $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
-        $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
-        push @template, "    \$OUT .= '$total_item_line';";
-      }
-
-      push @template, '}',
-                      '--@]';
-
-    } else {
-      $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
-      push @template, $line;  
-    }
-  
-  }
-
-  if ($DEBUG) {
-    warn "$_\n" foreach @template;
-  }
-
-  (@template);
-}
-
-sub terms {
-  my $self = shift;
-  my $conf = $self->conf;
-
-  #check for an invoice-specific override
-  return $self->invoice_terms if $self->invoice_terms;
-  
-  #check for a customer- specific override
-  my $cust_main = $self->cust_main;
-  return $cust_main->invoice_terms if $cust_main->invoice_terms;
-
-  #use configured default
-  $conf->config('invoice_default_terms') || '';
-}
-
-sub due_date {
-  my $self = shift;
-  my $duedate = '';
-  if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = $self->_date() + ( $1 * 86400 );
-  }
-  $duedate;
-}
-
-sub due_date2str {
-  my $self = shift;
-  $self->due_date ? time2str(shift, $self->due_date) : '';
-}
-
-sub balance_due_msg {
-  my $self = shift;
-  my $msg = $self->mt('Balance Due');
-  return $msg unless $self->terms;
-  if ( $self->due_date ) {
-    $msg .= ' - ' . $self->mt('Please pay by'). ' '.
-      $self->due_date2str($date_format);
-  } elsif ( $self->terms ) {
-    $msg .= ' - '. $self->terms;
-  }
-  $msg;
-}
-
-sub balance_due_date {
-  my $self = shift;
-  my $conf = $self->conf;
-  my $duedate = '';
-  if (    $conf->exists('invoice_default_terms') 
-       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
-  }
-  $duedate;
-}
-
-sub credit_balance_msg { 
-  my $self = shift;
-  $self->mt('Credit Balance Remaining')
-}
-
-=item invnum_date_pretty
-
-Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
-
-=cut
-
-sub invnum_date_pretty {
-  my $self = shift;
-  $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
-}
-
-=item _date_pretty
-
-Returns a string with the date, for example: "3/20/2008"
-
-=cut
-
-sub _date_pretty {
-  my $self = shift;
-  time2str($date_format, $self->_date);
-}
-
-=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
-
-Generate section information for all items appearing on this invoice.
-This will only be called for multi-section invoices.
-
-For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
-related display records (L<FS::cust_bill_pkg_display>) and organize 
-them into two groups ("early" and "late" according to whether they come 
-before or after the total), then into sections.  A subtotal is calculated 
-for each section.
-
-Section descriptions are returned in sort weight order.  Each consists 
-of a hash containing:
-
-description: the package category name, escaped
-subtotal: the total charges in that section
-tax_section: a flag indicating that the section contains only tax charges
-summarized: same as tax_section, for some reason
-sort_weight: the package category's sort weight
-
-If 'condense' is set on the display record, it also contains everything 
-returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
-coderefs to generate parts of the invoice.  This is not advised.
-
-Arguments:
-
-LATE: an arrayref to push the "late" section hashes onto.  The "early"
-group is simply returned from the method.
-
-SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
-Turning this on has the following effects:
-- Ignores display items with the 'summary' flag.
-- Combines all items into the "early" group.
-- Creates sections for all non-disabled package categories, even if they 
-have no charges on this invoice, as well as a section with no name.
-
-ESCAPE: an escape function to use for section titles.
-
-EXTRA_SECTIONS: an arrayref of additional sections to return after the 
-sorted list.  If there are any of these, section subtotals exclude 
-usage charges.
-
-FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
-passed through to C<_condense_section()>.
-
-=cut
-
-use vars qw(%pkg_category_cache);
-sub _items_sections {
-  my $self = shift;
-  my $late = shift;
-  my $summarypage = shift;
-  my $escape = shift;
-  my $extra_sections = shift;
-  my $format = shift;
-
-  my %subtotal = ();
-  my %late_subtotal = ();
-  my %not_tax = ();
-
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
-  {
-
-      my $usage = $cust_bill_pkg->usage;
-
-      foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
-        next if ( $display->summary && $summarypage );
-
-        my $section = $display->section;
-        my $type    = $display->type;
-
-        $not_tax{$section} = 1
-          unless $cust_bill_pkg->pkgnum == 0;
-
-        if ( $display->post_total && !$summarypage ) {
-          if (! $type || $type eq 'S') {
-            $late_subtotal{$section} += $cust_bill_pkg->setup
-              if $cust_bill_pkg->setup != 0
-              || $cust_bill_pkg->setup_show_zero;
-          }
-
-          if (! $type) {
-            $late_subtotal{$section} += $cust_bill_pkg->recur
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-
-          if ($type && $type eq 'R') {
-            $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-          
-          if ($type && $type eq 'U') {
-            $late_subtotal{$section} += $usage
-              unless scalar(@$extra_sections);
-          }
-
-        } else {
-
-          next if $cust_bill_pkg->pkgnum == 0 && ! $section;
-
-          if (! $type || $type eq 'S') {
-            $subtotal{$section} += $cust_bill_pkg->setup
-              if $cust_bill_pkg->setup != 0
-              || $cust_bill_pkg->setup_show_zero;
-          }
-
-          if (! $type) {
-            $subtotal{$section} += $cust_bill_pkg->recur
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-
-          if ($type && $type eq 'R') {
-            $subtotal{$section} += $cust_bill_pkg->recur - $usage
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-          
-          if ($type && $type eq 'U') {
-            $subtotal{$section} += $usage
-              unless scalar(@$extra_sections);
-          }
-
-        }
-
-      }
-
-  }
-
-  %pkg_category_cache = ();
-
-  push @$late, map { { 'description' => &{$escape}($_),
-                       'subtotal'    => $late_subtotal{$_},
-                       'post_total'  => 1,
-                       'sort_weight' => ( _pkg_category($_)
-                                            ? _pkg_category($_)->weight
-                                            : 0
-                                       ),
-                       ((_pkg_category($_) && _pkg_category($_)->condense)
-                                           ? $self->_condense_section($format)
-                                           : ()
-                       ),
-                   } }
-                 sort _sectionsort keys %late_subtotal;
-
-  my @sections;
-  if ( $summarypage ) {
-    @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
-                map { $_->categoryname } qsearch('pkg_category', {});
-    push @sections, '' if exists($subtotal{''});
-  } else {
-    @sections = keys %subtotal;
-  }
-
-  my @early = map { { 'description' => &{$escape}($_),
-                      'subtotal'    => $subtotal{$_},
-                      'summarized'  => $not_tax{$_} ? '' : 'Y',
-                      'tax_section' => $not_tax{$_} ? '' : 'Y',
-                      'sort_weight' => ( _pkg_category($_)
-                                           ? _pkg_category($_)->weight
-                                           : 0
-                                       ),
-                       ((_pkg_category($_) && _pkg_category($_)->condense)
-                                           ? $self->_condense_section($format)
-                                           : ()
-                       ),
-                    }
-                  } @sections;
-  push @early, @$extra_sections if $extra_sections;
-
-  sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
-
-}
-
-#helper subs for above
-
-sub _sectionsort {
-  _pkg_category($a)->weight <=> _pkg_category($b)->weight;
-}
-
-sub _pkg_category {
-  my $categoryname = shift;
-  $pkg_category_cache{$categoryname} ||=
-    qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
-}
-
-my %condensed_format = (
-  'label' => [ qw( Description Qty Amount ) ],
-  'fields' => [
-                sub { shift->{description} },
-                sub { shift->{quantity} },
-                sub { my($href, %opt) = @_;
-                      ($opt{dollar} || ''). $href->{amount};
-                    },
-              ],
-  'align'  => [ qw( l r r ) ],
-  'span'   => [ qw( 5 1 1 ) ],            # unitprices?
-  'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
-);
-
-sub _condense_section {
-  my ( $self, $format ) = ( shift, shift );
-  ( 'condensed' => 1,
-    map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
-      qw( description_generator
-          header_generator
-          total_generator
-          total_line_generator
-        )
-  );
-}
-
-sub _condensed_generator_defaults {
-  my ( $self, $format ) = ( shift, shift );
-  return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
-}
-
-my %html_align = (
-  'c' => 'center',
-  'l' => 'left',
-  'r' => 'right',
-);
-
-sub _condensed_header_generator {
-  my ( $self, $format ) = ( shift, shift );
-
-  my ( $f, $prefix, $suffix, $separator, $column ) =
-    _condensed_generator_defaults($format);
-
-  if ($format eq 'latex') {
-    $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
-    $suffix = "\\\\\n\\hline";
-    $separator = "&\n";
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
-          };
-  } elsif ( $format eq 'html' ) {
-    $prefix = '<th></th>';
-    $suffix = '';
-    $separator = '';
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return qq!<th align="$html_align{$a}">$d</th>!;
-      };
-  }
-
-  sub {
-    my @args = @_;
-    my @result = ();
-
-    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
-      push @result,
-        &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
-    }
-
-    $prefix. join($separator, @result). $suffix;
-  };
-
-}
-
-sub _condensed_description_generator {
-  my ( $self, $format ) = ( shift, shift );
-
-  my ( $f, $prefix, $suffix, $separator, $column ) =
-    _condensed_generator_defaults($format);
-
-  my $money_char = '$';
-  if ($format eq 'latex') {
-    $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
-    $suffix = '\\\\';
-    $separator = " & \n";
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
-          };
-    $money_char = '\\dollar';
-  }elsif ( $format eq 'html' ) {
-    $prefix = '"><td align="center"></td>';
-    $suffix = '';
-    $separator = '';
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return qq!<td align="$html_align{$a}">$d</td>!;
-      };
-    #$money_char = $conf->config('money_char') || '$';
-    $money_char = '';  # this is madness
-  }
-
-  sub {
-    #my @args = @_;
-    my $href = shift;
-    my @result = ();
-
-    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
-      my $dollar = '';
-      $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
-      push @result,
-        &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
-                    map { $f->{$_}->[$i] } qw(align span width)
-                  );
-    }
-
-    $prefix. join( $separator, @result ). $suffix;
-  };
-
-}
-
-sub _condensed_total_generator {
-  my ( $self, $format ) = ( shift, shift );
-
-  my ( $f, $prefix, $suffix, $separator, $column ) =
-    _condensed_generator_defaults($format);
-  my $style = '';
-
-  if ($format eq 'latex') {
-    $prefix = "& ";
-    $suffix = "\\\\\n";
-    $separator = " & \n";
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
-          };
-  }elsif ( $format eq 'html' ) {
-    $prefix = '';
-    $suffix = '';
-    $separator = '';
-    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
-      };
-  }
-
-
-  sub {
-    my @args = @_;
-    my @result = ();
-
-    #  my $r = &{$f->{fields}->[$i]}(@args);
-    #  $r .= ' Total' unless $i;
-
-    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
-      push @result,
-        &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
-                    map { $f->{$_}->[$i] } qw(align span width)
-                  );
-    }
-
-    $prefix. join( $separator, @result ). $suffix;
-  };
-
-}
-
-=item total_line_generator FORMAT
-
-Returns a coderef used for generation of invoice total line items for this
-usage_class.  FORMAT is either html or latex
-
-=cut
-
-# should not be used: will have issues with hash element names (description vs
-# total_item and amount vs total_amount -- another array of functions?
-
-sub _condensed_total_line_generator {
-  my ( $self, $format ) = ( shift, shift );
-
-  my ( $f, $prefix, $suffix, $separator, $column ) =
-    _condensed_generator_defaults($format);
-  my $style = '';
-
-  if ($format eq 'latex') {
-    $prefix = "& ";
-    $suffix = "\\\\\n";
-    $separator = " & \n";
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
-          };
-  }elsif ( $format eq 'html' ) {
-    $prefix = '';
-    $suffix = '';
-    $separator = '';
-    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
-    $column =
-      sub { my ($d,$a,$s,$w) = @_;
-            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
-      };
-  }
-
-
-  sub {
-    my @args = @_;
-    my @result = ();
-
-    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
-      push @result,
-        &{$column}( &{$f->{fields}->[$i]}(@args),
-                    map { $f->{$_}->[$i] } qw(align span width)
-                  );
-    }
-
-    $prefix. join( $separator, @result ). $suffix;
-  };
-
-}
-
-#sub _items_extra_usage_sections {
-#  my $self = shift;
-#  my $escape = shift;
-#
-#  my %sections = ();
-#
-#  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
-#  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
-#  {
-#    next unless $cust_bill_pkg->pkgnum > 0;
-#
-#    foreach my $section ( keys %usage_class ) {
-#
-#      my $usage = $cust_bill_pkg->usage($section);
-#
-#      next unless $usage && $usage > 0;
-#
-#      $sections{$section} ||= 0;
-#      $sections{$section} += $usage;
-#
-#    }
-#
-#  }
-#
-#  map { { 'description' => &{$escape}($_),
-#          'subtotal'    => $sections{$_},
-#          'summarized'  => '',
-#          'tax_section' => '',
-#        }
-#      }
-#    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
-#
-#}
-
-sub _items_extra_usage_sections {
-  my $self = shift;
-  my $conf = $self->conf;
-  my $escape = shift;
-  my $format = shift;
-
-  my %sections = ();
-  my %classnums = ();
-  my %lines = ();
-
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
-
-  my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-    next unless $cust_bill_pkg->pkgnum > 0;
-
-    foreach my $classnum ( keys %usage_class ) {
-      my $section = $usage_class{$classnum}->classname;
-      $classnums{$section} = $classnum;
-
-      foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
-        my $amount = $detail->amount;
-        next unless $amount && $amount > 0;
-        $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
-        $sections{$section}{amount} += $amount;  #subtotal
-        $sections{$section}{calls}++;
-        $sections{$section}{duration} += $detail->duration;
-
-        my $desc = $detail->regionname; 
-        my $description = $desc;
-        $description = substr($desc, 0, $maxlength). '...'
-          if $format eq 'latex' && length($desc) > $maxlength;
-
-        $lines{$section}{$desc} ||= {
-          description     => &{$escape}($description),
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          ref             => '',
-          amount          => 0,
-          calls           => 0,
-          duration        => 0,
-          #unit_amount     => $cust_bill_pkg->unitrecur,
-          quantity        => $cust_bill_pkg->quantity,
-          product_code    => 'N/A',
-          ext_description => [],
-        };
-
-        $lines{$section}{$desc}{amount} += $amount;
-        $lines{$section}{$desc}{calls}++;
-        $lines{$section}{$desc}{duration} += $detail->duration;
-
-      }
-    }
-  }
-
-  my %sectionmap = ();
-  foreach (keys %sections) {
-    my $usage_class = $usage_class{$classnums{$_}};
-    $sectionmap{$_} = { 'description' => &{$escape}($_),
-                        'amount'    => $sections{$_}{amount},    #subtotal
-                        'calls'       => $sections{$_}{calls},
-                        'duration'    => $sections{$_}{duration},
-                        'summarized'  => '',
-                        'tax_section' => '',
-                        'sort_weight' => $usage_class->weight,
-                        ( $usage_class->format
-                          ? ( map { $_ => $usage_class->$_($format) }
-                              qw( description_generator header_generator total_generator total_line_generator )
-                            )
-                          : ()
-                        ), 
-                      };
-  }
-
-  my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
-                 values %sectionmap;
-
-  my @lines = ();
-  foreach my $section ( keys %lines ) {
-    foreach my $line ( keys %{$lines{$section}} ) {
-      my $l = $lines{$section}{$line};
-      $l->{section}     = $sectionmap{$section};
-      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
-      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
-      push @lines, $l;
-    }
-  }
-
-  return(\@sections, \@lines);
-
-}
-
-sub _did_summary {
-    my $self = shift;
-    my $end = $self->_date;
-
-    # start at date of previous invoice + 1 second or 0 if no previous invoice
-    my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
-    $start = 0 if !$start;
-    $start++;
-
-    my $cust_main = $self->cust_main;
-    my @pkgs = $cust_main->all_pkgs;
-    my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
-       = (0,0,0,0,0);
-    my @seen = ();
-    foreach my $pkg ( @pkgs ) {
-       my @h_cust_svc = $pkg->h_cust_svc($end);
-       foreach my $h_cust_svc ( @h_cust_svc ) {
-           next if grep {$_ eq $h_cust_svc->svcnum} @seen;
-           next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+    my $cust_main = $self->cust_main;
+    my @pkgs = $cust_main->all_pkgs;
+    my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+       = (0,0,0,0,0);
+    my @seen = ();
+    foreach my $pkg ( @pkgs ) {
+       my @h_cust_svc = $pkg->h_cust_svc($end);
+       foreach my $h_cust_svc ( @h_cust_svc ) {
+           next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+           next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
 
            my $inserted = $h_cust_svc->date_inserted;
            my $deleted = $h_cust_svc->date_deleted;
@@ -4795,23 +2861,6 @@ sub _items_svc_phone_sections {
 
 }
 
-sub _items { # seems to be unused
-  my $self = shift;
-
-  #my @display = scalar(@_)
-  #              ? @_
-  #              : qw( _items_previous _items_pkg );
-  #              #: qw( _items_pkg );
-  #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
-  my @display = qw( _items_previous _items_pkg );
-
-  my @b = ();
-  foreach my $display ( @display ) {
-    push @b, $self->$display(@_);
-  }
-  @b;
-}
-
 sub _items_previous {
   my $self = shift;
   my $conf = $self->conf;
@@ -4845,475 +2894,6 @@ sub _items_previous {
   #};
 }
 
-=item _items_pkg [ OPTIONS ]
-
-Return line item hashes for each package item on this invoice. Nearly 
-equivalent to 
-
-$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
-
-The only OPTIONS accepted is 'section', which may point to a hashref 
-with a key named 'condensed', which may have a true value.  If it 
-does, this method tries to merge identical items into items with 
-'quantity' equal to the number of items (not the sum of their 
-separate quantities, for some reason).
-
-=cut
-
-sub _items_pkg {
-  my $self = shift;
-  my %options = @_;
-
-  warn "$me _items_pkg searching for all package line items\n"
-    if $DEBUG > 1;
-
-  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
-
-  warn "$me _items_pkg filtering line items\n"
-    if $DEBUG > 1;
-  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-
-  if ($options{section} && $options{section}->{condensed}) {
-
-    warn "$me _items_pkg condensing section\n"
-      if $DEBUG > 1;
-
-    my %itemshash = ();
-    local $Storable::canonical = 1;
-    foreach ( @items ) {
-      my $item = { %$_ };
-      delete $item->{ref};
-      delete $item->{ext_description};
-      my $key = freeze($item);
-      $itemshash{$key} ||= 0;
-      $itemshash{$key} ++; # += $item->{quantity};
-    }
-    @items = sort { $a->{description} cmp $b->{description} }
-             map { my $i = thaw($_);
-                   $i->{quantity} = $itemshash{$_};
-                   $i->{amount} =
-                     sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
-                   $i;
-                 }
-             keys %itemshash;
-  }
-
-  warn "$me _items_pkg returning ". scalar(@items). " items\n"
-    if $DEBUG > 1;
-
-  @items;
-}
-
-sub _taxsort {
-  return 0 unless $a->itemdesc cmp $b->itemdesc;
-  return -1 if $b->itemdesc eq 'Tax';
-  return 1 if $a->itemdesc eq 'Tax';
-  return -1 if $b->itemdesc eq 'Other surcharges';
-  return 1 if $a->itemdesc eq 'Other surcharges';
-  $a->itemdesc cmp $b->itemdesc;
-}
-
-sub _items_tax {
-  my $self = shift;
-  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-}
-
-=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
-
-Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
-list of hashrefs describing the line items they generate on the invoice.
-
-OPTIONS may include:
-
-format: the invoice format.
-
-escape_function: the function used to escape strings.
-
-DEPRECATED? (expensive, mostly unused?)
-format_function: the function used to format CDRs.
-
-section: a hashref containing 'description'; if this is present, 
-cust_bill_pkg_display records not belonging to this section are 
-ignored.
-
-multisection: a flag indicating that this is a multisection invoice,
-which does something complicated.
-
-multilocation: a flag to display the location label for the package.
-
-Returns a list of hashrefs, each of which may contain:
-
-pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
-ext_description, which is an arrayref of detail lines to show below 
-the package line.
-
-=cut
-
-sub _items_cust_bill_pkg {
-  my $self = shift;
-  my $conf = $self->conf;
-  my $cust_bill_pkgs = shift;
-  my %opt = @_;
-
-  my $format = $opt{format} || '';
-  my $escape_function = $opt{escape_function} || sub { shift };
-  my $format_function = $opt{format_function} || '';
-  my $no_usage = $opt{no_usage} || '';
-  my $unsquelched = $opt{unsquelched} || ''; #unused
-  my $section = $opt{section}->{description} if $opt{section};
-  my $summary_page = $opt{summary_page} || ''; #unused
-  my $multilocation = $opt{multilocation} || '';
-  my $multisection = $opt{multisection} || '';
-  my $discount_show_always = 0;
-
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
-
-  my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
-
-  my @b = ();
-  my ($s, $r, $u) = ( undef, undef, undef );
-  foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
-  {
-
-    foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
-      if ( $_ && !$cust_bill_pkg->hidden ) {
-        $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
-        $_->{amount}      =~ s/^\-0\.00$/0.00/;
-        $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-        push @b, { %$_ }
-          if $_->{amount} != 0
-          || $discount_show_always
-          || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-          || (   $_->{_is_setup} && $_->{setup_show_zero} )
-        ;
-        $_ = undef;
-      }
-    }
-
-    my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
-
-    warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
-         $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
-      if $DEBUG > 1;
-
-    foreach my $display ( grep { defined($section)
-                                 ? $_->section eq $section
-                                 : 1
-                               }
-                          #grep { !$_->summary || !$summary_page } # bunk!
-                          grep { !$_->summary || $multisection }
-                          @cust_bill_pkg_display
-                        )
-    {
-
-      warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
-           $display->billpkgdisplaynum. "\n"
-        if $DEBUG > 1;
-
-      my $type = $display->type;
-
-      my $desc = $cust_bill_pkg->desc;
-      $desc = substr($desc, 0, $maxlength). '...'
-        if $format eq 'latex' && length($desc) > $maxlength;
-
-      my %details_opt = ( 'format'          => $format,
-                          'escape_function' => $escape_function,
-                          'format_function' => $format_function,
-                          'no_usage'        => $opt{'no_usage'},
-                        );
-
-      if ( $cust_bill_pkg->pkgnum > 0 ) {
-
-        warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
-          if $DEBUG > 1;
-        my $cust_pkg = $cust_bill_pkg->cust_pkg;
-
-        # start/end dates for invoice formats that do nonstandard 
-        # things with them
-        my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
-
-        if (    (!$type || $type eq 'S')
-             && (    $cust_bill_pkg->setup != 0
-                  || $cust_bill_pkg->setup_show_zero
-                )
-           )
-         {
-
-          warn "$me _items_cust_bill_pkg adding setup\n"
-            if $DEBUG > 1;
-
-          my $description = $desc;
-          $description .= ' Setup'
-            if $cust_bill_pkg->recur != 0
-            || $discount_show_always
-            || $cust_bill_pkg->recur_show_zero;
-
-          my @d = ();
-          unless ( $cust_pkg->part_pkg->hide_svc_detail
-                || $cust_bill_pkg->hidden )
-          {
-
-            push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short($self->_date, undef, 'I')
-              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
-
-            if ( $multilocation ) {
-              my $loc = $cust_pkg->location_label;
-              $loc = substr($loc, 0, $maxlength). '...'
-                if $format eq 'latex' && length($loc) > $maxlength;
-              push @d, &{$escape_function}($loc);
-            }
-
-          } #unless hiding service details
-
-          push @d, $cust_bill_pkg->details(%details_opt)
-            if $cust_bill_pkg->recur == 0;
-
-          if ( $cust_bill_pkg->hidden ) {
-            $s->{amount}      += $cust_bill_pkg->setup;
-            $s->{unit_amount} += $cust_bill_pkg->unitsetup;
-            push @{ $s->{ext_description} }, @d;
-          } else {
-            $s = {
-              _is_setup       => 1,
-              description     => $description,
-              #pkgpart         => $part_pkg->pkgpart,
-              pkgnum          => $cust_bill_pkg->pkgnum,
-              amount          => $cust_bill_pkg->setup,
-              setup_show_zero => $cust_bill_pkg->setup_show_zero,
-              unit_amount     => $cust_bill_pkg->unitsetup,
-              quantity        => $cust_bill_pkg->quantity,
-              ext_description => \@d,
-            };
-          };
-
-        }
-
-        if (    ( !$type || $type eq 'R' || $type eq 'U' )
-             && (
-                     $cust_bill_pkg->recur != 0
-                  || $cust_bill_pkg->setup == 0
-                  || $discount_show_always
-                  || $cust_bill_pkg->recur_show_zero
-                )
-           )
-        {
-
-          warn "$me _items_cust_bill_pkg adding recur/usage\n"
-            if $DEBUG > 1;
-
-          my $is_summary = $display->summary;
-          my $description = ($is_summary && $type && $type eq 'U')
-                            ? "Usage charges" : $desc;
-
-          #pry be a bit more efficient to look some of this conf stuff up
-          # outside the loop
-          unless (
-            $conf->exists('disable_line_item_date_ranges')
-              || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
-          ) {
-            my $time_period;
-            my $date_style = $conf->config( 'cust_bill-line_item-date_style',
-                                            $cust_main->agentnum
-                                          );
-            if ( defined($date_style) && $date_style eq 'month_of' ) {
-              $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
-            } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
-              my $desc = $conf->config( 'cust_bill-line_item-date_description',
-                                         $cust_main->agentnum
-                                      );
-              $desc .= ' ' unless $desc =~ /\s$/;
-              $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
-            } else {
-              $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
-                           " - ". time2str($date_format, $cust_bill_pkg->edate);
-            }
-            $description .= " ($time_period)";
-          }
-
-          my @d = ();
-          my @seconds = (); # for display of usage info
-
-          #at least until cust_bill_pkg has "past" ranges in addition to
-          #the "future" sdate/edate ones... see #3032
-          my @dates = ( $self->_date );
-          my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
-          push @dates, $prev->sdate if $prev;
-          push @dates, undef if !$prev;
-
-          unless ( $cust_pkg->part_pkg->hide_svc_detail
-                || $cust_bill_pkg->itemdesc
-                || $cust_bill_pkg->hidden
-                || $is_summary && $type && $type eq 'U' )
-          {
-
-            warn "$me _items_cust_bill_pkg adding service details\n"
-              if $DEBUG > 1;
-
-            push @d, map &{$escape_function}($_),
-                         $cust_pkg->h_labels_short(@dates, 'I')
-                                                   #$cust_bill_pkg->edate,
-                                                   #$cust_bill_pkg->sdate)
-              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
-
-            warn "$me _items_cust_bill_pkg done adding service details\n"
-              if $DEBUG > 1;
-
-            if ( $multilocation ) {
-              my $loc = $cust_pkg->location_label;
-              $loc = substr($loc, 0, $maxlength). '...'
-                if $format eq 'latex' && length($loc) > $maxlength;
-              push @d, &{$escape_function}($loc);
-            }
-
-            # Display of seconds_since_sqlradacct:
-            # On the invoice, when processing @detail_items, look for a field
-            # named 'seconds'.  This will contain total seconds for each 
-            # service, in the same order as @ext_description.  For services 
-            # that don't support this it will show undef.
-            if ( $conf->exists('svc_acct-usage_seconds') 
-                 and ! $cust_bill_pkg->pkgpart_override ) {
-              foreach my $cust_svc ( 
-                  $cust_pkg->h_cust_svc(@dates, 'I') 
-                ) {
-
-                # eval because not having any part_export_usage exports 
-                # is a fatal error, last_bill/_date because that's how 
-                # sqlradius_hour billing does it
-                my $sec = eval {
-                  $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
-                };
-                push @seconds, $sec;
-              }
-            } #if svc_acct-usage_seconds
-
-          }
-
-          unless ( $is_summary ) {
-            warn "$me _items_cust_bill_pkg adding details\n"
-              if $DEBUG > 1;
-
-            #instead of omitting details entirely in this case (unwanted side
-            # effects), just omit CDRs
-            $details_opt{'no_usage'} = 1
-              if $type && $type eq 'R';
-
-            push @d, $cust_bill_pkg->details(%details_opt);
-          }
-
-          warn "$me _items_cust_bill_pkg calculating amount\n"
-            if $DEBUG > 1;
-  
-          my $amount = 0;
-          if (!$type) {
-            $amount = $cust_bill_pkg->recur;
-          } elsif ($type eq 'R') {
-            $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
-          } elsif ($type eq 'U') {
-            $amount = $cust_bill_pkg->usage;
-          }
-  
-          if ( !$type || $type eq 'R' ) {
-
-            warn "$me _items_cust_bill_pkg adding recur\n"
-              if $DEBUG > 1;
-
-            if ( $cust_bill_pkg->hidden ) {
-              $r->{amount}      += $amount;
-              $r->{unit_amount} += $cust_bill_pkg->unitrecur;
-              push @{ $r->{ext_description} }, @d;
-            } else {
-              $r = {
-                description     => $description,
-                #pkgpart         => $part_pkg->pkgpart,
-                pkgnum          => $cust_bill_pkg->pkgnum,
-                amount          => $amount,
-                recur_show_zero => $cust_bill_pkg->recur_show_zero,
-                unit_amount     => $cust_bill_pkg->unitrecur,
-                quantity        => $cust_bill_pkg->quantity,
-                %item_dates,
-                ext_description => \@d,
-              };
-              $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
-            }
-
-          } else {  # $type eq 'U'
-
-            warn "$me _items_cust_bill_pkg adding usage\n"
-              if $DEBUG > 1;
-
-            if ( $cust_bill_pkg->hidden ) {
-              $u->{amount}      += $amount;
-              $u->{unit_amount} += $cust_bill_pkg->unitrecur;
-              push @{ $u->{ext_description} }, @d;
-            } else {
-              $u = {
-                description     => $description,
-                #pkgpart         => $part_pkg->pkgpart,
-                pkgnum          => $cust_bill_pkg->pkgnum,
-                amount          => $amount,
-                recur_show_zero => $cust_bill_pkg->recur_show_zero,
-                unit_amount     => $cust_bill_pkg->unitrecur,
-                quantity        => $cust_bill_pkg->quantity,
-                %item_dates,
-                ext_description => \@d,
-              };
-            }
-          }
-
-        } # recurring or usage with recurring charge
-
-      } else { #pkgnum tax or one-shot line item (??)
-
-        warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
-          if $DEBUG > 1;
-
-        if ( $cust_bill_pkg->setup != 0 ) {
-          push @b, {
-            'description' => $desc,
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-          };
-        }
-        if ( $cust_bill_pkg->recur != 0 ) {
-          push @b, {
-            'description' => "$desc (".
-                             time2str($date_format, $cust_bill_pkg->sdate). ' - '.
-                             time2str($date_format, $cust_bill_pkg->edate). ')',
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-          };
-        }
-
-      }
-
-    }
-
-    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
-                                && $conf->exists('discount-show-always'));
-
-  }
-
-  foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
-    if ( $_  ) {
-      $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
-      $_->{amount}      =~ s/^\-0\.00$/0.00/;
-      $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-      push @b, { %$_ }
-        if $_->{amount} != 0
-        || $discount_show_always
-        || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-        || (   $_->{_is_setup} && $_->{setup_show_zero} )
-    }
-  }
-
-  warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
-    if $DEBUG > 1;
-
-  @b;
-
-}
-
 sub _items_credits {
   my( $self, %opt ) = @_;
   my $trim_len = $opt{'trim_len'} || 60;
@@ -5362,51 +2942,6 @@ sub _items_payments {
 
 }
 
-=item _items_discounts_avail
-
-Returns an array of line item hashrefs representing available term discounts
-for this invoice.  This makes the same assumptions that apply to term 
-discounts in general: that the package is billed monthly, at a flat rate, 
-with no usage charges.  A prorated first month will be handled, as will 
-a setup fee if the discount is allowed to apply to setup fees.
-
-=cut
-
-sub _items_discounts_avail {
-  my $self = shift;
-  my $list_pkgnums = 0; # if any packages are not eligible for all discounts
-
-  my %plans = $self->discount_plans;
-
-  $list_pkgnums = grep { $_->list_pkgnums } values %plans;
-
-  map {
-    my $months = $_;
-    my $plan = $plans{$months};
-
-    my $term_total = sprintf('%.2f', $plan->discounted_total);
-    my $percent = sprintf('%.0f', 
-                          100 * (1 - $term_total / $plan->base_total) );
-    my $permonth = sprintf('%.2f', $term_total / $months);
-    my $detail = $self->mt('discount on item'). ' '.
-                 join(', ', map { "#$_" } $plan->pkgnums)
-      if $list_pkgnums;
-
-    # discounts for non-integer months don't work anyway
-    $months = sprintf("%d", $months);
-
-    +{
-      description => $self->mt('Save [_1]% by paying for [_2] months',
-                                $percent, $months),
-      amount      => $self->mt('[_1] ([_2] per month)', 
-                                $term_total, $money_char.$permonth),
-      ext_description => ($detail || ''),
-    }
-  } #map
-  sort { $b <=> $a } keys %plans;
-
-}
-
 =item call_details [ OPTION => VALUE ... ]
 
 Returns an array of CSV strings representing the call details for this invoice
index b382232..82b09b6 100644 (file)
@@ -8,7 +8,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status
              FS::cust_main::Billing_Discount
              FS::cust_main::Location
              FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
-             FS::geocode_Mixin
+             FS::geocode_Mixin FS::Quotable_Mixin
              FS::o2m_Common
              FS::Record
            );
index 6adc852..b5d51d3 100644 (file)
@@ -1,10 +1,10 @@
 package FS::prospect_main;
 
 use strict;
-use base qw( FS::o2m_Common FS::Record );
+use base qw( FS::Quotable_Mixin FS::o2m_Common FS::Record );
 use vars qw( $DEBUG );
 use Scalar::Util qw( blessed );
-use FS::Record qw( dbh qsearch ); #qsearchs );
+use FS::Record qw( dbh qsearch qsearchs );
 use FS::agent;
 use FS::cust_location;
 use FS::contact;
@@ -213,6 +213,9 @@ sub check {
 
 =item name
 
+Returns a name for this prospect, as a string (company name for commercial
+prospects, contact name for residential prospects).
+
 =cut
 
 sub name {
@@ -259,6 +262,16 @@ sub qual {
   qsearch( 'qual', { 'prospectnum' => $self->prospectnum } );
 }
 
+=item agent
+
+Returns the agent (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent {
+  my $self = shift;
+  qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
+}
 
 =item search HASHREF
 
index 4202335..0cfb11e 100644 (file)
@@ -1,10 +1,12 @@
 package FS::quotation;
+use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
 
 use strict;
-use base qw( FS::otaker_Mixin FS::Record );
-use FS::Record; # qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs );
+use FS::CurrentUser;
 use FS::cust_main;
 use FS::prospect_main;
+use FS::quotation_pkg;
 
 =head1 NAME
 
@@ -73,6 +75,8 @@ points to.  You can ask the object for a copy with the I<hash> method.
 =cut
 
 sub table { 'quotation'; }
+sub notice_name { 'Quotation'; }
+sub template_conf { 'quotation_'; }
 
 =item insert
 
@@ -111,7 +115,7 @@ sub check {
 
   $self->_date(time) unless $self->_date;
 
-  #XXX set usernum
+  $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
 
   $self->SUPER::check;
 }
@@ -134,6 +138,16 @@ sub cust_main {
   qsearchs('cust_main', { 'custnum' => $self->custnum } );
 }
 
+=item cust_bill_pkg
+
+=cut
+
+sub cust_bill_pkg {
+  my $self = shift;
+  #actually quotation_pkg objects
+  qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
+}
+
 =back
 
 =head1 BUGS
index e7aba20..590874d 100644 (file)
@@ -641,3 +641,11 @@ FS/part_svc_class.pm
 t/part_svc_class.t
 FS/ftp_target.pm
 t/ftp_target.t
+FS/quotation.pm
+t/quotation.t
+FS/quotation_pkg.pm
+t/quotation_pkg.t
+FS/quotation_pkg_discount.pm
+t/quotation_pkg_discount.t
+FS/Quotable_Mixin.pm
+t/Quotable_Mixin.t
diff --git a/conf/quotation_html b/conf/quotation_html
new file mode 100644 (file)
index 0000000..1dfb944
--- /dev/null
@@ -0,0 +1,266 @@
+<STYLE TYPE="text/css">
+.invoice { font-family: sans-serif; font-size: 10pt }
+.invoice_header { font-size: 10pt }
+.invoice_headerright TH { border-top: 2px solid #000000; border-bottom: 2px solid #000000 }
+.invoice_headerright TD { font-size: 10pt; empty-cells: show }
+.invoice_summary TH { border-bottom: 2px solid #000000 }
+.invoice_summary TD { font-size: 10pt; empty-cells: show }
+.invoice_longtable table { cellspacing: none }
+.invoice_longtable TH { border-top: 2px solid #000000; border-bottom: 1px solid #000000; padding-left: none; padding-right: none; font-size: 10pt }
+.invoice_desc TD { border-top: 2px solid #000000; font-weight: bold; font-size: 10pt }
+.invoice_desc_more TD { font-weight: bold; font-size: 10pt }
+.invoice_extdesc TD { font-size: 8pt }
+.invoice_totaldesc TD { font-size: 10pt; empty-cells: show }
+.allcaps { text-transform:uppercase }
+</STYLE>
+
+<table class="invoice" bgcolor="#ffffff" WIDTH=625 CELLSPACING=8><tr><td>
+
+  <table class="invoice_header" width="100%">
+    <tr>
+     <td><img src="<%= $cid ? "cid:$cid" : "cust_bill-logo.cgi?invnum=$invnum;template=$template" %>"></td>
+     <td align="left"><%= $returnaddress %></td>
+      <td align="right">
+        <table CLASS="invoice_headerright" cellspacing=0>
+          <tr>
+            <td align="center">
+              <%= emt('Quotation date') %><BR>
+              <B><%= $date %></B>
+            </td>
+            <td>
+            </td>
+            <td align="center">
+              <%= emt('Quotation #') %><BR>
+              <B><%= $quotationnum %></B>
+            </td>
+            <td>
+            </td>
+            <td align="center">
+              <%= $custnum ? emt('Customer #') : $prospectnum ? emt('Prospect #') : '' %><BR>
+              <B><%= $custnum || $prospectnum %></B>
+            </td>
+          </tr>
+          <tr>
+            <th>&nbsp;</th>
+            <th colspan=3 align="center" class="allcaps">
+             <FONT SIZE="+3"><%= substr(emt($notice_name),0,1) %></FONT><FONT SIZE="+2"><%= substr(emt($notice_name),1) %></FONT>
+            </th>
+            <th>&nbsp;</th>
+          </tr>
+        </table>
+      </td>
+    </tr>
+
+    <tr>
+      <td>
+      </td>
+      <td align="left">
+        <b><%= $name %></b><BR>
+        <%= join('<BR>', grep length($_), $company,
+                                          $address1,
+                                          $address2,
+                                          "$city,&nbsp;$state&nbsp;&nbsp;$zip",
+                                          $country,
+                )
+        %>
+      </td>
+      <%= $ship_enable ? ('<td align="left">'.
+                          join('<BR>',grep length($_), '<b>'.emt('Service Address').'</b>',
+                                                       $ship_company,
+                                                       $ship_address1,
+                                                       $ship_address2,
+                                                       "$ship_city,&nbsp;$ship_state&nbsp;$ship_zip",
+                                                       $ship_country,
+                                                       ' ',
+                                                       ' ',
+                              ).
+                           '</td><tr><td></td><td></td>'
+                         )
+                       : ''
+      %>
+      <td align="right">
+    <%=
+       if($barcode_cid) {
+           $OUT .= qq! <img src="cid:$barcode_cid"><br> !;
+       }
+       elsif($barcode_img) {
+           $OUT .= qq! <img src="cust_bill-barcode.cgi?invnum=$invnum;template=$template"><br> !;
+       }
+    %>
+        <%= $terms ? emt('Terms') . ": $terms" : '' %><BR>
+        <%= $po_line %>
+      </td>
+    </tr>
+
+  </table>
+  <%= $summary %>
+  <%=
+      my $notfirst = 0;
+      my $columncount = $unitprices ? 5 : 3;
+      foreach my $section ( grep { !$summary || $_->{description} ne $finance_section } @sections ) {
+        if ($section->{'pretotal'} && !$summary) {
+          $OUT .= '</table>' if $notfirst;
+          $OUT .=
+            '<table width="100%"><tr><td>'.
+            '<p align="right"><b><font size="+1">'.
+            uc(substr($section->{'pretotal'},0,1)).
+            '</font><font size="+0">'. uc(substr($section->{'pretotal'},1)).
+            '</font></b>'.
+            '<p>'.
+            '</td></tr>';
+        }
+        unless ($section->{'summarized'}) {
+          $OUT .= '</table>' if ( $notfirst || $section->{'pretotal'} && !$summary );
+          $OUT .= '<table><tr><td>';
+          my $sectionhead = $section->{'description'} || emt('Charges');
+          $OUT .=
+              '<p class="allcaps"><b><font size="+1">'. substr($sectionhead,0,1).
+              '</font><font size="+0">'. substr($sectionhead,1).
+              '</font></b>'.
+              '<p>'.
+              '</td></tr></table>';
+
+          $OUT .=
+            '<table class="invoice_longtable" CELLSPACING=0 WIDTH="100%">'.
+            '<tr>';
+
+          if ($section->{header_generator}) {
+            my $header = &{$section->{header_generator}}();
+            $OUT .= $header;
+            $columncount = scalar(my @array = split /<\/th><th/i, $header);
+          } else {
+            $OUT .=  '<th align="center">' . emt('Ref') . '</th>'.
+                     '<th align="left">' . emt('Description') . '</th>'.
+                     ( $unitprices 
+                       ? '<th align="left">' . emt('Unit Price') . '</th>'.
+                         '<th align="left">' . emt('Quantity') . '</th>'
+                        : '' ).
+                     '<th align="right">' . emt('Amount') . '</th>';
+          }
+            $OUT .= '</tr>';
+
+          my $lastref = 0;
+          foreach my $line (
+            grep { ( scalar(@sections) > 1 
+                   ? $section->{'description'} eq $_->{'section'}->{'description'}
+                   : 1
+                 ) }
+            @detail_items )
+          {
+            $OUT .=
+              '<tr class="invoice_desc';
+            if ( $section->{description_generator} ) {
+              $OUT .= &{$section->{description_generator}}($line);
+            } else {
+              $OUT .=  ( ($line->{'ref'} && $line->{'ref'} ne $lastref) ? '' : '_more' ).
+                       '">'.
+                       '<td align="center">'. 
+                       ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
+                       '<td align="left">'. $line->{'description'}. '</td>'.
+                       ( $unitprices 
+                           ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
+                             '<td align="left">'. $line->{'quantity'}. '</td>'
+                           : ''
+                       ).
+
+                       '<td align="right">'. $line->{'amount'}. '</td>';
+            }
+            $OUT .= '</tr>';
+            $lastref = $line->{'ref'};
+            if ( @{$line->{'ext_description'} } ) {
+              unless ( $section->{description_generator} ) {
+                $OUT .= '<tr class="invoice_extdesc"><td></td><td';
+                $OUT .= $unitprices ? ' colspan=3' : '';
+                $OUT .= '><table width="100%">';
+              }
+              foreach my $ext_desc ( @{$line->{'ext_description'} } ) {
+                $OUT .=
+                  '<tr class="invoice_extdesc">'.
+                    ( $section->{'description_generator'} ? '<td></td>' : '' ).
+                    '<td align="left" '.
+                         ( $ext_desc =~ /<\/?TD>/i ? '' : 'colspan=99' ). '>'.
+                      '&nbsp;&nbsp;'. $ext_desc.
+                    '</td>'.
+                  '</tr>'
+              }
+              unless ( $section->{description_generator} ) {
+                $OUT .= '</table></td><td></td>';
+              }
+              $OUT .= '</tr>';
+            }
+          }
+
+
+          if ($section->{'description'} || $multisection and !$section->{no_subtotal}) {
+            my $style = 'border-top: 3px solid #000000;'.
+                        'border-bottom: 3px solid #000000;';
+            $OUT .=
+              '<tr class="invoice_totaldesc">'.
+              qq(<td style="$style">&nbsp;</td>);
+            if ($section->{total_generator}) {
+              $OUT .= &{$section->{total_generator}}($section);
+            } else {
+              $OUT .= qq(<td align="left" style="$style"). 
+                      ( $unitprices ? ' colspan=3>' : '>' ).
+                      $section->{'description'}. ' ' . emt('Total') . '</td>'.
+                      qq(<td align="right" style="$style">).
+                      $section->{'subtotal'}. '</td>';
+            }
+            $OUT .= '</tr>';
+          }
+        } 
+        if ($section->{'posttotal'}) {
+          $OUT .= '<tr><td align="right" colspan='. $columncount. '>';
+          $OUT .=
+            '<p><font size="+1">'. $section->{'posttotal'}.
+            '</font>'.
+            '<p>';
+          $OUT .= '</td></tr>';
+        }
+
+        $notfirst++;
+
+      }
+
+      my $style = 'border-top: 3px solid #000000;';
+      my $linenum = 0;
+
+      foreach my $line ( @total_items ) {
+
+        $style .= 'border-bottom: 3px solid #000000;'
+          if ++$linenum == scalar(@total_items) - ( $balance_due_below_line ? 1 : 0 );
+
+        $OUT .=
+          '<tr class="invoice_totaldesc">';
+        if ($section->{total_line_generator}) {
+          $OUT .= &{$section->{total_line_generator}}($line);
+        } else {
+          $OUT .= qq(<td style="$style">&nbsp;</td>).
+                  qq(<td align="left" style="$style" colspan=").
+                    ( $columncount - 2 ). '">'.
+                    $line->{'total_item'}. '</td>'.
+                  qq(<td align="right" style="$style">).
+                    $line->{'total_amount'}. '</td>';
+        }
+        $OUT .= '</tr>';
+
+        $style='';
+
+      }
+
+    %>
+  </table>
+  <br><br>
+
+<%= length($summary)
+      ? ''
+      : ( $smallernotes
+            ? '<FONT SIZE="-1">'.$notes.'</FONT>'
+            : $notes
+        )
+%>
+
+  <hr NOSHADE SIZE=2 COLOR="#000000">
+  <p align="center" <%= $smallerfooter ? 'STYLE="font-size:75%;"' : '' %>><%= $footer %>
+
+</td></tr></table>
diff --git a/conf/quotation_latex b/conf/quotation_latex
new file mode 100644 (file)
index 0000000..772c2eb
--- /dev/null
@@ -0,0 +1,362 @@
+%% file: Standard Multipage.tex\r
+%% Purpose: Multipage bill template for e-Bills\r
+%% \r
+%% Created by Mark Asplen-Taylor\r
+%% Asplen Management Ltd\r
+%% www.asplen.co.uk\r
+%%\r
+%% Modified for Freeside by Kristian Hoffman\r
+%%\r
+%% Changes\r
+%%     0.1     4/12/00 Created\r
+%%     0.2     18/10/01        More fields added\r
+%%     1.0     16/11/01        RELEASED\r
+%%     1.2     16/10/02        Invoice number added\r
+%%     1.3     2/12/02 Logo graphic added\r
+%%     1.4     7/2/03  Multipage headers/footers added\r
+%%      n/a     forked for Freeside; checked into CVS\r
+%%\r
+\r
+\documentclass[letterpaper]{article}\r
+\r
+\usepackage{fancyhdr,lastpage,ifthen,array,fslongtable,afterpage,caption,multirow,bigstrut}\r
+\usepackage{graphicx}                  % required for logo graphic\r
+\usepackage[utf8]{inputenc}             % multilanguage support\r
+\usepackage[T1]{fontenc}\r
+\r
+\addtolength{\voffset}{-0.0cm}         % top margin to top of header\r
+\addtolength{\hoffset}{-0.6cm}         % left margin on page\r
+\addtolength{\topmargin}{[@-- defined($topmargin) ? $topmargin : '-1.25cm' --@]}\r
+\setlength{\headheight}{2.0cm}                 % height of header\r
+\setlength{\headsep}{[@-- defined($headsep) ? $headsep : '1.0cm' --@]}\r
+\setlength{\footskip}{1.0cm}           % bottom of footer from bottom of text\r
+\r
+%\addtolength{\textwidth}{2.1in}       % width of text\r
+\setlength{\textwidth}{19.5cm}\r
+\setlength{\textheight}{[@-- defined($textheight) ? $textheight : '19.5cm' --@]}\r
+\setlength{\oddsidemargin}{-0.9cm}     % odd page left margin\r
+\setlength{\evensidemargin}{-0.9cm}    % even page left margin\r
+\r
+\LTchunksize=40\r
+\r
+\renewcommand{\headrulewidth}{0pt}\r
+\renewcommand{\footrulewidth}{1pt}\r
+\r
+\renewcommand{\footrule}{\r
+[@--\r
+  $coupon ? '\ifthenelse{\equal{\thepage}{1}}' : '';\r
+--@]\r
+  {\r
+  }\r
+  {\r
+    \vbox to 0pt{\rule{\headwidth}{\footrulewidth}\vss}\r
+  }\r
+}\r
+\r
+\newcommand{\extracouponspace}{[@-- defined($extracouponspace) ? $extracouponspace : '3.6cm' --@]}\r
+\r
+% Adjust the inset of the mailing address\r
+\newcommand{\addressinset}[1][]{\hspace{1.0cm}}\r
+\r
+% Adjust the inset of the return address and logo\r
+\newcommand{\returninset}[1][]{\hspace{-0.25cm}}\r
+\r
+% New command for address lines i.e. skip them if blank\r
+\newcommand{\addressline}[1]{\ifthenelse{\equal{#1}{}}{}{#1\\}}\r
+\r
+% Inserts dollar symbol\r
+\newcommand{\dollar}[1][]{\symbol{36}}\r
+\r
+% Remove plain style header/footer\r
+\fancypagestyle{plain}{\r
+  \fancyhead{}\r
+}\r
+\fancyhf{}\r
+\r
+% Define fancy header/footer for first and subsequent pages\r
+\fancyfoot[C]{\r
+  \ifthenelse{\equal{\thepage}{1}}\r
+  { % First page\r
+[@--\r
+  if ($coupon) {\r
+    $OUT .= '\vspace{-\extracouponspace}';\r
+    $OUT .= '\rule[0.5em]{\textwidth}{\footrulewidth}\\\\';\r
+    $OUT .= $coupon;\r
+    $OUT .= '\vspace{'. $couponfootsep. '}' if defined($couponfootsep);\r
+  }\r
+  '';\r
+--@] [@-- $smallerfooter ? '\scriptsize{' : '\small{' --@]\r
+[@-- $footer --@]\r
+    }[@-- $coupon ? '\vspace{\extracouponspace}' : '' --@]\r
+  }\r
+  { % ... pages\r
+    [@-- $smallerfooter ? '\scriptsize{' : '\small{' --@]\r
+[@-- $smallfooter --@]\r
+    }\r
+  }\r
+}\r
+\r
+\fancyfoot[R]{\r
+  \ifthenelse{\equal{\thepage}{1}}\r
+  { % First page\r
+  }\r
+  { % ... pages\r
+    \small{\thepage\ of \pageref{LastPage}}\r
+  }\r
+}\r
+\r
+\fancyhead[L]{\r
+  \ifthenelse{\equal{\thepage}{1}}\r
+  { % First page\r
+    \returninset\r
+    \makebox{\r
+      \begin{tabular}{ll}\r
+        \includegraphics{[@-- $logo_file --@]} & [@-- $verticalreturnaddress ? '\\\\' : '' --@]\r
+        \begin{minipage}[b]{5.5cm}\r
+[@-- $returnaddress --@]\r
+        \end{minipage}\\\r
+      \end{tabular}\r
+    }\r
+  }\r
+  { % ... pages\r
+    %\includegraphics{[@-- $logo_file --@]}    % Uncomment if you want the logo on all pages.\r
+  }\r
+}\r
+\r
+\fancyhead[R]{\r
+  \ifthenelse{\equal{\thepage}{1}}\r
+  { % First page\r
+    \begin{tabular}{ccc}\r
+    [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\\r
+    \vspace{0.2cm}\r
+    \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]} \\\hline\r
+    \rule{0pt}{5ex} &~~ \huge{\textsc{[@-- emt($notice_name) --@]}} & \\\r
+    \vspace{-0.2cm}\r
+     & & \\\hline\r
+    \end{tabular}\r
+  }\r
+  { % ... pages\r
+    \small{\r
+      \begin{tabular}{lll}\r
+      [@-- join(' & ', emt('Invoice date'), emt('Invoice #'), emt('Customer #') ) --@]\\\r
+      \textbf{[@-- $date --@]} & \textbf{[@-- $invnum --@]} & \textbf{[@-- $custnum --@]}\\\r
+      \end{tabular}\r
+    }\r
+  }\r
+}\r
+\r
+\pagestyle{fancy}\r
+\r
+\r
+%% Font options are:\r
+%%     bch     Bitsream Charter\r
+%%     put     Utopia\r
+%%     phv     Adobe Helvetica\r
+%%     pnc     New Century Schoolbook\r
+%%     ptm     Times\r
+%%     pcr     Courier\r
+\r
+\renewcommand{\familydefault}{phv}\r
+\r
+\r
+% Commands for freeside table header...\r
+\r
+\newcommand{\FSdescriptionlength} { [@-- $unitprices ? '8.2cm' : '12.8cm' --@] }\r
+\newcommand{\FSdescriptioncolumncount} { [@-- $unitprices ? '4' : '6' --@] }\r
+\newcommand{\FSunitcolumns}{ [@-- \r
+  $unitprices \r
+  ? '\makebox[2.5cm][l]{\textbf{~~'.emt('Unit Price').'}}&\makebox[1.4cm]{\textbf{~'.emt('Quantity').'}}&' \r
+  : '' --@] }\r
+\r
+\newcommand{\FShead}{\r
+  \hline\r
+  \rule{0pt}{2.5ex}\r
+  \makebox[1.4cm]{\textbf{Ref}} &\r
+  \multicolumn{\FSdescriptioncolumncount}{l}{\makebox[\FSdescriptionlength][l]{\textbf{[@-- emt('Description') --@]}}}&\r
+  \FSunitcolumns\r
+  \makebox[1.6cm][r]{\textbf{[@-- emt('Amount') --@]}} \\\r
+  \hline\r
+}\r
+\r
+% ...description...\r
+\newcommand{\FSdesc}[5]{\r
+  \multicolumn{1}{c}{\rule{0pt}{2.5ex}\textbf{#1}} &\r
+  \multicolumn{[@-- $unitprices ? '4' : '6' --@]}{l}{\textbf{#2}} &\r
+[@-- $unitprices ? '  \multicolumn{1}{l}{\textbf{#3}} &'."\n".\r
+                   '  \multicolumn{1}{r}{\textbf{#4}} &'."\n"\r
+                 : ''\r
+--@]\r
+  \multicolumn{1}{r}{\textbf{\dollar #5}}\\\r
+}\r
+% ...extended description...\r
+\newcommand{\FSextdesc}[1]{\r
+  \multicolumn{1}{l}{\rule{0pt}{1.0ex}} &\r
+%%  \multicolumn{2}{l}{\small{~-~#1}}\\\r
+#1\\\r
+}\r
+% ...and total line items.\r
+\newcommand{\FStotaldesc}[2]{\r
+  & \multicolumn{6}{l}{#1} & #2\\\r
+}\r
+\r
+\r
+\begin{document}\r
+%      Headers and footers defined for the first page\r
+\addressinset \rule{0.5cm}{0cm} \r
+\makebox{\r
+\begin{minipage}[t]{7.0cm}\r
+\vspace{[@-- defined($addresssep) ? $addresssep : '0.25cm' --@]}\r
+\textbf{[@-- $payname --@]}\\\r
+\addressline{[@-- $company --@]}\r
+\addressline{[@-- $address1 --@]}\r
+\addressline{[@-- $address2 --@]}\r
+\addressline{[@-- $city --@], [@-- $state --@]~~[@-- $zip --@]}\r
+\addressline{[@-- $country --@]}\r
+\end{minipage}}\r
+\hfill\r
+\makebox{\r
+\begin{minipage}[t]{6.4cm}\r
+[@--\r
+  if ($ship_enable) {\r
+    $OUT .= '\textbf{' . emt('Service Address') . '}\\\\';\r
+    $OUT .= "\\addressline{$ship_company}";\r
+    $OUT .= "\\addressline{$ship_address1}";\r
+    $OUT .= "\\addressline{$ship_address2}";\r
+    $OUT .= "\\addressline{$ship_city, $ship_state~~$ship_zip}";\r
+    $OUT .= "\\addressline{$ship_country}";\r
+    $OUT .= '~\\\\';\r
+  }else{\r
+    $OUT .= '';\r
+  }\r
+--@]\r
+\begin{flushright}\r
+[@-- $terms ? emt('Terms') .": $terms" : '' --@]\\\r
+[@-- $po_line --@]\\\r
+\end{flushright}\r
+\end{minipage}}\r
+\vspace{1.5cm}\r
+%\r
+[@-- $summary --@]\r
+%\r
+\section*{}\r
+[@--\r
+  foreach my $section ( grep { !$summary || $_->{description} ne $finance_section } @sections ) {\r
+    if ($section->{'pretotal'} && !$summary) {\r
+      $OUT .= '\begin{flushright}';\r
+      $OUT .= '\large\textsc{'. $section->{'pretotal'}. '}\\\\';\r
+      $OUT .= '\\end{flushright}';\r
+    }\r
+    $OUT .= '\pagebreak' if $section->{'post_total'};\r
+    unless ($section->{'summarized'} ) {\r
+      $OUT .= '\captionsetup{singlelinecheck=false,justification=raggedright,font={Large,sc,bf}}';\r
+      $OUT .= '\ifthenelse{\equal{\thepage}{1}}{\setlength{\LTextracouponspace}{\extracouponspace}}{\setlength{\LTextracouponspace}{0pt}}'\r
+        if $coupon;\r
+      $OUT .= '\begin{longtable}{cllllllr}';\r
+      $OUT .= '\caption*{ ';\r
+      $OUT .= ($section->{'description'}) ? $section->{'description'}: emt('Charges');\r
+      $OUT .= '}\\\\';\r
+      if ($section->{header_generator}) {\r
+        $OUT .= &{$section->{header_generator}}();\r
+      } else {\r
+        $OUT .= '\FShead';\r
+      }\r
+      $OUT .= '\endfirsthead';\r
+      $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued from previous page').'}\\\\';\r
+      if ($section->{header_generator}) {\r
+        $OUT .= &{$section->{header_generator}}();\r
+      } else {\r
+        $OUT .= '\FShead';\r
+      }\r
+      $OUT .= '\endhead';\r
+      $OUT .= '\multicolumn{7}{r}{\rule{0pt}{2.5ex}'.emt('Continued on next page...').'}\\\\';\r
+      $OUT .= '\endfoot';\r
+      $OUT .= '\hline';\r
+\r
+      if (scalar(@sections) > 1 and !$section->{no_subtotal}) {\r
+        if ($section->{total_generator}) {\r
+          $OUT .= &{$section->{total_generator}}($section);\r
+        } else {\r
+          $OUT .= '\FStotaldesc{' . $section->{'description'} . ' Total}' .\r
+                  '{' . $section->{'subtotal'} . '}' . "\n";\r
+        }\r
+      }\r
+\r
+      #if ($section == $sections[$#sections]) {\r
+        foreach my $line (grep {$_->{section}->{description} eq $section->{description}} @total_items) {\r
+          if ($section->{total_line_generator}) {\r
+            $OUT .= &{$section->{total_line_generator}}($line);\r
+          } else {\r
+            $OUT .= '\FStotaldesc{' . $line->{'total_item'} . '}' .\r
+                    '{' . $line->{'total_amount'} . '}' . "\n";\r
+          }\r
+        }\r
+      #}\r
+\r
+      $OUT .= '\hline';\r
+      $OUT .= '\endlastfoot';\r
+\r
+      my $lastref = 0;\r
+      foreach my $line (\r
+        grep { ( scalar( @sections ) > 1 \r
+               ? $section->{'description'} eq $_->{'section'}->{'description'}\r
+               : 1\r
+             ) }\r
+        @detail_items )\r
+      {\r
+        my $ext_description = $line->{'ext_description'};\r
+  \r
+        # Don't break-up small packages.\r
+        my $rowbreak = @$ext_description < 5 ? '*' : '';\r
+  \r
+        $OUT .= "\\hline\n" if ($line->{'ref'} && $line->{'ref'} ne $lastref);\r
+        if ($section->{description_generator}) {\r
+          $OUT .= &{$section->{description_generator}}($line);\r
+        } else {\r
+          $OUT .= '\FSdesc'.\r
+                  '{' . ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ) . '}'.\r
+                  '{' . $line->{'description'} . '}' .\r
+                  '{' . ( $unitprices ? $line->{'unit_amount'} : '' ) . '}'.\r
+                  '{' . ( $unitprices ? $line->{'quantity'} : ''  ) . '}' .\r
+                  '{' . $line->{'amount'} . "}${rowbreak}\n";\r
+        }\r
+        $lastref = $line->{'ref'};\r
+\r
+        foreach my $ext_desc (@$ext_description) {\r
+          if ($section->{extended_description_generator}) {\r
+            $OUT .= &{$section->{extended_description_generator}}($ext_desc);\r
+          } else {\r
+            if ( $ext_desc !~ /[^\\]&/ ) {\r
+              $ext_desc = substr($ext_desc, 0, 80) . '...'\r
+                if (length($ext_desc) > 80);\r
+              $ext_desc = '\multicolumn{6}{l}{\small{~~~'. $ext_desc. '}}';\r
+            }else{\r
+              $ext_desc = "~~~$ext_desc";\r
+            }\r
+            $OUT .= '\FSextdesc{' . $ext_desc . '}' . "${rowbreak}\n";\r
+          }\r
+        }\r
+\r
+      }\r
+\r
+      $OUT .= '\end{longtable}';\r
+    }\r
+    if ($section->{'posttotal'}) {\r
+      $OUT .= '\begin{flushright}';\r
+      $OUT .= '\normalfont\large\bfseries\textsc{'. $section->{'posttotal'}. '}\\\\';\r
+      $OUT .= '\\end{flushright}';\r
+    }\r
+  }\r
+\r
+--@]\r
+\vfill\r
+\begin{minipage}[t]{\textwidth}\r
+  [@-- length($summary)\r
+         ? ''\r
+        : ( $smallernotes\r
+              ? '\scriptsize{ '.$notes.' }'\r
+              : $notes\r
+          )\r
+  --@]\r
+  [@-- $coupon ? '\ifthenelse{\equal{\thepage}{1}}{\rule{0pt}{\extracouponspace}}{}' : '' --@]\r
+\end{minipage}\r
+\end{document}\r
diff --git a/conf/quotation_latexnotes b/conf/quotation_latexnotes
new file mode 100644 (file)
index 0000000..58fd68a
--- /dev/null
@@ -0,0 +1,8 @@
+%%
+%%     Add any quotation notes in here
+%%
+\section*{\textsc{Notes}}
+\begin{enumerate}
+\item Thank you for your interest in our services.
+\item If you have any questions please email or telephone.
+\end{enumerate}
index 7671c36..a695665 100644 (file)
@@ -1,6 +1,6 @@
 <% include( 'elements/process.html',
                'table'       => 'quotation',
-               'redirect'    => $p.'view/quotation.html?',
+               'redirect'    => popurl(3).'view/quotation.html?',
            )
 %>
 <%init>
index f706425..8b60623 100644 (file)
@@ -3,8 +3,23 @@
                  'table'  => 'quotation',
                  'labels' => { 
                                'quotationnum' => 'Quotation number',
+                               'prospectnum'  => 'Prospect',
+                               'custnum'      => 'Customer',
+                               '_date'        => 'Date',
+                               'disabled'     => 'Disabled',
                              },
+                 'fields' => [
+                   { field=>'prospectnum', type=>'fixed-prospect_main' },
+                   { field=>'custnum',     type=>'fixed-cust_main' },
+                   { field=>'_date',       type=>'fixed-date' },
+                   { field=>'disabled',    type=>'checkbox', value=>'Y'},
+                             ],
                  #XXX some way to disable the "view all"
+                 'new_callback' => sub { my( $cgi, $quotation) = @_;
+                                         $quotation->$_( $cgi->param($_) )
+                                           foreach qw( prospectnum custnum );
+                                         $quotation->_date(time);
+                                       },
            )
 %>
 <%init>
diff --git a/httemplate/elements/tr-fixed-cust_main.html b/httemplate/elements/tr-fixed-cust_main.html
new file mode 100644 (file)
index 0000000..00bcb66
--- /dev/null
@@ -0,0 +1,15 @@
+% if ( $cust_main ) {
+  <% include('tr-fixed.html', %opt ) %>
+% }
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $cust_main = $value ? qsearchs('cust_main', {custnum=>$value} )
+                       : '';
+
+$opt{'formatted_value'} = $cust_main->name if $cust_main;
+
+</%init>
diff --git a/httemplate/elements/tr-fixed-date.html b/httemplate/elements/tr-fixed-date.html
new file mode 100644 (file)
index 0000000..716e5ce
--- /dev/null
@@ -0,0 +1,13 @@
+<% include('tr-fixed.html', %opt ) %>
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $conf = new FS::Conf;
+my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
+$opt{'formatted_value'} = time2str($date_format, $value);
+
+</%init>
diff --git a/httemplate/elements/tr-fixed-prospect_main.html b/httemplate/elements/tr-fixed-prospect_main.html
new file mode 100644 (file)
index 0000000..8da0ffb
--- /dev/null
@@ -0,0 +1,15 @@
+% if ( $prospect_main ) {
+  <% include('tr-fixed.html', %opt ) %>
+% }
+<%init>
+
+my %opt = @_;
+
+my $value = $opt{'curr_value'} || $opt{'value'};
+
+my $prospect_main = $value ? qsearchs('prospect_main', {prospectnum=>$value} )
+                           : '';
+
+$opt{'formatted_value'} = $prospect_main->name if $prospect_main;
+
+</%init>
index 9e85348..801d64b 100644 (file)
 
 <BR>
 
+% if ( $curuser->access_right('Generate quotation') ) { 
+  <FONT CLASS="fsinnerbox-title"><% mt( 'Quotations' ) |h %></FONT>
+  <A HREF="<%$p%>edit/quotation.html?prospectnum=<% $prospectnum %>">New quotation</A>
+% my @quotations = $prospect_main->quotation;
+% if ( @quotations ) {
+    <& /elements/table-grid.html &>
+%     my $bgcolor1 = '#eeeeee';
+%     my $bgcolor2 = '#ffffff';
+%     my $bgcolor = '';
+      <TR>
+        <TH CLASS="grid" BGCOLOR="#cccccc">#</TH>
+        <TH CLASS="grid" BGCOLOR="#cccccc"><% mt('Date') |h %></TH>
+      </TR>
+%     foreach my $quotation (@quotations) {
+        <TR>
+          <TD CLASS="grid" BGCOLOR="#cccccc"><% $quotation->quotationnum %></TD>
+          <TD CLASS="grid" BGCOLOR="#cccccc"><% time2str($date_format, $quotation->_date) |h %></TD>
+        </TR>
+%     }
+    </TABLE>
+% }
+    <BR><BR>
+% }
+
+
 % if ( $curuser->access_right('Qualify service') ) { 
 <% include( '/elements/popup_link-prospect_main.html',
               'action'        => $p. 'misc/qual.html',
     <BR><BR>
 % }
 
+<!--
 <% ntable("#cccccc") %>
 
 <TR>
 </TR>
 
 </TABLE>
+-->
 
 <%init>
 
@@ -95,6 +122,10 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
   unless $curuser->access_right('View prospect');
 
+my $conf = new FS::Conf;
+
+my $date_format = $conf->config('date_format') || '%m/%d/%Y';
+
 my $prospectnum;
 if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
   $prospectnum = $1;
index 2c2c6b7..866ade2 100755 (executable)
@@ -22,21 +22,20 @@ XXX resending quotations
 
 % } 
 
-XXX view typset quotation
+</%doc>
 
-% if ( $conf->exists('invoice_latex') ) { 
+% if ( $conf->exists('quotation_latex') ) { 
 
-  <A HREF="<% $p %>view/cust_bill-pdf.cgi?<% $link %>"><% mt('View typeset invoice PDF') |h %></A>
+  <A HREF="<% $p %>view/quotation-pdf.cgi?<% $link %>"><% mt('View typeset quotation PDF') |h %></A>
   <BR><BR>
 % } 
 
-XXX actually show the quotation
-
-% if ( $conf->exists('invoice_html') ) { 
-  <% join('', $cust_bill->print_html(\%opt) ) %>
+% if ( $conf->exists('quotation_html') ) { 
+    <% join('', $quotation->print_html() ) %>
 % } else { 
-  <PRE><% join('', $cust_bill->print_text(\%opt) ) %></PRE>
-% } 
+%   die "quotation_html config missing";
+% }
+% #plaintext quotations? <PRE><% join('', $quotation->print_text() ) %></PRE>
 
 </%doc>
 
@@ -56,7 +55,7 @@ if ( $query =~ /^(\d+)$/ ) {
   $quotationnum = $cgi->param('quotationnum');
 }
 
-#my $conf = new FS::Conf;
+my $conf = new FS::Conf;
 
 my $quotation = qsearchs({
   'select'    => 'quotation.*',
@@ -67,6 +66,7 @@ my $quotation = qsearchs({
 });
 die "Quotation #$quotationnum not found!" unless $quotation;
 
+my $menubar;
 if ( my $custnum = $quotation->custnum ) {
   my $display_custnum = $quotation->cust_main->display_custnum;
   $menubar = menubar(
@@ -78,4 +78,9 @@ if ( my $custnum = $quotation->custnum ) {
   );
 }
 
+my $link = "quotationnum=$quotationnum";
+#$link .= ';template='. uri_escape($template) if $template;
+#$link .= ';notice_name='. $notice_name if $notice_name;
+
+
 </%init>