[freeside-commits] branch FREESIDE_3_BRANCH updated. 1d6a32338660e3d7202faa7e4ce14736b4569a48

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


The branch, FREESIDE_3_BRANCH has been updated
       via  1d6a32338660e3d7202faa7e4ce14736b4569a48 (commit)
      from  c64ff892b9067f7aa719e78b20379d3274907946 (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 1d6a32338660e3d7202faa7e4ce14736b4569a48
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Sep 27 17:29:17 2013 -0700

    invoice configurations, #24723

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index a946ebf..9c6a0fb 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -342,6 +342,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 9927eb3..05699de 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4208,6 +4208,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 68a5912..b46d447 100644
--- a/FS/FS/agent.pm
+++ b/FS/FS/agent.pm
@@ -354,6 +354,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 7dc6ac4..ec13cb9 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4905,9 +4905,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 0ef0fa3..ed1a0cd 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -698,3 +698,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 d01fe72..cb8c3b2 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -330,6 +330,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 0d446d9..c5d8f27 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -593,6 +593,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{'Prepaid cards'}          = [ $fsurl.'search/prepay_credit.html', 'View outstanding cards, generate new cards' ];
   $config_billing{'Call rates and regions'} = [ \%config_billing_rates, 'Manage rate plans, regions and prefixes for VoIP and call billing' ];
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