[freeside-commits] branch master updated. eb3bd392a89b8b666dc512951e78913c05b98810

Mark Wells mark at 420.am
Fri Sep 27 17:29:33 PDT 2013


The branch, master has been updated
       via  eb3bd392a89b8b666dc512951e78913c05b98810 (commit)
      from  e3012c0751dad6710ea35b6d074b551bffdad09b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit eb3bd392a89b8b666dc512951e78913c05b98810
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Sep 27 17:19:36 2013 -0700

    invoice configurations, #24723

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 780e3ff..f1fc5cb 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -353,6 +353,8 @@ if ( -e $addl_handler_use_file ) {
   use FS::sales_pkg_class;
   use FS::svc_alarm;
   use FS::cable_model;
+  use FS::invoice_mode;
+  use FS::invoice_conf;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index ed37904..16546b3 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4377,6 +4377,51 @@ sub tables_hashref {
       'index'       => [ [ 'derivenum', ], ],
     },
 
+    'invoice_mode' => {
+      'columns' => [
+        'modenum',      'serial', '', '', '', '',
+        'agentnum',        'int', 'NULL', '', '', '',
+        'modename',    'varchar', '', 32, '', '',
+      ],
+      'primary_key' => 'modenum',
+      'unique'      => [ ],
+      'index'       => [ ],
+    },
+
+    'invoice_conf' => {
+      'columns' => [
+        'confnum',              'serial',   '', '', '', '',
+        'modenum',              'int',      '', '', '', '',
+        'locale',               'varchar',  'NULL', 16, '', '',
+        'notice_name',          'varchar',  'NULL', 64, '', '',
+        'subject',              'varchar',  'NULL', 64, '', '',
+        'htmlnotes',            'text',     'NULL', '', '', '',
+        'htmlfooter',           'text',     'NULL', '', '', '',
+        'htmlsummary',          'text',     'NULL', '', '', '',
+        'htmlreturnaddress',    'text',     'NULL', '', '', '',
+        'latexnotes',           'text',     'NULL', '', '', '',
+        'latexfooter',          'text',     'NULL', '', '', '',
+        'latexsummary',         'text',     'NULL', '', '', '',
+        'latexcoupon',          'text',     'NULL', '', '', '',
+        'latexsmallfooter',     'text',     'NULL', '', '', '',
+        'latexreturnaddress',   'text',     'NULL', '', '', '',
+        'latextopmargin',       'varchar',  'NULL', 16, '', '',
+        'latexheadsep',         'varchar',  'NULL', 16, '', '',
+        'latexaddresssep',      'varchar',  'NULL', 16, '', '',
+        'latextextheight',      'varchar',  'NULL', 16, '', '',
+        'latexextracouponspace','varchar',  'NULL', 16, '', '',
+        'latexcouponfootsep',   'varchar',  'NULL', 16, '', '',
+        'latexcouponamountenclosedsep', 'varchar',  'NULL', 16, '', '',
+        'latexcoupontoaddresssep',      'varchar',  'NULL', 16, '', '',
+        'latexverticalreturnaddress',      'char',  'NULL',  1, '', '',
+        'latexcouponaddcompanytoaddress',  'char',  'NULL',  1, '', '',
+        'logo_png',             'blob',     'NULL', '', '', '',
+        'logo_eps',             'blob',     'NULL', '', '', '',
+        'lpr',                  'varchar',  'NULL', $char_d, '', '',
+      ],
+      'primary_key' => 'confnum',
+      'unique' => [ [ 'modenum', 'locale' ] ],
+    },
 
     # name type nullability length default local
 
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index 356de5b..840df75 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -18,6 +18,7 @@ use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( generate_ps generate_pdf );
 use FS::pkg_category;
 use FS::pkg_class;
+use FS::invoice_mode;
 use FS::L10N;
 
 $DEBUG = 0;
@@ -30,12 +31,51 @@ FS::UID->install_callback( sub {
   $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
 } );
 
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item conf [ MODE ]
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale.
+
+If the "mode" pseudo-field is set on the object, the configuration handle
+will be an L<FS::invoice_conf> for that invoice mode (and the customer's
+locale).
+
+=cut
+
+sub conf {
+  my $self = shift;
+  my $mode = $self->get('mode');
+  if ($self->{_conf} and !defined($mode)) {
+    return $self->{_conf};
+  }
+
+  my $cust_main = $self->cust_main;
+  my $locale = $cust_main ? $cust_main->locale : '';
+  my $conf;
+  if ( $mode ) {
+    if ( ref $mode and $mode->isa('FS::invoice_mode') ) {
+      $mode = $mode->modenum;
+    } elsif ( $mode =~ /\D/ ) {
+      die "invalid invoice mode $mode";
+    }
+    $conf = qsearchs('invoice_conf', { modenum => $mode, locale => $locale });
+    if (!$conf) {
+      $conf = qsearchs('invoice_conf', { modenum => $mode, locale => '' });
+      # it doesn't have a locale, but system conf still might
+      $conf->set('locale' => $locale) if $conf;
+    }
+  }
+  # if $mode is unspecified, or if there is no invoice_conf matching this mode
+  # and locale, then use the system config only (but with the locale)
+  $conf ||= FS::Conf->new({ 'locale' => $locale });
+  # cache it
+  return $self->{_conf} = $conf;
+}
+
+=item print_text OPTIONS
 
 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.
+Options can be passed as a hash.
 
 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.
@@ -50,25 +90,19 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 
 sub print_text {
   my $self = shift;
-  my( $today, $template, %opt );
+  my %params;
   if ( ref($_[0]) ) {
-    %opt = %{ shift() };
-    $today = delete($opt{'time'}) || '';
-    $template = delete($opt{template}) || '';
+    %params = %{ shift() };
   } else {
-    ( $today, $template, %opt ) = @_;
+    %params = @_;
   }
 
-  my %params = ( 'format' => 'template' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+  $params{'format'} = 'template'; # for some reason
 
   $self->print_generic( %params );
 }
 
-=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
+=item print_latex HASHREF
 
 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
@@ -76,15 +110,16 @@ 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.
+Options can be passed as a hash.
 
 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<template>, if specified, is the name of a suffix for alternate invoices.  
+This is strongly deprecated; see L<FS::invoice_conf> for the right way to
+customize invoice templates for different purposes.
 
 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
@@ -92,22 +127,20 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 
 sub print_latex {
   my $self = shift;
-  my $conf = $self->conf;
-  my( $today, $template, %opt );
+  my %params;
+
   if ( ref($_[0]) ) {
-    %opt = %{ shift() };
-    $today = delete($opt{'time'}) || '';
-    $template = delete($opt{template}) || '';
+    %params = %{ shift() };
   } else {
-    ( $today, $template, %opt ) = @_;
+    %params = @_;
   }
 
-  my %params = ( 'format' => 'latex' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name no_date no_number );
+  $params{'format'} = 'latex';
+  my $conf = $self->conf;
 
+  # this needs to go away
+  my $template = $params{'template'};
+  # and this especially
   $template ||= $self->_agent_template
     if $self->can('_agent_template');
 
@@ -191,7 +224,8 @@ Non optional options include
 
 Optional options include
 
-template - a value used as a suffix for a configuration template
+template - a value used as a suffix for a configuration template.  Please 
+don't use this.
 
 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.
@@ -214,6 +248,7 @@ locale - override customer's locale
 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;
@@ -227,6 +262,8 @@ sub print_generic {
     unless $cust_main->payname
         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
 
+  my $locale = $params{'locale'} || $cust_main->locale;
+
   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
                      'html'     => [ '<%=', '%>' ],
                      'template' => [ '{', '}' ],
@@ -235,11 +272,18 @@ sub print_generic {
   warn "$me print_generic creating template\n"
     if $DEBUG > 1;
 
+  # set the notice name here, and nowhere else.
+  my $notice_name =  $params{notice_name}
+                  || $conf->config('notice_name')
+                  || $self->notice_name;
+
   #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");
+
+  # the base template
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
 
@@ -380,6 +424,7 @@ sub print_generic {
 
   # generate template variables
   my $returnaddress;
+
   if (
          defined( $conf->config_orbase( "invoice_${format}returnaddress",
                                         $template
@@ -457,7 +502,7 @@ sub print_generic {
     'today'           => time2str($date_format_long, $today),
     'terms'           => $self->terms,
     'template'        => $template, #params{'template'},
-    'notice_name'     => ($params{'notice_name'} || $self->notice_name),#escape_function?
+    'notice_name'     => $notice_name, # escape?
     'current_charges' => sprintf("%.2f", $self->charged),
     'duedate'         => $self->due_date2str($rdate_format), #date_format?
 
@@ -499,7 +544,7 @@ sub print_generic {
   );
  
   #localization
-  my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
+  my $lh = FS::L10N->get_handle( $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
@@ -942,9 +987,6 @@ sub print_generic {
 
   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
@@ -1544,12 +1586,9 @@ sub print_html {
   my %params;
   if ( ref($_[0]) ) {
     %params = %{ shift() }; 
-  }else{
-    $params{'time'} = shift;
-    $params{'template'} = shift;
-    $params{'cid'} = shift;
+  } else {
+    %params = @_;
   }
-
   $params{'format'} = 'html';
   
   $self->print_generic( %params );
diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm
index 0cd07ef..d70ff18 100644
--- a/FS/FS/agent.pm
+++ b/FS/FS/agent.pm
@@ -378,6 +378,23 @@ sub payment_gateway {
   $payment_gateway;
 }
 
+=item invoice_modes
+
+Returns all L<FS::invoice_mode> objects that are valid for this agent (i.e.
+those with this agentnum or null agentnum).
+
+=cut
+
+sub invoice_modes {
+  my $self = shift;
+  qsearch( {
+      table     => 'invoice_mode',
+      hashref   => { agentnum => $self->agentnum },
+      extra_sql => ' OR agentnum IS NULL',
+      order_by  => ' ORDER BY modename',
+  } );
+}
+
 =item num_prospect_cust_main
 
 Returns the number of prospects (customers with no packages ever ordered) for
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 97dd38b..a747a78 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -80,7 +80,7 @@ FS::cust_bill - Object methods for cust_bill records
   $tax_amount = $record->tax;
 
   @lines = $cust_bill->print_text;
-  @lines = $cust_bill->print_text $time;
+  @lines = $cust_bill->print_text('time' => $time);
 
 =head1 DESCRIPTION
 
@@ -153,7 +153,13 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
-sub notice_name { 'Invoice'; }
+
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+  my $self = shift;
+  $self->conf->config('notice_name') || 'Invoice'
+}
 
 sub cust_linked { $_[0]->cust_main_custnum; } 
 sub cust_unlinked_msg {
@@ -1043,7 +1049,7 @@ Options:
 
 sender address, required
 
-=item tempate
+=item template
 
 alternate template name, optional
 
@@ -1077,15 +1083,10 @@ sub generate_email {
 
   my %return = (
     'from'      => $args{'from'},
-    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+    'subject'   => ($args{'subject'} || $self->email_subject),
   );
 
-  my %opt = (
-    'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
-    'template'      => $args{'template'},
-    'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
-    'no_coupon'     => $args{'no_coupon'},
-  );
+  $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
 
   my $cust_main = $self->cust_main;
 
@@ -1127,7 +1128,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
-        $data = [ $self->print_text(\%opt) ];
+        $data = [ $self->print_text(\%args) ];
       }
 
     }
@@ -1184,10 +1185,10 @@ sub generate_email {
           'Filename'   => 'barcode.png',
           'Content-ID' => "<$barcode_content_id>",
         ;
-        $opt{'barcode_cid'} = $barcode_content_id;
+        $args{'barcode_cid'} = $barcode_content_id;
       }
 
-      $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
+      $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
     }
 
     $alternative->attach(
@@ -1249,7 +1250,7 @@ sub generate_email {
 
       $related->add_part($image) if $image;
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
+      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
 
       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
@@ -1281,7 +1282,7 @@ sub generate_email {
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
-        { $self->mimebuild_pdf(\%opt) }
+        { $self->mimebuild_pdf(\%args) }
       ];
     }
   
@@ -1301,7 +1302,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
-        $return{'body'} = [ $self->print_text(\%opt) ];
+        $return{'body'} = [ $self->print_text(\%args) ];
       }
 
     }
@@ -1330,105 +1331,48 @@ sub mimebuild_pdf {
   );
 }
 
-=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
+=item send HASHREF
 
 Sends this invoice to the destinations configured for this customer: sends
 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
-Options can be passed as a hashref (recommended) or as a list of up to 
-four values for templatename, agentnum, invoice_from and amount.
+Options can be passed as a hashref.  Positional parameters are no longer
+allowed.
 
-I<template>, if specified, is the name of a suffix for alternate invoices.
+I<template>: a suffix for alternate invoices
 
-I<agentnum>, if specified, means that this invoice will only be sent for customers
-of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
-single agent) or an arrayref of agentnums.
+I<agentnum>: obsolete, now does nothing.
 
-I<invoice_from>, if specified, overrides the default email invoice From: address.
+I<invoice_from> overrides the default email invoice From: address.
 
-I<amount>, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
+I<amount>: obsolete, does nothing
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> overrides "Invoice" as the name of the sent document 
+(templates from 10/2009 or newer required).
 
-I<lpr>, if specified, is passed to 
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
 
 =cut
 
-sub queueable_send {
-  my %opt = @_;
-
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my @args = ( $opt{template}, $opt{agentnum} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
-
-  my $error = $self->send( @args );
-  die $error if $error;
-
-}
-
 sub send {
   my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : +{ @_ };
   my $conf = $self->conf;
 
-  my( $template, $invoice_from, $notice_name );
-  my $agentnums = '';
-  my $balance_over = 0;
-  my $lpr = '';
-
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    if ( $agentnums = $opt->{'agentnum'} ) {
-      $agentnums = [ $agentnums ] unless ref($agentnums);
-    }
-    $invoice_from = $opt->{'invoice_from'};
-    $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
-    $notice_name = $opt->{'notice_name'};
-    $lpr = $opt->{'lpr'}
-  } else {
-    $template = scalar(@_) ? shift : '';
-    if ( scalar(@_) && $_[0]  ) {
-      $agentnums = ref($_[0]) ? shift : [ shift ];
-    }
-    $invoice_from = shift if scalar(@_);
-    $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
-  }
-
   my $cust_main = $self->cust_main;
 
-  return 'N/A' unless ! $agentnums
-                   or grep { $_ == $cust_main->agentnum } @$agentnums;
-
-  return ''
-    unless $cust_main->total_owed_date($self->_date) > $balance_over;
-
-  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
-                    $conf->config('invoice_from', $cust_main->agentnum );
-
-  my %opt = (
-    'template'     => $template,
-    'invoice_from' => $invoice_from,
-    'notice_name'  => ( $notice_name || 'Invoice' ),
-  );
-
   my @invoicing_list = $cust_main->invoicing_list;
 
-  #$self->email_invoice(\%opt)
-  $self->email(\%opt)
+  $self->email($opt)
     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
     && ! $self->invoice_noemail;
 
-  $opt{'lpr'} = $lpr;
-  #$self->print_invoice(\%opt)
-  $self->print(\%opt)
+  $self->print($opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
   #this has never been used post-$ORIGINAL_ISP afaik
-  $self->fax_invoice(\%opt)
+  $self->fax_invoice($opt)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
@@ -1437,16 +1381,17 @@ sub send {
 
 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
 
-Emails this invoice.
+Sends this invoice to the customer's email destination(s).
 
-Options can be passed as a hashref (recommended) or as a list of up to 
-two values for templatename and invoice_from.
+Options must be passed as a hashref.  Positional parameters are no longer
+allowed.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
-I<invoice_from>, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From: 
+address.
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> is the name of the sent document.
 
 =cut
 
@@ -1456,38 +1401,30 @@ sub queueable_email {
   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
     or die "invalid invoice number: " . $opt{invnum};
 
-  my %args = ( 'template' => $opt{template} );
-  $args{$_} = $opt{$_}
-    foreach grep { exists($opt{$_}) && $opt{$_} }
-              qw( invoice_from notice_name no_coupon );
+  my %args = map {$_ => $opt{$_}} 
+             grep { $opt{$_} }
+              qw( invoice_from notice_name no_coupon template );
 
   my $error = $self->email( \%args );
   die $error if $error;
 
 }
 
-#sub email_invoice {
 sub email {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $invoice_from, $notice_name, $no_coupon );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $invoice_from = $opt->{'invoice_from'};
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-    $no_coupon = $opt->{'no_coupon'} || 0;
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $invoice_from = shift if scalar(@_);
-    $notice_name = 'Invoice';
-    $no_coupon = 0;
+  my $opt = shift;
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::email called with positional parameters";
   }
 
-  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
-                    $conf->config('invoice_from', $self->cust_main->agentnum );
+  my $template = $opt->{template};
+  my $from = delete $opt->{invoice_from};
+
+  # this is where we set the From: address
+  $from ||= $self->_agent_invoice_from ||    #XXX should go away
+            $conf->config('invoice_from', $self->cust_main->agentnum );
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
@@ -1497,20 +1434,19 @@ sub email {
       die 'No recipients for customer #'. $self->custnum;
     } else {
       #default: better to notify this person than silence
-      @invoicing_list = ($invoice_from);
+      @invoicing_list = ($from);
     }
   }
 
+  # this is where we set the Subject:
   my $subject = $self->email_subject($template);
 
   my $error = send_email(
     $self->generate_email(
-      'from'        => $invoice_from,
+      'from'        => $from,
       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
       'subject'     => $subject,
-      'template'    => $template,
-      'notice_name' => $notice_name,
-      'no_coupon'   => $no_coupon,
+      %$opt, # template, etc.
     )
   );
   die "can't email invoice: $error\n" if $error;
@@ -1537,12 +1473,12 @@ sub email_subject {
   eval qq("$subject");
 }
 
-=item lpr_data HASHREF | [ TEMPLATE ]
+=item lpr_data HASHREF
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-Options can be passed as a hashref (recommended) or as a single optional value
-for template.
+Options must be passed as a hashref.  Positional parameters are no longer 
+allowed.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1553,31 +1489,21 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 sub lpr_data {
   my $self = shift;
   my $conf = $self->conf;
-  my( $template, $notice_name );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
+  my $opt = shift;
+  if ($opt and !ref($opt)) {
+    # nobody does this anyway
+    die "FS::cust_bill::lpr_data called with positional parameters";
   }
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
-  [ $self->$method( \%opt ) ];
+  [ $self->$method( $opt ) ];
 }
 
-=item print HASHREF | [ TEMPLATE ]
+=item print HASHREF
 
 Prints this invoice.
 
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1585,48 +1511,34 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 
 =cut
 
-#sub print_invoice {
 sub print {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $notice_name, $lpr );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-    $lpr = $opt->{'lpr'}
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
-    $lpr = '';
+  my $opt = shift;
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::print called with positional parameters";
   }
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
+  my $lpr = delete $opt->{lpr};
   if($conf->exists('invoice_print_pdf')) {
     # Add the invoice to the current batch.
-    $self->batch_invoice(\%opt);
+    $self->batch_invoice($opt);
   }
   else {
     do_print(
-      $self->lpr_data(\%opt),
+      $self->lpr_data($opt),
       'agentnum' => $self->cust_main->agentnum,
       'lpr'      => $lpr,
     );
   }
 }
 
-=item fax_invoice HASHREF | [ TEMPLATE ] 
+=item fax_invoice HASHREF
 
 Faxes this invoice.
 
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1638,15 +1550,9 @@ sub fax_invoice {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $notice_name );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
+  my $opt = shift;
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::fax_invoice called with positional parameters";
   }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
@@ -1655,12 +1561,7 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
-  my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
+  my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
@@ -1749,29 +1650,6 @@ sub spool_invoice {
   );
 }
 
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
-  my $self = shift;
-
-  return ''
-    if scalar(
-               grep { $_->owed > 0 } 
-                    qsearch('cust_bill', {
-                      'custnum' => $self->custnum,
-                      #'_date'   => { op=>'>', value=>$self->_date },
-                      'invnum'  => { op=>'>', value=>$self->invnum },
-                    } )
-             );
-    
-  $self->send(@_);
-}
-
 =item send_csv OPTION => VALUE, ...
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 0c50b84..13b0150 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4895,9 +4895,9 @@ sub queueable_print {
   my %opt = @_;
 
   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
-    or die "invalid customer number: " . $opt{custvnum};
+    or die "invalid customer number: " . $opt{custnum};
 
-  my $error = $self->print( $opt{template} );
+  my $error = $self->print( { 'template' => $opt{template} } );
   die $error if $error;
 }
 
diff --git a/FS/FS/invoice_conf.pm b/FS/FS/invoice_conf.pm
new file mode 100644
index 0000000..043cab0
--- /dev/null
+++ b/FS/FS/invoice_conf.pm
@@ -0,0 +1,274 @@
+package FS::invoice_conf;
+
+use strict;
+use base qw( FS::Record FS::Conf );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::invoice_conf - Object methods for invoice_conf records
+
+=head1 SYNOPSIS
+
+  use FS::invoice_conf;
+
+  $record = new FS::invoice_conf \%hash;
+  $record = new FS::invoice_conf { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::invoice_conf object represents a set of localized invoice 
+configuration values.  FS::invoice_conf inherits from FS::Record and FS::Conf,
+and supports the FS::Conf interface.  The following fields are supported:
+
+=over 4
+
+=item confnum - primary key
+
+=item modenum - L<FS::invoice_mode> foreign key
+
+=item locale - locale string (see L<FS::Locales>)
+
+=item notice_name - the title to display on the invoice
+
+=item subject - subject line of the email
+
+=item htmlnotes - "notes" section (HTML)
+
+=item htmlfooter - footer (HTML)
+
+=item htmlsummary - summary header, for invoices in summary format (HTML)
+
+=item htmlreturnaddress - return address (HTML)
+
+=item latexnotes - "notes" section (LaTeX)
+
+=item latexfooter - footer (LaTeX)
+
+=item latexsummary - summary header, for invoices in summary format (LaTeX)
+
+=item latexreturnaddress - return address (LaTeX)
+
+=item latexcoupon - payment coupon section (LaTeX)
+
+=item latexsmallfooter - footer for pages after the first (LaTeX)
+
+=item latextopmargin - top margin
+
+=item latexheadsep - distance from bottom of header to top of body
+
+=item latexaddresssep - distance from top of body to customer address
+
+=item latextextheight - maximum height of invoice body text
+
+=item latexextracouponspace - additional footer space to allow for coupon
+
+=item latexcouponfootsep - distance from bottom of coupon content to top
+of page footer
+
+=item latexcouponamountenclosedsep - distance from coupon balance line to
+"Amount Enclosed" box
+
+=item latexcoupontoaddresssep - distance from "Amount Enclosed" box to 
+coupon mailing address
+
+=item latexverticalreturnaddress - 'Y' to place the return address below 
+the company logo rather than beside it
+
+=item latexcouponaddcompanytoaddress - 'Y' to add the company name to the 
+address on the payment coupon
+
+=item logo_png - company logo, as a PNG, for HTML invoices
+
+=item logo_eps - company logo, as an EPS, for LaTeX invoices
+
+=item lpr - command to print the invoice (passed on stdin as a PDF)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice configuration.  To add it to the database, see 
+L<"insert">.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'invoice_conf'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# slightly special: you can insert/replace the invoice mode this way
+
+sub insert {
+  my $self = shift;
+  if (!$self->modenum) {
+    my $invoice_mode = FS::invoice_mode->new({
+        'modename' => $self->modename,
+        'agentnum' => $self->agentnum,
+    });
+    my $error = $invoice_mode->insert;
+    return $error if $error;
+    $self->set('modenum' => $invoice_mode->modenum);
+  } else {
+    my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+    my $changed = 0;
+    foreach (qw(agentnum modename)) {
+      $changed ||= ($invoice_mode->get($_) eq $self->get($_));
+      $invoice_mode->set($_, $self->get($_));
+    }
+    my $error = $invoice_mode->replace if $changed;
+    return $error if $error;
+  }
+  $self->SUPER::insert(@_);
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error = $self->FS::Record::delete; # not Conf::delete
+  return $error if $error;
+  my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+  if ( $invoice_mode and
+       FS::invoice_conf->count('modenum = '.$invoice_mode->modenum) == 0 ) {
+    $error = $invoice_mode->delete;
+    return $error if $error;
+  }
+  '';
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my $self = shift;
+  my $error = $self->SUPER::replace(@_);
+  return $error if $error;
+
+  my $invoice_mode = FS::invoice_mode->by_key($self->modenum);
+  my $changed = 0;
+  foreach (qw(agentnum modename)) {
+    $changed ||= ($invoice_mode->get($_) eq $self->get($_));
+    $invoice_mode->set($_, $self->get($_));
+  }
+  $error = $invoice_mode->replace if $changed;
+  return $error if $error;
+}
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('confnum')
+    || $self->ut_number('modenum')
+    || $self->ut_textn('locale')
+    || $self->ut_anything('notice_name')
+    || $self->ut_anything('subject')
+    || $self->ut_anything('htmlnotes')
+    || $self->ut_anything('htmlfooter')
+    || $self->ut_anything('htmlsummary')
+    || $self->ut_anything('htmlreturnaddress')
+    || $self->ut_anything('latexnotes')
+    || $self->ut_anything('latexfooter')
+    || $self->ut_anything('latexsummary')
+    || $self->ut_anything('latexcoupon')
+    || $self->ut_anything('latexsmallfooter')
+    || $self->ut_anything('latexreturnaddress')
+    || $self->ut_textn('latextopmargin')
+    || $self->ut_textn('latexheadsep')
+    || $self->ut_textn('latexaddresssep')
+    || $self->ut_textn('latextextheight')
+    || $self->ut_textn('latexextracouponspace')
+    || $self->ut_textn('latexcouponfootsep')
+    || $self->ut_textn('latexcouponamountenclosedsep')
+    || $self->ut_textn('latexcoupontoaddresssep')
+    || $self->ut_flag('latexverticalreturnaddress')
+    || $self->ut_flag('latexcouponaddcompanytoaddress')
+    || $self->ut_anything('logo_png')
+    || $self->ut_anything('logo_eps')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+# hook _config to substitute our own values; let FS::Conf do the rest of 
+# the interface
+
+sub _config {
+  my $self = shift;
+  # if we fall back, we still want FS::Conf to respect our locale
+  $self->{locale} = $self->get('locale');
+  my ($key, $agentnum, $nodefault) = @_;
+  # some fields, but not all, start with invoice_
+  my $colname = $key;
+  if ( $key =~ /^invoice_(.*)$/ ) {
+    $colname = $1;
+  }
+  if ( length($self->get($colname)) ) {
+    return FS::conf->new({ 'name'   => $key,
+                           'value'  => $self->get($colname) });
+  } else {
+    return $self->FS::Conf::_config(@_);
+  }
+}
+
+# disambiguation
+sub set {
+  my $self = shift;
+  $self->FS::Record::set(@_);
+}
+
+sub exists {
+  my $self = shift;
+  $self->FS::Conf::exists(@_);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Template_Mixin>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/invoice_mode.pm b/FS/FS/invoice_mode.pm
new file mode 100644
index 0000000..115dd44
--- /dev/null
+++ b/FS/FS/invoice_mode.pm
@@ -0,0 +1,157 @@
+package FS::invoice_mode;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::invoice_conf;
+
+=head1 NAME
+
+FS::invoice_mode - Object methods for invoice_mode records
+
+=head1 SYNOPSIS
+
+  use FS::invoice_mode;
+
+  $record = new FS::invoice_mode \%hash;
+  $record = new FS::invoice_mode { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::invoice_mode object represents an invoice rendering style.  
+FS::invoice_mode inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 4
+
+=item modenum - primary key
+
+=item agentnum - the agent who owns this invoice mode (can be null)
+
+=item modename - descriptive name for internal use
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice mode.  To add the object to the database, 
+see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'invoice_mode'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('modenum')
+    || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
+    || $self->ut_text('modename')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item invoice_conf [ LOCALE ]
+
+Returns the L<FS::invoice_conf> for this invoice mode, with the specified
+locale.  If there isn't one with that locale, returns the one with null 
+locale.  If that doesn't exist, returns nothing.
+
+=cut
+
+sub invoice_conf {
+  my $self = shift;
+  my $locale = shift;
+  my $invoice_conf;
+  if ( $locale ) {
+    $invoice_conf = qsearchs('invoice_conf', {
+        modenum => $self->modenum,
+        locale  => $locale,
+    });
+  }
+  $invoice_conf ||= qsearchs('invoice_conf', {
+      modenum => $self->modenum,
+      locale  => '',
+  });
+  $invoice_conf;
+}
+
+=item agent
+
+Returns the agent associated with this invoice mode, if any.
+
+=cut
+
+sub agent {
+  my $self = shift;
+  $self->agentnum ? FS::agent->by_key($self->agentnum) : '';
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_event/Action/cust_bill_email.pm b/FS/FS/part_event/Action/cust_bill_email.pm
index 1a3bca4..3331a4c 100644
--- a/FS/FS/part_event/Action/cust_bill_email.pm
+++ b/FS/FS/part_event/Action/cust_bill_email.pm
@@ -9,14 +9,22 @@ sub eventtable_hashref {
   { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode',
+                 },
+  );
+}
+
 sub default_weight { 51; }
 
 sub do_action {
   my( $self, $cust_bill ) = @_;
 
-  #my $cust_main = $self->cust_main($cust_bill);
   my $cust_main = $cust_bill->cust_main;
 
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->email unless $cust_main->invoice_noemail;
 }
 
diff --git a/FS/FS/part_event/Action/cust_bill_print.pm b/FS/FS/part_event/Action/cust_bill_print.pm
index 6b3e6f4..ea6e0aa 100644
--- a/FS/FS/part_event/Action/cust_bill_print.pm
+++ b/FS/FS/part_event/Action/cust_bill_print.pm
@@ -9,6 +9,14 @@ sub eventtable_hashref {
   { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode',
+                 },
+  );
+}
+
 sub default_weight { 51; }
 
 sub do_action {
@@ -17,6 +25,7 @@ sub do_action {
   #my $cust_main = $self->cust_main($cust_bill);
   my $cust_main = $cust_bill->cust_main;
 
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->print;
 }
 
diff --git a/FS/FS/part_event/Action/cust_bill_print_pdf.pm b/FS/FS/part_event/Action/cust_bill_print_pdf.pm
index 6b37f38..6c01d42 100644
--- a/FS/FS/part_event/Action/cust_bill_print_pdf.pm
+++ b/FS/FS/part_event/Action/cust_bill_print_pdf.pm
@@ -9,6 +9,14 @@ sub eventtable_hashref {
   { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode'
+                 },
+  );
+}
+
 sub default_weight { 51; }
 
 sub do_action {
@@ -20,6 +28,7 @@ sub do_action {
   my $opt = { $self->options };
   $opt->{'notice_name'} ||= 'Invoice';
 
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->batch_invoice($opt);
 }
 
diff --git a/FS/FS/part_event/Action/cust_bill_send.pm b/FS/FS/part_event/Action/cust_bill_send.pm
index 587a7c6..c6928dc 100644
--- a/FS/FS/part_event/Action/cust_bill_send.pm
+++ b/FS/FS/part_event/Action/cust_bill_send.pm
@@ -9,11 +9,20 @@ sub eventtable_hashref {
   { 'cust_bill' => 1 };
 }
 
+sub option_fields {
+  (
+    'modenum'     => { label  => 'Invoice mode',
+                       type   => 'select-invoice_mode',
+                     },
+  );
+}
+
 sub default_weight { 50; }
 
 sub do_action {
   my( $self, $cust_bill ) = @_;
 
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->send;
 }
 
diff --git a/FS/FS/part_event/Action/cust_bill_send_agent.pm b/FS/FS/part_event/Action/cust_bill_send_agent.pm
index 670a32c..bbb757b 100644
--- a/FS/FS/part_event/Action/cust_bill_send_agent.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_agent.pm
@@ -7,6 +7,9 @@ sub description {
   'Send invoice (email/print/fax) with alternate template, for specific agents';
 }
 
+# this event is just cust_bill_send_alternate + an implicit (and inefficient)
+# 'agent' condition
+
 sub eventtable_hashref {
   { 'cust_bill' => 1 };
 }
@@ -17,6 +20,9 @@ sub option_fields {
                               type     => 'select-agent',
                               multiple => 1
                             },
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode',
+                 },
     'agent_templatename' => { label    => 'Template',
                               type     => 'select-invoice_template',
                             },
@@ -32,10 +38,15 @@ sub do_action {
   #my $cust_main = $self->cust_main($cust_bill);
   my $cust_main = $cust_bill->cust_main;
 
+  my %agentnums = map { $_=>1 } split(/\s*,\s*/, $self->option('agentnum'));
+  if (keys(%agentnums) and !exists($agentnums{$cust_main->agentnum})) {
+    return;
+  }
+
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->send(
-    $self->option('agent_templatename'),
-    [ split(/\s*,\s*/, $self->option('agentnum') ) ],
-    $self->option('agent_invoice_from'),
+    'template'      => $self->option('agent_templatename'),
+    'invoice_from'  => $self->option('agent_invoice_from'),
   );
 }
 
diff --git a/FS/FS/part_event/Action/cust_bill_send_alternate.pm b/FS/FS/part_event/Action/cust_bill_send_alternate.pm
index cfd9264..fb71a5a 100644
--- a/FS/FS/part_event/Action/cust_bill_send_alternate.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_alternate.pm
@@ -11,6 +11,8 @@ sub eventtable_hashref {
 
 sub option_fields {
   (
+    'modenum'      => { label    => 'Invoice mode',
+                        type     => 'select-invoice_mode' },
     'templatename' => { label    => 'Template',
                         type     => 'select-invoice_template',
                       },
@@ -25,7 +27,8 @@ sub do_action {
   #my $cust_main = $self->cust_main($cust_bill);
   my $cust_main = $cust_bill->cust_main;
 
-  $cust_bill->send( $self->option('templatename') );
+  $cust_bill->set('mode' => $self->option('modenum'));
+  $cust_bill->send({'template' => $self->option('templatename')});
 }
 
 1;
diff --git a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
index 083da8b..c744362 100644
--- a/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_if_newest.pm
@@ -18,6 +18,9 @@ sub eventtable_hashref {
 
 sub option_fields {
   (
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode',
+                 },
     'if_newest_templatename' => { label    => 'Template',
                                   type     => 'select-invoice_template',
                                 },
@@ -29,10 +32,17 @@ sub default_weight { 50; }
 sub do_action {
   my( $self, $cust_bill ) = @_;
 
-  #my $cust_main = $self->cust_main($cust_bill);
-  my $cust_main = $cust_bill->cust_main;
-
-  $cust_bill->send( $self->option('templatename') );
+  my $invnum = $cust_bill->invnum;
+  my $custnum = $cust_bill->custnum;
+  return '' if scalar(
+    grep { $_->owed > 0 }
+      qsearch('cust_bill', {
+          'custnum' => $custnum,
+          'invnum'  => { op=>'>', value=>$invnum },
+        })
+    );
+  $cust_bill->set('mode' => $self->option('modenum'));
+  $cust_bill->send( 'template' => $self->option('templatename') );
 }
 
 1;
diff --git a/FS/FS/part_event/Action/cust_bill_send_reminder.pm b/FS/FS/part_event/Action/cust_bill_send_reminder.pm
index 073bb8f..354f969 100644
--- a/FS/FS/part_event/Action/cust_bill_send_reminder.pm
+++ b/FS/FS/part_event/Action/cust_bill_send_reminder.pm
@@ -11,9 +11,13 @@ sub eventtable_hashref {
 
 sub option_fields {
   (
+    'modenum' => {  label => 'Invoice mode',
+                    type  => 'select-invoice_mode',
+                 },
+    # totally unnecessary, since the invoice mode can set notice_name and lpr,
+    # but for compatibility...
     'notice_name' => 'Reminder name',
-    #'notes'      => { 'label' => 'Reminder notes' }, 
-    #include standard notes?  no/prepend/append
+    #'notes'      => { 'label' => 'Reminder notes' },  # invoice mode does this
     'lpr'         => 'Optional alternate print command',
   );
 }
@@ -23,9 +27,7 @@ sub default_weight { 50; }
 sub do_action {
   my( $self, $cust_bill ) = @_;
 
-  #my $cust_main = $self->cust_main($cust_bill);
-  #my $cust_main = $cust_bill->cust_main;
-
+  $cust_bill->set('mode' => $self->option('modenum'));
   $cust_bill->send({
     'notice_name' => $self->option('notice_name'),
     'lpr'         => $self->option('lpr'),
diff --git a/FS/FS/part_event/Action/cust_statement_send.pm b/FS/FS/part_event/Action/cust_statement_send.pm
index 74cc48c..67a94aa 100644
--- a/FS/FS/part_event/Action/cust_statement_send.pm
+++ b/FS/FS/part_event/Action/cust_statement_send.pm
@@ -19,7 +19,8 @@ sub default_weight {
 sub do_action {
   my( $self, $cust_statement ) = @_;
 
-  $cust_statement->send( 'statement' ); #XXX configure
+  $cust_statement->send( 'template' => 'statement' ); #XXX configure
+                                                      #XXX use an invoice mode?
 
 }
 
diff --git a/FS/MANIFEST b/FS/MANIFEST
index a3b11f7..339965e 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -720,3 +720,7 @@ FS/svc_alarm.pm
 t/svc_alarm.t
 FS/cable_model.pm
 t/cable_model.t
+FS/invoice_mode.pm
+t/invoice_mode.t
+FS/invoice_conf.pm
+t/invoice_conf.t
diff --git a/FS/t/invoice_conf.t b/FS/t/invoice_conf.t
new file mode 100644
index 0000000..b707fa3
--- /dev/null
+++ b/FS/t/invoice_conf.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::invoice_conf;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/invoice_mode.t b/FS/t/invoice_mode.t
new file mode 100644
index 0000000..5f945f0
--- /dev/null
+++ b/FS/t/invoice_mode.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::invoice_mode;
+$loaded=1;
+print "ok 1\n";
diff --git a/bin/generate-table-module b/bin/generate-table-module
index b536360..37a5812 100755
--- a/bin/generate-table-module
+++ b/bin/generate-table-module
@@ -95,10 +95,10 @@ close TEST;
 # add them to MANIFEST
 ###
 
-#system('cvs edit FS/MANIFEST');
-
 open(MANIFEST,">>FS/MANIFEST") or die $!;
 print MANIFEST "FS/$table.pm\n",
                "t/$table.t\n";
 close MANIFEST;
 
+system("git add FS/FS/$table.pm FS/t/$table.t");
+
diff --git a/httemplate/browse/invoice_conf.html b/httemplate/browse/invoice_conf.html
new file mode 100644
index 0000000..c8fd1bf
--- /dev/null
+++ b/httemplate/browse/invoice_conf.html
@@ -0,0 +1,70 @@
+<& elements/browse.html,
+  'title'         => 'Invoice modes',
+  'name_singular' => 'configuration',
+  'menubar'       => \@menubar,
+  'query'         => {
+                      'select'    => $select,
+                      'table'     => 'invoice_conf',
+                      'addl_from' => ' JOIN invoice_mode USING (modenum)',
+                      'extra_sql' => ' WHERE '.$curuser->agentnums_sql(
+                        'table'       => 'invoice_mode',
+                        'null_right'  => ['Edit global templates'],
+                      ),
+                      'order_by'  => q( ORDER BY modename asc, COALESCE(locale,'') asc),
+                     },
+  'count_query'   => 'SELECT COUNT(*) FROM invoice_conf JOIN invoice_mode USING (modenum)',
+  'header'        => [ 'Name', 'Agent', 'Locale', 'Overrides', ],
+  'fields'        => [  $modename,
+                        $agent,
+                        $locale_label,
+                        $overrides,
+                     ],
+  'align'         => 'llcl',
+  'links'         => [ '', '', $link ],
+  'disable_maxselect' => 1,
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View templates', 'View global templates',
+                                  'Edit templates', 'Edit global templates', ]);
+
+my @overrides = grep {$_ ne 'modenum' and $_ ne 'confnum'} FS::invoice_conf->fields;                                 
+my $select = join(',', 'modename', 'agentnum', 'confnum', 'invoice_conf.*');
+
+my @menubar = ();
+if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) {
+  push @menubar, 'Add a new invoice mode' => $p.'edit/invoice_conf.html';
+}
+
+my $locale_style = 'font-size:0.8em; padding:3px; background-color:';
+
+my $last_modenum = 0;
+my $modename = sub {
+  return '' if $_[0]->modenum == $last_modenum;
+  $_[0]->modename;
+};
+
+my $agent = sub {
+  return '' if $_[0]->modenum == $last_modenum;
+  $last_modenum = $_[0]->modenum;
+  $_[0]->agentnum ? FS::agent->by_key($_[0]->agentnum)->agent : '(global)';
+};
+
+my $locale_label = sub {
+  my $l = $_[0]->locale;
+  $l ? +{ FS::Locales->locale_info($l) }->{'label'} : '(default)';
+};
+
+my $overrides = sub {
+  my $invoice_conf = shift;
+  [ map { [ { data => $_ } ] }
+    grep { length $invoice_conf->get($_) }
+    @overrides
+  ],
+};
+
+my $link = [ $p.'edit/invoice_conf.html?', 'confnum' ];
+</%init>
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 3270f04..051788c 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -331,6 +331,7 @@ Example:
 %     qw( width height config ),                           #htmlarea
 %     qw( alt_format ),                                    #select-cust_location
 %     qw( classnum ),                                   # select-inventory_item
+%     qw( aligned ),                                    # columnstart
 %   ;
 %
 %   #select-table
diff --git a/httemplate/edit/invoice_conf.html b/httemplate/edit/invoice_conf.html
new file mode 100644
index 0000000..b7b3a4e
--- /dev/null
+++ b/httemplate/edit/invoice_conf.html
@@ -0,0 +1,296 @@
+<& elements/edit.html,
+    'body_etc'          => $body_etc,
+    'name_singular'     => 'invoice configuration',
+    'table'             => 'invoice_conf',
+    'viewall_dir'       => 'browse',
+    'fields'            => \@fields,
+    'labels'            => \%labels,
+    'new_callback'      => \&new_callback,
+    'edit_callback'     => \&edit_callback,
+    'error_callback'    => \&error_callback,
+    'html_init'         => \&html_init,
+    'html_table_bottom' => \&html_table_bottom,
+    'html_bottom'       => '</DIV>', # close tablebreak-tabs
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+# ???
+die "access denied"
+  unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+
+my $body_etc = '';
+$body_etc = q!onload="document.getElementById('locale').onchange()"!
+  if $cgi->param('locale') eq 'new';
+
+my $modenum = $cgi->param('modenum');
+my $mode = $modenum ? qsearchs('invoice_mode', { modenum => $modenum }) : '';
+
+my %textarea = (type => 'textarea', rows => 10, cols => 40);
+my @fields = (
+      { field => 'modenum', type => 'hidden' },
+      { field => 'agentnum',
+        type  => 'select-agent',
+      },
+      { field => 'modename',  size=>60, },
+      { type  => 'tablebreak-tabs',
+        include_opt_callback => \&menubar_opt_callback,
+      },
+      { field => 'locale', type => 'hidden' },
+      { field => 'notice_name',   size=>60, },
+      { field => 'subject',       size=>60, },
+      { field => 'lpr',           size=>60, },
+
+      { type  => 'columnstart', aligned => 1 },
+      { type  => 'title', value => '<BR>' },
+      map ( { +{ type => 'justtitle', value => $_ } }
+        'Notes',
+        'Footer',
+        'Summary header',
+        'Return address',
+        'Coupon',
+        'Small footer',
+        'Top margin',
+        'Header separation',
+        'Address separation',
+        'Text height',
+        'Coupon height',
+        'Footer separation',
+      ),
+
+      { type  => 'columnnext' },
+      { type  => 'title', value => 'LaTeX' },
+      { field => 'latexnotes',          %textarea },
+      { field => 'latexfooter',         %textarea },
+      { field => 'latexsummary',        %textarea },
+      { field => 'latexreturnaddress',  %textarea },
+      { field => 'latexcoupon',         %textarea },
+      { field => 'latexsmallfooter',    %textarea },
+      { field => 'latextopmargin',                size => 16 },
+      { field => 'latexheadsep',                  size => 16 },
+      { field => 'latexaddresssep',               size => 16 },
+      { field => 'latextextheight',               size => 16 },
+      { field => 'latexextracouponspace',         size => 16 },
+      { field => 'latexcouponfootsep',            size => 16 },
+      # are these still used?
+      #{ field => 'latexcouponamountenclosedsep',  size => 16 },
+      #{ field => 'latexverticalreturnaddress',    type => 'checkbox' },
+      #{ field => 'latexcouponaddcompanytoaddress',type => 'checkbox' },
+      # logo -- implement if someone really needs it...
+
+      { type  => 'columnnext' },
+      { type  => 'title', value => 'HTML' },
+      { field => 'htmlnotes',           %textarea }, #htmlarea?
+      { field => 'htmlfooter',          %textarea },
+      { field => 'htmlsummary',         %textarea },
+      { field => 'htmlreturnaddress',   %textarea },
+      # logo
+
+      { type  => 'columnend' },
+);
+
+my %labels = (
+  'confnum'             => 'Configuration',
+  'locale'              => 'Locale',
+  'agentnum'            => 'Agent',
+  'modename'            => 'Mode name',
+  'notice_name'         => 'Notice name',
+  'subject'             => 'Email Subject: header',
+  'lpr'                 => 'Alternate lpr command',
+
+  map { $_ => '' } (qw(
+    latexnotes
+    latexfooter
+    latexsummary
+    latexreturnaddress
+    latexcoupon
+    latexsmallfooter
+    latextopmargin
+    latexheadsep
+    latexaddresssep
+    latextextheight
+    latexextracouponspace
+    latexcouponfootsep
+    htmlnotes
+    htmlfooter
+    htmlsummary
+    htmlreturnaddress
+    logo_png
+    logo_eps
+  ) ),
+
+); 
+
+sub get_invoice_mode { # because we can't quite use agent_virt here
+  my $modenum = shift;
+  qsearchs({
+    'table'     => 'invoice_mode',
+    'hashref'   => { 'modenum' => $modenum },
+    'extra_sql' => ' AND '.
+      $FS::CurrentUser::CurrentUser->agentnums_sql(
+        'null_right' => 'Edit global templates',
+        'viewall_right' => 'Edit global templates' ),
+  });
+};
+
+sub error_callback {
+  my ($cgi, $object) = @_;
+  foreach (qw(modename agentnum)) {
+    $object->set($_, $cgi->param($_));
+  }
+  if ($object->confnum) {
+    return edit_callback(@_);
+  } else {
+    return new_callback(@_);
+  }
+}
+
+sub new_callback {
+  my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+  my $modenum;
+  if ( $cgi->param('locale') =~ /^(\w+)$/ ) {
+    $object->set('locale' => $1);
+  }
+
+  if ( $cgi->param('modenum') =~ /^(\d+)$/ ) {
+    $modenum = $1; # we're adding a locale to an existing mode
+    $object->set('modenum' => $modenum);
+    my $invoice_mode = get_invoice_mode($modenum)
+      or die "invoice mode $modenum not found";
+
+    $object->set('modename', $invoice_mode->modename);
+    $object->set('agentnum', $invoice_mode->agentnum);
+
+    # also, need to select a locale
+    # make a list of available locales
+    my %existing_locales = map { $_->locale }
+                          qsearch('invoice_conf', { modenum => $modenum });
+
+    my @locales = grep { !exists($existing_locales{$_}) } 
+                         FS::Conf->new->config('available-locales');
+    my %labels;
+    foreach (@locales) {
+      my %info = FS::Locales->locale_info($_);
+      $labels{$_} = $info{'label'};
+    }
+    unshift @locales, 'new';
+    $labels{'new'} = 'Select language';
+
+    # insert a field def
+    my $i = 0;
+    $i++ until ( $fields_arrayref->[$i]->{'field'} eq 'locale' );
+    my $locale_field = $fields_arrayref->[$i];
+
+    my $onchange_locale = "document.getElementById('submit').disabled = 
+    (this.options[this.selectedIndex].value == 'new');";
+
+    %$locale_field = (
+      field   => 'locale',
+      type    => 'select',
+      options => \@locales,
+      labels  => \%labels,
+      curr_value  => 'new',
+      onchange    => $onchange_locale,
+    );
+
+  } # otherwise it's a completely new mode, so the locale is default
+
+}
+
+sub edit_callback {
+  # massive false laziness with msg_template UI
+  my ($cgi, $object, $fields_arrayref, $opt_hashref) = @_;
+
+  # a little different here in that we treat the content object
+  # as "primary" (this is edit/invoice_conf.html, etc.)
+  # so all we need from the invoice_mode is its name
+  # (and agent identity)
+  my $modenum = $object->modenum;
+  my $invoice_mode = get_invoice_mode($modenum)
+    or die "invoice mode $modenum not found";
+  $object->set('modename', $invoice_mode->modename);
+  $object->set('agentnum', $invoice_mode->agentnum);
+
+}
+
+sub menubar_opt_callback {
+  my $object = shift;
+  my $modenum = $object->modenum or return; 
+  my (@tabs, @options, %labels);
+  my $display_new = 0;
+  my $selected = '';
+  foreach my $l ('', FS::Conf->new->config('available-locales')) {
+    my $invoice_conf =
+      qsearchs('invoice_conf', { modenum => $modenum, locale => $l });
+    if ( $invoice_conf ) {
+      my %info = FS::Locales->locale_info($l) if $l;
+      my $label = $info{'label'} || mt('Default');
+      push @tabs, $label, $invoice_conf->confnum;
+      $selected = $label if $object->locale eq $l;
+    }
+    else {
+      $display_new = 1; # there is at least one unused locale left
+    }
+  }
+  push @tabs, mt('New'), "modenum=$modenum;locale=new" if $display_new;
+  $selected = mt('New') if $object->locale eq 'new';
+  $selected ||= mt('Default');
+  (
+    'url_base' => $cgi->url() . '?',
+    'selected' => $selected,
+    'tabs'     => \@tabs
+  );
+}
+
+sub html_init {
+q!
+<STYLE>
+.fstabcontainer th { vertical-align: middle; text-align: center }
+</STYLE>
+!
+}
+
+sub html_table_bottom {
+  my $object = shift;
+  my $locale = '';
+  my $modenum = '';
+
+  if ($object->locale =~ /^(\w+)$/) {
+    $locale = $1;
+  }
+  if ($object->modenum =~ /^(\d+)$/) {
+    $modenum = $1;
+  }
+  my $html;
+  my $show_delete = 1;
+  # don't allow the default locale to be removed unless it's the last one
+  # in the mode
+  $show_delete = 0 if (
+    $locale eq 'new' or
+    $modenum eq '' or
+    ($locale eq '' and
+     FS::invoice_conf->count("modenum = $modenum and locale is not null") > 0
+    )
+  );
+
+  if ( $show_delete ) {    
+    # set up a delete link
+    my $confnum = $object->confnum;
+    my $url = $p."misc/delete-invoice_conf.html?$confnum";
+    my $link = qq!<A HREF="javascript:areyousure('$url','Really delete this configuration?')">! .
+      'Delete this configuration' .
+      '</A>';
+    $html = qq!<TR><TD></TD>
+      <TD STYLE="font-style: italic; font-size: small">$link</TD></TR>
+      <SCRIPT TYPE="text/javascript">
+      function areyousure(url, message) {
+          if (confirm(message)) window.location.href = url;
+      }
+      </SCRIPT>
+      !;
+  }
+  $html;
+}
+
+</%init>
diff --git a/httemplate/edit/process/invoice_conf.html b/httemplate/edit/process/invoice_conf.html
new file mode 100644
index 0000000..1d45e12
--- /dev/null
+++ b/httemplate/edit/process/invoice_conf.html
@@ -0,0 +1,21 @@
+<& elements/process.html,
+  'table'       => 'invoice_conf',
+  'viewall_dir' => 'browse',
+  'fields'      => [ FS::invoice_conf->fields, 'modename', 'agentnum' ],
+  'precheck_callback' => \&precheck_callback,
+&>
+<%init>
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right(['Edit templates','Edit global templates']);
+
+sub precheck_callback {
+  my $cgi = shift;
+  $cgi->param('locale') =~ /^(\w*)$/;
+  my $locale = $1;
+  return mt('Language required') if $locale eq 'new'; # the user didn't choose
+  die "unknown locale $locale" if ( $locale and 
+                                    !FS::Locales->locale_info($locale) );
+}
+# invoice_conf itself knows to create/update invoice_mode if necessary,
+# so nothing special here
+</%init>
diff --git a/httemplate/elements/columnstart.html b/httemplate/elements/columnstart.html
index be37d81..1ffbcb9 100644
--- a/httemplate/elements/columnstart.html
+++ b/httemplate/elements/columnstart.html
@@ -1,6 +1,81 @@
+<%doc>
+<table>
+  <& /elements/columnstart.html &>
+    <tr> ... </tr>
+    <tr> ... </tr>
+  <& /elements/columnnext.html &>
+    ...
+  <& /elements/columnend.html &>
+</table>
+
+Pass 'aligned' => 1 to have corresponding rows in the columns line up.
+</%doc>
+% my $id = sprintf('table%08d', rand(100000000));
 <TR>
   <TD CLASS="background" COLSPAN=99>
-    <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0>
+    <TABLE BORDER=0 CELLSPACING=0 CELLPADDING=0 id="<%$id%>">
       <TR>
         <TD VALIGN="top">
           <TABLE BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
+% if ( $aligned ) {
+%# Instead of changing all the tr-* elements to sometimes output table
+%# cells without wrapping them in a row, we're just going to completely
+%# rebuild the table on the client side.
+<script type="text/javascript">
+<&| onload.js &>
+  var table = document.getElementById('<%$id%>'); // has one row, always
+  var rows = []; // row contents, each containing 
+  var n_rows = []; // rows in each subtable
+  var n_cols = []; // cols in each subtable
+  var total_rows = 0; // max(n_rows)
+  for(var i=0; i < table.rows[0].cells.length; i++) {
+    // these are cells created by columnstart/columnnext
+    // each contains a table, and nothing else
+    var subtable = table.rows[0].cells[i].children[0];
+    n_rows[i] = subtable.rows.length;
+    if ( total_rows < n_rows[i] ) {
+      total_rows = n_rows[i];
+    }
+    n_cols[i] = 0;
+    var subrows = []; // the rows of this table
+    for(var j=0; j < n_rows[i]; j++) {
+      // these are the actual tr-* rows within the table, and 
+      // can contain multiple cells
+      subrows[j] = [];
+      var tr = subtable.rows[j];
+      if ( n_cols[i] < tr.cells.length ) {
+        n_cols[i] = tr.cells.length;
+      }
+      for(var k=0; k < tr.cells.length; k++) {
+        subrows[j][k] = tr.cells[k];
+      }
+    } // for(j)
+    rows[i] = subrows;
+  } // for(i)
+  var new_table = document.createElement('TABLE');
+  for (var j = 0; j < total_rows; j++) {
+    var tr = document.createElement('TR');
+    for (var i = 0; i < rows.length; i++) { // subtables
+      var k = 0; // subrow position
+      if ( j < n_rows[i] ) { // then subtable i has this row
+        for (k = 0; k < rows[i][j].length; k++) { // cells
+          tr.appendChild(rows[i][j][k]);
+        }
+      } // else k is just 0
+      if ( k < n_cols[i] ) { // then we need a spacer
+        var spacer = document.createElement('TD');
+        spacer.setAttribute('colspan', n_cols[i] - k);
+        tr.appendChild(spacer);
+      }
+    } // for(i); subtables
+    // tr is complete
+    new_table.appendChild(tr);
+  } // for(j); rows
+  table.parentNode.insertBefore( new_table, table );
+  table.parentNode.removeChild(table);
+</&>
+</script>
+% } # if $aligned
+<%args>
+$aligned => 0
+</%args>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 8cbbd17..edb5e81 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -600,6 +600,7 @@ $config_billing{'Billing events'} = [ $fsurl.'browse/part_event.html', 'Billing
     || $curuser->access_right('Edit global billing events');
 if ( $curuser->access_right('Configuration') ) {
   #$config_billing{'Invoice events'}         = [ $fsurl.'browse/part_bill_event.cgi', 'Deprecated, old-style actions for overdue invoices' ];
+  $config_billing{'Invoice configurations'}      = [ $fsurl.'browse/invoice_conf.html', 'Adjust invoice settings for special-purpose notices' ];
   $config_billing{'Invoice templates'}      = [ $fsurl.'browse/invoice_template.html', 'Edit templates for HTML, plaintext and typeset invoices' ];
   $config_billing{'separator'} = ''; #its a separator!
   $config_billing{'Prepaid cards'}          = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ];
diff --git a/httemplate/elements/tr-select-invoice_mode.html b/httemplate/elements/tr-select-invoice_mode.html
new file mode 100644
index 0000000..3dccdcc
--- /dev/null
+++ b/httemplate/elements/tr-select-invoice_mode.html
@@ -0,0 +1,10 @@
+<& tr-select-table.html,
+  'label'       => 'Invoice mode',
+  'table'       => 'invoice_mode',
+  'field'       => 'modenum',
+  'name_col'    => 'modename',
+  'agent_virt'  => 1,
+  'agent_null'  => 1,
+  'empty_label' => '(none)',
+  @_
+&>
diff --git a/httemplate/misc/delete-invoice_conf.html b/httemplate/misc/delete-invoice_conf.html
new file mode 100644
index 0000000..6cc6ddc
--- /dev/null
+++ b/httemplate/misc/delete-invoice_conf.html
@@ -0,0 +1,19 @@
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+  unless $curuser->access_right(['Edit templates', 'Edit global templates']);
+
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ or die "bad confnum";
+my $invoice_conf = FS::invoice_conf->by_key($1)
+  or die "couldn't find invoice_conf #$1";
+if ( !$curuser->access_right('Edit global templates') ) {
+  my $agentnum = FS::invoice_mode->by_key($invoice_conf->modenum)->agentnum;
+  die "access denied"
+    unless $curuser->agentnums_href->{$agentnum};
+}
+
+my $error = $invoice_conf->delete; # may also delete the invoice_mode
+my $url = $p.'browse/invoice_conf.html';
+</%init>
+<% $cgi->redirect($url) %>
diff --git a/httemplate/misc/email-invoice.cgi b/httemplate/misc/email-invoice.cgi
index 269722f..b24e042 100755
--- a/httemplate/misc/email-invoice.cgi
+++ b/httemplate/misc/email-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
 my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 die "Can't find invoice!\n" unless $cust_bill;
 
-$cust_bill->email($template); 
+$cust_bill->email({ 'template' => $template }); 
 
 my $custnum = $cust_bill->getfield('custnum');
 
diff --git a/httemplate/misc/fax-invoice.cgi b/httemplate/misc/fax-invoice.cgi
index 2591fce..f72fc7e 100755
--- a/httemplate/misc/fax-invoice.cgi
+++ b/httemplate/misc/fax-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
 my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 die "Can't find invoice!\n" unless $cust_bill;
 
-$cust_bill->fax_invoice($template);
+$cust_bill->fax_invoice({ 'template' => $template });
 
 my $custnum = $cust_bill->getfield('custnum');
 
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
index aeef687..5ce6e76 100755
--- a/httemplate/misc/print-invoice.cgi
+++ b/httemplate/misc/print-invoice.cgi
@@ -12,7 +12,7 @@ my $invnum = $3;
 my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 die "Can't find invoice!\n" unless $cust_bill;
 
-$cust_bill->print($template);
+$cust_bill->print({ 'template' => $template});
 
 my $custnum = $cust_bill->getfield('custnum');
 
diff --git a/httemplate/misc/send-invoice.cgi b/httemplate/misc/send-invoice.cgi
index 32dfe27..08dd0e0 100644
--- a/httemplate/misc/send-invoice.cgi
+++ b/httemplate/misc/send-invoice.cgi
@@ -13,6 +13,10 @@ my $invnum      = $cgi->param('invnum');
 my $template    = $cgi->param('template');
 my $notice_name = $cgi->param('notice_name') if $cgi->param('notice_name');
 my $method      = $cgi->param('method');
+my $mode;
+if ( $cgi->param('mode') =~ /^(\d+)$/ ) {
+  $mode = $1;
+}
 
 $method .= '_invoice' if $method eq 'fax'; #!
 
@@ -21,6 +25,7 @@ die "unknown method $method" unless $method{$method};
 my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
 die "Can't find invoice!\n" unless $cust_bill;
 
+$cust_bill->set('mode' => $mode) if $mode;
 $cust_bill->$method({ 'template'    => $template,
                       'notice_name' => $notice_name,
                    }); 
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
index 95ce60b..4822ab7 100755
--- a/httemplate/view/cust_bill.cgi
+++ b/httemplate/view/cust_bill.cgi
@@ -104,12 +104,35 @@
 % my $br = 0;
 % if ( $cust_bill->num_cust_event ) { $br++;
 <A HREF="<%$p%>search/cust_event.html?invnum=<% $cust_bill->invnum %>">( <% mt('View invoice events') |h %> )</A> 
-% } 
+% }
 
 % if ( $cust_bill->num_cust_bill_event ) { $br++;
 <A HREF="<%$p%>search/cust_bill_event.cgi?invnum=<% $cust_bill->invnum %>">( <% mt('View deprecated, old-style invoice events') |h %> )</A> 
 % }
 
+% my @modes = grep {! $_->disabled} 
+%   $cust_bill->cust_main->agent->invoice_modes;
+% if ( @modes ) {
+( <% mt('View as:') %>
+<FORM STYLE="display:inline" ACTION="<% $cgi->url %>" METHOD="GET">
+<INPUT NAME="invnum" VALUE="<% $invnum %>" TYPE="hidden">
+<& /elements/select-table.html,
+  table       => 'invoice_mode',
+  field       => 'mode',
+  curr_value  => scalar($cgi->param('mode')),
+  records     => \@modes,
+  name_col    => 'modename',
+  onchange    => 'change_invoice_mode',
+  empty_label => '(default)',
+&> )
+<SCRIPT TYPE="text/javascript">
+function change_invoice_mode(obj) {
+  obj.form.submit();
+}
+</SCRIPT>
+% $br++;
+% }
+
 <% $br ? '<BR><BR>' : '' %>
 
 % if ( $conf->exists('invoice_html') ) { 
@@ -126,7 +149,9 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 die "access denied"
   unless $curuser->access_right('View invoices');
 
-my( $invnum, $template, $notice_name );
+my $conf = FS::Conf->new;
+
+my( $invnum, $mode, $template, $notice_name );
 my($query) = $cgi->keywords;
 if ( $query =~ /^((.+)-)?(\d+)$/ ) {
   $template = $2;
@@ -136,10 +161,9 @@ if ( $query =~ /^((.+)-)?(\d+)$/ ) {
   $invnum = $cgi->param('invnum');
   $template = $cgi->param('template');
   $notice_name = $cgi->param('notice_name');
+  $mode = $cgi->param('mode');
 }
 
-my $conf = new FS::Conf;
-
 my %opt = (
   'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
   'template'      => $template,
@@ -163,10 +187,13 @@ my $cust_bill = qsearchs({
 });
 die "Invoice #$invnum not found!" unless $cust_bill;
 
+$cust_bill->set('mode' => $mode);
+
 my $custnum = $cust_bill->custnum;
 my $display_custnum = $cust_bill->cust_main->display_custnum;
 
 my $link = "invnum=$invnum";
+$link .= ';mode=' . $mode if $mode;
 $link .= ';template='. uri_escape($template) if $template;
 $link .= ';notice_name='. $notice_name if $notice_name;
 
diff --git a/httemplate/view/cust_statement.html b/httemplate/view/cust_statement.html
index 3e1345e..5d37b31 100755
--- a/httemplate/view/cust_statement.html
+++ b/httemplate/view/cust_statement.html
@@ -35,10 +35,10 @@
 
 % if ( $conf->exists('invoice_html') ) { 
 
-  <% join('', $cust_statement->print_html('', $templatename) ) %>
+  <% join('', $cust_statement->print_html('template' => $templatename) ) %>
 % } else { 
 
-  <PRE><% join('', $cust_statement->print_text('', $templatename) ) %></PRE>
+  <PRE><% join('', $cust_statement->print_text('template' => $templatename) ) %></PRE>
 % } 
 
 <% include('/elements/footer.html') %>
diff --git a/httemplate/view/elements/cust_bill-typeset b/httemplate/view/elements/cust_bill-typeset
index 00f503f..778e538 100644
--- a/httemplate/view/elements/cust_bill-typeset
+++ b/httemplate/view/elements/cust_bill-typeset
@@ -6,7 +6,7 @@ die "access denied"
 
 my $type = shift;
 
-my( $invnum, $template, $notice_name );
+my( $invnum, $mode, $template, $notice_name );
 my($query) = $cgi->keywords;
 if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
   $template = $2;
@@ -16,7 +16,8 @@ if ( $query =~ /^((.+)-)?(\d+)(.pdf)?$/ ) { #probably not necessary anymore?
   $invnum = $cgi->param('invnum');
   $invnum =~ s/\.pdf//i; #probably not necessary anymore
   $template = $cgi->param('template');
-  $notice_name = ( $cgi->param('notice_name') || 'Invoice' );
+  $notice_name = $cgi->param('notice_name');
+  $mode = $cgi->param('mode');
 }
 
 my $conf = new FS::Conf;
@@ -36,6 +37,8 @@ my $cust_bill = qsearchs({
 });
 die "Invoice #$invnum not found!" unless $cust_bill;
 
+$cust_bill->set(mode => $mode);
+
 my $method = "print_$type";
 
 my $content = $cust_bill->$method(\%opt);

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Mason.pm                                     |    2 +
 FS/FS/Schema.pm                                    |   45 +++
 FS/FS/Template_Mixin.pm                            |  117 +++++---
 FS/FS/agent.pm                                     |   17 ++
 FS/FS/cust_bill.pm                                 |  274 +++++-------------
 FS/FS/cust_main.pm                                 |    4 +-
 FS/FS/invoice_conf.pm                              |  274 ++++++++++++++++++
 FS/FS/invoice_mode.pm                              |  157 +++++++++++
 FS/FS/part_event/Action/cust_bill_email.pm         |   10 +-
 FS/FS/part_event/Action/cust_bill_print.pm         |    9 +
 FS/FS/part_event/Action/cust_bill_print_pdf.pm     |    9 +
 FS/FS/part_event/Action/cust_bill_send.pm          |    9 +
 FS/FS/part_event/Action/cust_bill_send_agent.pm    |   17 +-
 .../part_event/Action/cust_bill_send_alternate.pm  |    5 +-
 .../part_event/Action/cust_bill_send_if_newest.pm  |   18 +-
 FS/FS/part_event/Action/cust_bill_send_reminder.pm |   12 +-
 FS/FS/part_event/Action/cust_statement_send.pm     |    3 +-
 FS/MANIFEST                                        |    4 +
 FS/t/invoice_conf.t                                |    5 +
 FS/t/invoice_mode.t                                |    5 +
 bin/generate-table-module                          |    4 +-
 httemplate/browse/invoice_conf.html                |   70 +++++
 httemplate/edit/elements/edit.html                 |    1 +
 httemplate/edit/invoice_conf.html                  |  296 ++++++++++++++++++++
 httemplate/edit/process/invoice_conf.html          |   21 ++
 httemplate/elements/columnstart.html               |   77 +++++-
 httemplate/elements/menu.html                      |    1 +
 httemplate/elements/tr-select-invoice_mode.html    |   10 +
 httemplate/misc/delete-invoice_conf.html           |   19 ++
 httemplate/misc/email-invoice.cgi                  |    2 +-
 httemplate/misc/fax-invoice.cgi                    |    2 +-
 httemplate/misc/print-invoice.cgi                  |    2 +-
 httemplate/misc/send-invoice.cgi                   |    5 +
 httemplate/view/cust_bill.cgi                      |   35 ++-
 httemplate/view/cust_statement.html                |    4 +-
 httemplate/view/elements/cust_bill-typeset         |    7 +-
 36 files changed, 1284 insertions(+), 268 deletions(-)
 create mode 100644 FS/FS/invoice_conf.pm
 create mode 100644 FS/FS/invoice_mode.pm
 create mode 100644 FS/t/invoice_conf.t
 create mode 100644 FS/t/invoice_mode.t
 create mode 100644 httemplate/browse/invoice_conf.html
 create mode 100644 httemplate/edit/invoice_conf.html
 create mode 100644 httemplate/edit/process/invoice_conf.html
 create mode 100644 httemplate/elements/tr-select-invoice_mode.html
 create mode 100644 httemplate/misc/delete-invoice_conf.html




More information about the freeside-commits mailing list