RT#14829: automatic payments triggered by bill now show up as Payment by fs_queue
[freeside.git] / FS / FS / Conf.pm
index 091070e..a22e236 100644 (file)
@@ -1,6 +1,9 @@
 package FS::Conf;
 
 package FS::Conf;
 
-use vars qw($base_dir @config_items @base_items @card_types $DEBUG);
+use strict;
+use vars qw( $base_dir @config_items @base_items @card_types $DEBUG
+             $conf_cache $conf_cache_enabled
+           );
 use Carp;
 use IO::File;
 use File::Basename;
 use Carp;
 use IO::File;
 use File::Basename;
@@ -8,18 +11,19 @@ use MIME::Base64;
 use Locale::Currency;
 use FS::ConfItem;
 use FS::ConfDefaults;
 use Locale::Currency;
 use FS::ConfItem;
 use FS::ConfDefaults;
-use FS::Conf_compat17;
 use FS::Locales;
 use FS::payby;
 use FS::conf;
 use FS::Record qw(qsearch qsearchs);
 use FS::Locales;
 use FS::payby;
 use FS::conf;
 use FS::Record qw(qsearch qsearchs);
-use FS::UID qw(dbh datasrc use_confcompat);
+use FS::UID qw(dbh datasrc);
 use FS::Misc::Invoicing qw( spool_formats );
 
 $base_dir = '%%%FREESIDE_CONF%%%';
 
 $DEBUG = 0;
 
 use FS::Misc::Invoicing qw( spool_formats );
 
 $base_dir = '%%%FREESIDE_CONF%%%';
 
 $DEBUG = 0;
 
+$conf_cache_enabled = 0;
+
 =head1 NAME
 
 FS::Conf - Freeside configuration values
 =head1 NAME
 
 FS::Conf - Freeside configuration values
@@ -108,18 +112,12 @@ specific value(s) is returned.
 
 =cut
 
 
 =cut
 
-sub _usecompat {
-  my ($self, $method) = (shift, shift);
-  carp "NO CONFIGURATION RECORDS FOUND -- USING COMPATIBILITY MODE"
-    if use_confcompat;
-  my $compat = new FS::Conf_compat17 ("$base_dir/conf." . datasrc);
-  $compat->$method(@_);
-}
-
 sub _config {
   my($self,$name,$agentnum,$agentonly)=@_;
   my $hashref = { 'name' => $name };
   local $FS::Record::conf = undef;  # XXX evil hack prevents recursion
 sub _config {
   my($self,$name,$agentnum,$agentonly)=@_;
   my $hashref = { 'name' => $name };
   local $FS::Record::conf = undef;  # XXX evil hack prevents recursion
+  $conf_cache = undef unless $conf_cache_enabled; # use cache only when it is
+                                                  # safe to do so
   my $cv;
   my @a = (
     ($agentnum || ()),
   my $cv;
   my @a = (
     ($agentnum || ()),
@@ -133,9 +131,14 @@ sub _config {
   foreach my $a (@a) {
     $hashref->{agentnum} = $a;
     foreach my $l (@l) {
   foreach my $a (@a) {
     $hashref->{agentnum} = $a;
     foreach my $l (@l) {
-      $hashref->{locale} = $l;
-      $cv = FS::Record::qsearchs('conf', $hashref);
-      return $cv if $cv;
+      my $key = join(':',$name, $a, $l);
+      if (! exists $conf_cache->{$key}){
+        $hashref->{locale} = $l;
+        # $conf_cache is reset in FS::UID during myconnect, so the cache is
+        # reset per connection
+        $conf_cache->{$key} = FS::Record::qsearchs('conf', $hashref);
+      }
+      return $conf_cache->{$key} if $conf_cache->{$key};
     }
   }
   return undef;
     }
   }
   return undef;
@@ -143,7 +146,6 @@ sub _config {
 
 sub config {
   my $self = shift;
 
 sub config {
   my $self = shift;
-  return $self->_usecompat('config', @_) if use_confcompat;
 
   carp "FS::Conf->config(". join(', ', @_). ") called"
     if $DEBUG > 1;
 
   carp "FS::Conf->config(". join(', ', @_). ") called"
     if $DEBUG > 1;
@@ -167,7 +169,6 @@ Returns the exact scalar value for key.
 
 sub config_binary {
   my $self = shift;
 
 sub config_binary {
   my $self = shift;
-  return $self->_usecompat('config_binary', @_) if use_confcompat;
 
   my $cv = $self->_config(@_) or return;
   length($cv->value) ? decode_base64($cv->value) : '';
 
   my $cv = $self->_config(@_) or return;
   length($cv->value) ? decode_base64($cv->value) : '';
@@ -182,7 +183,6 @@ is undefined.
 
 sub exists {
   my $self = shift;
 
 sub exists {
   my $self = shift;
-  return $self->_usecompat('exists', @_) if use_confcompat;
 
   #my($name, $agentnum)=@_;
 
 
   #my($name, $agentnum)=@_;
 
@@ -197,7 +197,6 @@ sub exists {
 
 sub config_bool {
   my $self = shift;
 
 sub config_bool {
   my $self = shift;
-  return $self->_usecompat('exists', @_) if use_confcompat;
 
   my($name,$agentnum,$agentonly) = @_;
 
 
   my($name,$agentnum,$agentonly) = @_;
 
@@ -252,7 +251,6 @@ KEY_SUFFIX, if it exists, otherwise for KEY
 # these to fall back to standard values
 sub config_orbase {
   my $self = shift;
 # these to fall back to standard values
 sub config_orbase {
   my $self = shift;
-  return $self->_usecompat('config_orbase', @_) if use_confcompat;
 
   my( $name, $suffix ) = @_;
   if ( $self->exists("${name}_$suffix") ) {
 
   my( $name, $suffix ) = @_;
   if ( $self->exists("${name}_$suffix") ) {
@@ -272,7 +270,6 @@ config_orbase.
 
 sub key_orbase {
   my $self = shift;
 
 sub key_orbase {
   my $self = shift;
-  #no compat for this...return $self->_usecompat('config_orbase', @_) if use_confcompat;
 
   my( $name, $suffix ) = @_;
   if ( $self->exists("${name}_$suffix") ) {
 
   my( $name, $suffix ) = @_;
   if ( $self->exists("${name}_$suffix") ) {
@@ -315,7 +312,6 @@ Creates the specified configuration key if it does not exist.
 
 sub touch {
   my $self = shift;
 
 sub touch {
   my $self = shift;
-  return $self->_usecompat('touch', @_) if use_confcompat;
 
   my($name, $agentnum) = @_;
   #unless ( $self->exists($name, $agentnum) ) {
 
   my($name, $agentnum) = @_;
   #unless ( $self->exists($name, $agentnum) ) {
@@ -336,7 +332,6 @@ Sets the specified configuration key to the given value.
 
 sub set {
   my $self = shift;
 
 sub set {
   my $self = shift;
-  return $self->_usecompat('set', @_) if use_confcompat;
 
   my($name, $value, $agentnum) = @_;
   $value =~ /^(.*)$/s;
 
   my($name, $value, $agentnum) = @_;
   $value =~ /^(.*)$/s;
@@ -361,6 +356,12 @@ sub set {
     $error = $new->insert;
   }
 
     $error = $new->insert;
   }
 
+  if (! $error) {
+    # clean the object cache
+    my $key = join(':',$name, $agentnum, $self->{locale});
+    $conf_cache->{ $key } = $new;
+  }
+
   die "error setting configuration value: $error \n"
     if $error;
 
   die "error setting configuration value: $error \n"
     if $error;
 
@@ -375,7 +376,6 @@ can be retrieved with config_binary.
 
 sub set_binary {
   my $self  = shift;
 
 sub set_binary {
   my $self  = shift;
-  return if use_confcompat;
 
   my($name, $value, $agentnum)=@_;
   $self->set($name, encode_base64($value), $agentnum);
 
   my($name, $value, $agentnum)=@_;
   $self->set($name, encode_base64($value), $agentnum);
@@ -389,7 +389,6 @@ Deletes the specified configuration key.
 
 sub delete {
   my $self = shift;
 
 sub delete {
   my $self = shift;
-  return $self->_usecompat('delete', @_) if use_confcompat;
 
   my($name, $agentnum) = @_;
   if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum, locale => $self->{locale}}) ) {
 
   my($name, $agentnum) = @_;
   if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum, locale => $self->{locale}}) ) {
@@ -416,7 +415,6 @@ sub delete {
 
 sub delete_bool {
   my $self = shift;
 
 sub delete_bool {
   my $self = shift;
-  return $self->_usecompat('delete', @_) if use_confcompat;
 
   my($name, $agentnum) = @_;
 
 
   my($name, $agentnum) = @_;
 
@@ -447,7 +445,7 @@ in the directory DIR.
 sub import_config_item { 
   my ($self,$item,$dir) = @_;
   my $key = $item->key;
 sub import_config_item { 
   my ($self,$item,$dir) = @_;
   my $key = $item->key;
-  if ( -e "$dir/$key" && ! use_confcompat ) {
+  if ( -e "$dir/$key" ) {
     warn "Inserting $key\n" if $DEBUG;
     local $/;
     my $value = readline(new IO::File "$dir/$key");
     warn "Inserting $key\n" if $DEBUG;
     local $/;
     my $value = readline(new IO::File "$dir/$key");
@@ -456,68 +454,9 @@ sub import_config_item {
     }else{
       $self->set($key, $value);
     }
     }else{
       $self->set($key, $value);
     }
-  }else {
-    warn "Not inserting $key\n" if $DEBUG;
-  }
-}
-
-=item verify_config_item CONFITEM DIR 
-
-  Compares the item specified by the CONFITEM (see L<FS::ConfItem>) in
-the database to the legacy file value in DIR.
-
-=cut
-
-sub verify_config_item { 
-  return '' if use_confcompat;
-  my ($self,$item,$dir) = @_;
-  my $key = $item->key;
-  my $type = $item->type;
-
-  my $compat = new FS::Conf_compat17 $dir;
-  my $error = '';
-  
-  $error .= "$key fails existential comparison; "
-    if $self->exists($key) xor $compat->exists($key);
-
-  if ( $type !~ /^(binary|image)$/ ) {
-
-    {
-      no warnings;
-      $error .= "$key fails scalar comparison; "
-        unless scalar($self->config($key)) eq scalar($compat->config($key));
-    }
-
-    my (@new) = $self->config($key);
-    my (@old) = $compat->config($key);
-    unless ( scalar(@new) == scalar(@old)) { 
-      $error .= "$key fails list comparison; ";
-    }else{
-      my $r=1;
-      foreach (@old) { $r=0 if ($_ cmp shift(@new)); }
-      $error .= "$key fails list comparison; "
-        unless $r;
-    }
-
   } else {
   } else {
-
-    no warnings 'uninitialized';
-    $error .= "$key fails binary comparison; "
-      unless scalar($self->config_binary($key)) eq scalar($compat->config_binary($key));
-
+    warn "Not inserting $key\n" if $DEBUG;
   }
   }
-
-#remove deprecated config on our own terms, not freeside-upgrade's
-#  if ($error =~ /existential comparison/ && $item->section eq 'deprecated') {
-#    my $proto;
-#    for ( @config_items ) { $proto = $_; last if $proto->key eq $key;  }
-#    unless ($proto->key eq $key) { 
-#      warn "removed config item $error\n" if $DEBUG;
-#      $error = '';
-#    }
-#  }
-
-  $error;
 }
 
 #item _orbase_items OPTIONS
 }
 
 #item _orbase_items OPTIONS
@@ -588,7 +527,6 @@ FS::ConfItem objects.  See L<FS::ConfItem>.
 
 sub config_items {
   my $self = shift; 
 
 sub config_items {
   my $self = shift; 
-  return $self->_usecompat('config_items', @_) if use_confcompat;
 
   ( @config_items, $self->_orbase_items(@_) );
 }
 
   ( @config_items, $self->_orbase_items(@_) );
 }
@@ -624,23 +562,11 @@ to conf records in the database.
 sub init_config {
   my $dir = shift;
 
 sub init_config {
   my $dir = shift;
 
-  {
-    local $FS::UID::use_confcompat = 0;
-    my $conf = new FS::Conf;
-    foreach my $item ( $conf->config_items(dir => $dir) ) {
-      $conf->import_config_item($item, $dir);
-      my $error = $conf->verify_config_item($item, $dir);
-      return $error if $error;
-    }
-  
-    my $compat = new FS::Conf_compat17 $dir;
-    foreach my $item ( $compat->config_items ) {
-      my $error = $conf->verify_config_item($item, $dir);
-      return $error if $error;
-    }
+  my $conf = new FS::Conf;
+  foreach my $item ( $conf->config_items(dir => $dir) ) {
+    $conf->import_config_item($item, $dir);
   }
 
   }
 
-  $FS::UID::use_confcompat = 0;
   '';  #success
 }
 
   '';  #success
 }
 
@@ -679,10 +605,12 @@ invoice_latexfooter
 invoice_latexsmallfooter
 invoice_latexnotes
 invoice_latexcoupon
 invoice_latexsmallfooter
 invoice_latexnotes
 invoice_latexcoupon
+invoice_latexwatermark
 invoice_html
 invoice_htmlreturnaddress
 invoice_htmlfooter
 invoice_htmlnotes
 invoice_html
 invoice_htmlreturnaddress
 invoice_htmlfooter
 invoice_htmlnotes
+invoice_htmlwatermark
 logo.png
 logo.eps
 );
 logo.png
 logo.eps
 );
@@ -733,6 +661,23 @@ my %batch_gateway_options = (
   },
 );
 
   },
 );
 
+my %invoice_mode_options = (
+  'type'        => 'select-sub',
+  'options_sub' => sub { 
+    my @modes = qsearch({
+        'table' => 'invoice_mode', 
+        'extra_sql' => ' WHERE '.
+          $FS::CurrentUser::CurrentUser->agentnums_sql(null => 1),
+        });
+    map { $_->modenum, $_->modename } @modes;
+  },
+  'option_sub'  => sub { 
+                         my $mode = FS::invoice_mode->by_key(shift);
+                         $mode ? $mode->modename : '',
+                       },
+  'per_agent' => 1,
+);
+
 my @cdr_formats = (
   '' => '',
   'default' => 'Default',
 my @cdr_formats = (
   '' => '',
   'default' => 'Default',
@@ -760,6 +705,11 @@ sub reason_type_options {
   }
 }
 
   }
 }
 
+my $validate_email = sub { $_[0] =~
+                             /^[^@]+\@[[:alnum:]-]+(\.[[:alnum:]-]+)+$/
+                             ? '' : 'Invalid email address';
+                         };
+
 #Billing (81 items)
 #Invoicing (50 items)
 #UI (69 items)
 #Billing (81 items)
 #Invoicing (50 items)
 #UI (69 items)
@@ -769,13 +719,6 @@ sub reason_type_options {
 
 @config_items = map { new FS::ConfItem $_ } (
 
 
 @config_items = map { new FS::ConfItem $_ } (
 
-  {
-    'key'         => 'address',
-    'section'     => 'deprecated',
-    'description' => 'This configuration option is no longer used.  See <a href="#invoice_template">invoice_template</a> instead.',
-    'type'        => 'text',
-  },
-
   {
     'key'         => 'event_log_level',
     'section'     => 'notification',
   {
     'key'         => 'event_log_level',
     'section'     => 'notification',
@@ -1095,13 +1038,6 @@ sub reason_type_options {
     'per_locale'  => 1,
   },
 
     'per_locale'  => 1,
   },
 
-  {
-    'key'         => 'deleteinvoices',
-    'section'     => 'UI',
-    'description' => 'Enable invoices deletions.  Be very careful!  Deleting an invoice will remove all traces that the invoice ever existed!  Normally, you would void or apply a credit against the invoice instead.',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'deletecredits',
     #not actually deprecated yet
   {
     'key'         => 'deletecredits',
     #not actually deprecated yet
@@ -1119,20 +1055,6 @@ sub reason_type_options {
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
-  {
-    'key'         => 'unapplypayments',
-    'section'     => 'deprecated',
-    'description' => '<B>DEPRECATED</B>, now controlled by ACLs.  Used to enable "unapplication" of unclosed payments.',
-    'type'        => 'checkbox',
-  },
-
-  {
-    'key'         => 'unapplycredits',
-    'section'     => 'deprecated',
-    'description' => '<B>DEPRECATED</B>, now controlled by ACLs.  Used to enable "unapplication" of unclosed credits.',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'dirhash',
     'section'     => 'shell',
   {
     'key'         => 'dirhash',
     'section'     => 'shell',
@@ -1251,10 +1173,7 @@ sub reason_type_options {
     'description' => 'Return address on email invoices (address only, see invoice_from_name)',
     'type'        => 'text',
     'per_agent'   => 1,
     'description' => 'Return address on email invoices (address only, see invoice_from_name)',
     'type'        => 'text',
     'per_agent'   => 1,
-    'validate'    => sub { $_[0] =~
-                             /^[^@]+\@[[:alnum:]-]+(\.[[:alnum:]-]+)+$/
-                             ? '' : 'Invalid email address';
-                         }
+    'validate'    => $validate_email,
   },
 
   {
   },
 
   {
@@ -1360,6 +1279,15 @@ sub reason_type_options {
     'per_locale'  => 1,
   },
 
     'per_locale'  => 1,
   },
 
+  {
+    'key'         => 'invoice_htmlwatermark',
+    'section'     => 'invoicing',
+    'description' => 'Watermark for HTML invoices. Appears in a semitransparent positioned DIV overlaid on the main invoice container.',
+    'type'        => 'textarea',
+    'per_agent'   => 1,
+    'per_locale'  => 1,
+  },
+
   {
     'key'         => 'invoice_latex',
     'section'     => 'invoicing',
   {
     'key'         => 'invoice_latex',
     'section'     => 'invoicing',
@@ -1547,6 +1475,15 @@ and customer address. Include units.',
     'per_locale'  => 1,
   },
 
     'per_locale'  => 1,
   },
 
+  {
+    'key'         => 'invoice_latexwatermark',
+    'section'     => 'invoicing',
+    'description' => 'Watermark for LaTeX invoices. See "texdoc background" for information on what this can contain. The content itself should be enclosed in braces, optionally followed by a comma and any formatting options.',
+    'type'        => 'textarea',
+    'per_agent'   => 1,
+    'per_locale'  => 1,
+  },
+
   {
     'key'         => 'invoice_email_pdf',
     'section'     => 'invoicing',
   {
     'key'         => 'invoice_email_pdf',
     'section'     => 'invoicing',
@@ -1561,6 +1498,13 @@ and customer address. Include units.',
     'type'        => 'checkbox'
   },
 
     'type'        => 'checkbox'
   },
 
+  {
+    'key'         => 'invoice_email_pdf_msgnum',
+    'section'     => 'invoicing',
+    'description' => 'Message template to send as the text and HTML part of PDF invoices. If not selected, a text and HTML version of the invoice will be sent.',
+    %msg_template_options,
+  },
+
   {
     'key'         => 'invoice_email_pdf_note',
     'section'     => 'invoicing',
   {
     'key'         => 'invoice_email_pdf_note',
     'section'     => 'invoicing',
@@ -1697,13 +1641,6 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
-  {
-    'key'         => 'invoice_send_receipts',
-    'section'     => 'deprecated',
-    'description' => '<b>DEPRECATED</b>, this used to send an invoice copy on payments and credits.  See the payment_receipt_email and XXXX instead.',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'payment_receipt',
     'section'     => 'notification',
   {
     'key'         => 'payment_receipt',
     'section'     => 'notification',
@@ -1713,10 +1650,17 @@ and customer address. Include units.',
     'agent_bool'  => 1,
   },
 
     'agent_bool'  => 1,
   },
 
+  {
+    'key'         => 'payment_receipt_statement_mode',
+    'section'     => 'notification',
+    'description' => 'Automatic payments will cause a post-payment statement to be sent to the customer. Select the invoice mode to use for this statement. If unspecified, it will use the "_statement" versions of invoice configuration settings, and have the notice name "Statement".',
+    %invoice_mode_options,
+  },
+
   {
     'key'         => 'payment_receipt_msgnum',
     'section'     => 'notification',
   {
     'key'         => 'payment_receipt_msgnum',
     'section'     => 'notification',
-    'description' => 'Template to use for payment receipts.',
+    'description' => 'Template to use for manual payment receipts.',
     %msg_template_options,
   },
   
     %msg_template_options,
   },
   
@@ -1894,13 +1838,6 @@ and customer address. Include units.',
 #    'description' => 'Directory which contains domain registry information.  Each registry is a directory.',
 #  },
 
 #    'description' => 'Directory which contains domain registry information.  Each registry is a directory.',
 #  },
 
-  {
-    'key'         => 'report_template',
-    'section'     => 'deprecated',
-    'description' => 'Deprecated template file for reports.',
-    'type'        => 'textarea',
-  },
-
   {
     'key'         => 'maxsearchrecordsperpage',
     'section'     => 'UI',
   {
     'key'         => 'maxsearchrecordsperpage',
     'section'     => 'UI',
@@ -2146,13 +2083,6 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
-  {
-    'key'         => 'safe-part_bill_event',
-    'section'     => 'UI',
-    'description' => 'Validates invoice event expressions against a preset list.  Useful for webdemos, annoying to powerusers.',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'show_ship_company',
     'section'     => 'UI',
   {
     'key'         => 'show_ship_company',
     'section'     => 'UI',
@@ -2345,6 +2275,7 @@ and customer address. Include units.',
                        'svc_acct'  => 'Account (svc_acct)',
                        'svc_phone' => 'Phone number (svc_phone)',
                        'svc_pbx'   => 'PBX (svc_pbx)',
                        'svc_acct'  => 'Account (svc_acct)',
                        'svc_phone' => 'Phone number (svc_phone)',
                        'svc_pbx'   => 'PBX (svc_pbx)',
+                       'none'      => 'None - package only',
                      ],
   },
   
                      ],
   },
   
@@ -2501,54 +2432,90 @@ and customer address. Include units.',
 
   {
     'key'         => 'enable_taxclasses',
 
   {
     'key'         => 'enable_taxclasses',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Enable per-package tax classes',
     'type'        => 'checkbox',
   },
 
   {
     'key'         => 'require_taxclasses',
     'description' => 'Enable per-package tax classes',
     'type'        => 'checkbox',
   },
 
   {
     'key'         => 'require_taxclasses',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Require a taxclass to be entered for every package',
     'type'        => 'checkbox',
   },
 
   {
     'description' => 'Require a taxclass to be entered for every package',
     'type'        => 'checkbox',
   },
 
   {
-    'key'         => 'enable_taxproducts',
-    'section'     => 'billing',
+    'key'         => 'tax_data_vendor',
+    'section'     => 'taxation',
     'description' => 'Tax data vendor you are using.',
     'type'        => 'select',
     'description' => 'Tax data vendor you are using.',
     'type'        => 'select',
-    'select_enum' => [ 'cch', 'billsoft', 'avalara' ],
+    'select_enum' => [ '', 'cch', 'billsoft', 'avalara', 'suretax' ],
   },
 
   {
     'key'         => 'taxdatadirectdownload',
   },
 
   {
     'key'         => 'taxdatadirectdownload',
-    'section'     => 'billing',  #well
-    'description' => 'Enable downloading tax data directly from the vendor site. at least three lines: URL, username, and password.j',
+    'section'     => 'taxation',
+    'description' => 'Enable downloading tax data directly from CCH. at least three lines: URL, username, and password.j',
     'type'        => 'textarea',
   },
 
   {
     'key'         => 'ignore_incalculable_taxes',
     'type'        => 'textarea',
   },
 
   {
     'key'         => 'ignore_incalculable_taxes',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Prefer to invoice without tax over not billing at all',
     'type'        => 'checkbox',
   },
 
   {
     'key'         => 'billsoft-company_code',
     'description' => 'Prefer to invoice without tax over not billing at all',
     'type'        => 'checkbox',
   },
 
   {
     'key'         => 'billsoft-company_code',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Billsoft tax service company code (3 letters)',
     'type'        => 'text',
   },
 
   {
     'key'         => 'avalara-taxconfig',
     'description' => 'Billsoft tax service company code (3 letters)',
     'type'        => 'text',
   },
 
   {
     'key'         => 'avalara-taxconfig',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Avalara tax service configuration. Four lines: company code, account number, license key, test mode (1 to enable).',
     'type'        => 'textarea',
   },
 
     'description' => 'Avalara tax service configuration. Four lines: company code, account number, license key, test mode (1 to enable).',
     'type'        => 'textarea',
   },
 
+  {
+    'key'         => 'suretax-hostname',
+    'section'     => 'taxation',
+    'description' => 'SureTax server name; defaults to the test server.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'suretax-client_number',
+    'section'     => 'taxation',
+    'description' => 'SureTax tax service client ID.',
+    'type'        => 'text',
+  },
+  {
+    'key'         => 'suretax-validation_key',
+    'section'     => 'taxation',
+    'description' => 'SureTax validation key (UUID).',
+    'type'        => 'text',
+  },
+  {
+    'key'         => 'suretax-business_unit',
+    'section'     => 'taxation',
+    'description' => 'SureTax client business unit name; optional.',
+    'type'        => 'text',
+    'per_agent'   => 1,
+  },
+  {
+    'key'         => 'suretax-regulatory_code',
+    'section'     => 'taxation',
+    'description' => 'SureTax client regulatory status.',
+    'type'        => 'select',
+    'select_enum' => [ '', 'ILEC', 'IXC', 'CLEC', 'VOIP', 'ISP', 'Wireless' ],
+    'per_agent'   => 1,
+  },
+
+
   {
     'key'         => 'welcome_msgnum',
     'section'     => 'notification',
   {
     'key'         => 'welcome_msgnum',
     'section'     => 'notification',
@@ -2655,6 +2622,13 @@ and customer address. Include units.',
     'select_enum' => [ qw(CARD DCRD CHEK DCHK CASH WEST MCRD MCHK PPAL) ],
   },
 
     'select_enum' => [ qw(CARD DCRD CHEK DCHK CASH WEST MCRD MCHK PPAL) ],
   },
 
+  {
+    'key'         => 'banned_pay-pad',
+    'section'     => 'billing',
+    'description' => 'Padding for encrypted storage of banned credit card hashes.  If you already have new-style SHA512 entries in the banned_pay table, do not change as this will invalidate the old entries.',
+    'type'        => 'text',
+  },
+
   {
     'key'         => 'payby-default',
     'section'     => 'deprecated',
   {
     'key'         => 'payby-default',
     'section'     => 'deprecated',
@@ -2670,13 +2644,6 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
-  {
-    'key'         => 'paymentforcedtobatch',
-    'section'     => 'deprecated',
-    'description' => 'See batch-enable_payby and realtime-disable_payby.  Used to (for CHEK): Cause per customer payment entry to be forced to a batch processor rather than performed realtime.',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'svc_acct-notes',
     'section'     => 'deprecated',
   {
     'key'         => 'svc_acct-notes',
     'section'     => 'deprecated',
@@ -2744,10 +2711,11 @@ and customer address. Include units.',
   },
 
   {
   },
 
   {
-    'key'         => 'users-allow_comp',
-    'section'     => 'deprecated',
-    'description' => '<b>DEPRECATED</b>, enable the <i>Complimentary customer</i> access right instead.  Was: Usernames (Freeside users, created with <a href="../docs/man/bin/freeside-adduser.html">freeside-adduser</a>) which can create complimentary customers, one per line.  If no usernames are entered, all users can create complimentary accounts.',
-    'type'        => 'textarea',
+    'key'         => 'dump-email_to',
+    'section'     => '',
+    'description' => "Optional email address to send success/failure message for database dumps.",
+    'type'        => 'text',
+    'validate'    => $validate_email,
   },
 
   {
   },
 
   {
@@ -2804,6 +2772,13 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'manual_process-single_invoice_amount',
+    'section'     => 'billing',
+    'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice',
+    'type'        => 'checkbox',
+  },
+
   {
     'key'         => 'manual_process-pkgpart',
     'section'     => 'billing',
   {
     'key'         => 'manual_process-pkgpart',
     'section'     => 'billing',
@@ -3054,7 +3029,7 @@ and customer address. Include units.',
                         },
     'option_sub'  => sub { require FS::Record;
                            require FS::agent_type;
                         },
     'option_sub'  => sub { require FS::Record;
                            require FS::agent_type;
-                          my $agent = FS::Record::qsearchs(
+                          my $agent_type = FS::Record::qsearchs(
                             'agent_type', { 'typenum'=>shift }
                           );
                            $agent_type ? $agent_type->atype : '';
                             'agent_type', { 'typenum'=>shift }
                           );
                            $agent_type ? $agent_type->atype : '';
@@ -3351,35 +3326,6 @@ and customer address. Include units.',
     'per_agent'   => 1,
   },
 
     'per_agent'   => 1,
   },
 
-  {
-    'key'         => 'city_not_required',
-    'section'     => 'required',
-    'description' => 'Turn off requirement for a City to be entered for billing & shipping addresses',
-    'type'        => 'checkbox',
-    'per_agent'   => 1,
-  },
-
-  {
-    'key'         => 'echeck-void',
-    'section'     => 'deprecated',
-    'description' => '<B>DEPRECATED</B>, now controlled by ACLs.  Used to enable local-only voiding of echeck payments in addition to refunds against the payment gateway',
-    'type'        => 'checkbox',
-  },
-
-  {
-    'key'         => 'cc-void',
-    'section'     => 'deprecated',
-    'description' => '<B>DEPRECATED</B>, now controlled by ACLs.  Used to enable local-only voiding of credit card payments in addition to refunds against the payment gateway',
-    'type'        => 'checkbox',
-  },
-
-  {
-    'key'         => 'unvoid',
-    'section'     => 'deprecated',
-    'description' => '<B>DEPRECATED</B>, now controlled by ACLs.  Used to enable unvoiding of voided payments',
-    'type'        => 'checkbox',
-  },
-
   {
     'key'         => 'address1-search',
     'section'     => 'UI',
   {
     'key'         => 'address1-search',
     'section'     => 'UI',
@@ -3409,12 +3355,6 @@ and customer address. Include units.',
     'per_agent'   => 1,
   },
 
     'per_agent'   => 1,
   },
 
-  { 'key'         => 'referral_credit',
-    'section'     => 'deprecated',
-    'description' => "Used to enable one-time referral credits in the amount of one month <i>referred</i> customer's recurring fee (irregardless of frequency).  Replace with a billing event on appropriate packages.",
-    'type'        => 'checkbox',
-  },
-
   { 'key'         => 'selfservice_server-cache_module',
     'section'     => 'self-service',
     'description' => 'Module used to store self-service session information.  All modules handle any number of self-service servers.  Cache::SharedMemoryCache is appropriate for a single database / single Freeside server.  Cache::FileCache is useful for multiple databases on a single server, or when IPC::ShareLite is not available (i.e. FreeBSD).', #  _Database stores session information in the database and is appropriate for multiple Freeside servers, but may be slower.',
   { 'key'         => 'selfservice_server-cache_module',
     'section'     => 'self-service',
     'description' => 'Module used to store self-service session information.  All modules handle any number of self-service servers.  Cache::SharedMemoryCache is appropriate for a single database / single Freeside server.  Cache::FileCache is useful for multiple databases on a single server, or when IPC::ShareLite is not available (i.e. FreeBSD).', #  _Database stores session information in the database and is appropriate for multiple Freeside servers, but may be slower.',
@@ -3714,14 +3654,14 @@ and customer address. Include units.',
 
   {
     'key'         => 'tax-ship_address',
 
   {
     'key'         => 'tax-ship_address',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the shipping address instead.',
     'type'        => 'checkbox',
   }
 ,
   {
     'key'         => 'tax-pkg_address',
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the shipping address instead.',
     'type'        => 'checkbox',
   }
 ,
   {
     'key'         => 'tax-pkg_address',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the package address instead (when present).',
     'type'        => 'checkbox',
   },
     'description' => 'By default, tax calculations are done based on the billing address.  Enable this switch to calculate tax based on the package address instead (when present).',
     'type'        => 'checkbox',
   },
@@ -3911,7 +3851,14 @@ and customer address. Include units.',
   {
     'key'         => 'batchconfig-RBC',
     'section'     => 'billing',
   {
     'key'         => 'batchconfig-RBC',
     'section'     => 'billing',
-    'description' => 'Configuration for Royal Bank of Canada PDS batching, four lines: 1. Client number, 2. Short name, 3. Long name, 4. Transaction code.',
+    'description' => 'Configuration for Royal Bank of Canada PDS batching, five lines: 1. Client number, 2. Short name, 3. Long name, 4. Transaction code 5. (optional) set to TEST to turn on test mode.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'batchconfig-RBC-login',
+    'section'     => 'billing',
+    'description' => 'FTPS login for uploading Royal Bank of Canada batches. Two lines: 1. username, 2. password. If not supplied, batches can still be created but not automatically uploaded.',
     'type'        => 'textarea',
   },
 
     'type'        => 'textarea',
   },
 
@@ -3951,6 +3898,13 @@ and customer address. Include units.',
     'type'        => 'text',
   },
 
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'batchconfig-nacha-origin_name',
+    'section'     => 'billing',
+    'description' => 'Configuration for NACHA batching, Origin name (defaults to company name, but sometimes bank name is needed instead.)',
+    'type'        => 'text',
+  },
+
   {
     'key'         => 'batch-manual_approval',
     'section'     => 'billing',
   {
     'key'         => 'batch-manual_approval',
     'section'     => 'billing',
@@ -4354,8 +4308,22 @@ and customer address. Include units.',
   {
     'key'         => 'previous_balance-exclude_from_total',
     'section'     => 'invoicing',
   {
     'key'         => 'previous_balance-exclude_from_total',
     'section'     => 'invoicing',
-    'description' => 'Do not include previous balance in the \'Total\' line.  Only meaningful when invoice_sections is false.  Optionally provide text to override the Total New Charges description',
-    'type'        => [ qw(checkbox text) ],
+    'description' => 'Show separate totals for previous invoice balance and new charges. Only meaningful when invoice_sections is false.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'previous_balance-text',
+    'section'     => 'invoicing',
+    'description' => 'Text for the label of the total previous balance, when it is shown separately. Defaults to "Previous Balance".',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'previous_balance-text-total_new_charges',
+    'section'     => 'invoicing',
+    'description' => 'Text for the label of the total of new charges, when it is shown separately. If invoice_show_prior_due_date is enabled, the due date of current charges will be appended. Defaults to "Total New Charges".',
+    'type'        => 'text',
   },
 
   {
   },
 
   {
@@ -4391,6 +4359,7 @@ and customer address. Include units.',
     'section'     => 'invoicing',
     'description' => 'Instead of showing payments (and credits) applied to the invoice, show those received since the previous invoice date.',
     'type'        => 'checkbox',
     'section'     => 'invoicing',
     'description' => 'Instead of showing payments (and credits) applied to the invoice, show those received since the previous invoice date.',
     'type'        => 'checkbox',
+                       'uscensus' => 'U.S. Census Bureau',
   },
 
   {
   },
 
   {
@@ -4478,6 +4447,13 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'cust_main-no_city_in_address',
+    'section'     => 'UI',
+    'description' => 'Turn off City for billing & shipping addresses',
+    'type'        => 'checkbox',
+  },
+
   {
     'key'         => 'census_year',
     'section'     => 'UI',
   {
     'key'         => 'census_year',
     'section'     => 'UI',
@@ -4488,7 +4464,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'tax_district_method',
 
   {
     'key'         => 'tax_district_method',
-    'section'     => 'UI',
+    'section'     => 'taxation',
     'description' => 'The method to use to look up tax district codes.',
     'type'        => 'select',
     #'select_hash' => [ FS::Misc::Geo::get_district_methods() ],
     'description' => 'The method to use to look up tax district codes.',
     'type'        => 'select',
     #'select_hash' => [ FS::Misc::Geo::get_district_methods() ],
@@ -5249,7 +5225,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'tax-cust_exempt-groups',
 
   {
     'key'         => 'tax-cust_exempt-groups',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'List of grouping possibilities for tax names, for per-customer exemption purposes, one tax name per line.  For example, "GST" would indicate the ability to exempt customers individually from taxes named "GST" (but not other taxes).',
     'type'        => 'textarea',
   },
     'description' => 'List of grouping possibilities for tax names, for per-customer exemption purposes, one tax name per line.  For example, "GST" would indicate the ability to exempt customers individually from taxes named "GST" (but not other taxes).',
     'type'        => 'textarea',
   },
@@ -5263,7 +5239,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'tax-cust_exempt-groups-num_req',
 
   {
     'key'         => 'tax-cust_exempt-groups-num_req',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'When using tax-cust_exempt-groups, control whether individual tax exemption numbers are required for exemption from different taxes.',
     'type'        => 'select',
     'select_hash' => [ ''            => 'Not required',
     'description' => 'When using tax-cust_exempt-groups, control whether individual tax exemption numbers are required for exemption from different taxes.',
     'type'        => 'select',
     'select_hash' => [ ''            => 'Not required',
@@ -5291,7 +5267,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'enable_tax_adjustments',
 
   {
     'key'         => 'enable_tax_adjustments',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Enable the ability to add manual tax adjustments.',
     'type'        => 'checkbox',
   },
     'description' => 'Enable the ability to add manual tax adjustments.',
     'type'        => 'checkbox',
   },
@@ -5744,7 +5720,7 @@ and customer address. Include units.',
 
   {
     'key'         => 'cust_class-tax_exempt',
 
   {
     'key'         => 'cust_class-tax_exempt',
-    'section'     => 'billing',
+    'section'     => 'taxation',
     'description' => 'Control the tax exemption flag per customer class rather than per indivual customer.',
     'type'        => 'checkbox',
   },
     'description' => 'Control the tax exemption flag per customer class rather than per indivual customer.',
     'type'        => 'checkbox',
   },
@@ -5779,6 +5755,13 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'selfservice-enable_payment_without_balance',
+    'section'     => 'self-service',
+    'description' => 'Allow selfservice customers to make payments even if balance is zero or below (resulting in an unapplied payment and negative balance.)',
+    'type'        => 'checkbox',
+  },
+
   {
     'key'         => 'logout-timeout',
     'section'     => 'UI',
   {
     'key'         => 'logout-timeout',
     'section'     => 'UI',
@@ -5932,47 +5915,21 @@ and customer address. Include units.',
     'type'        => 'checkbox',
   },
 
     'type'        => 'checkbox',
   },
 
-  { key => "apacheroot", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "apachemachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "apachemachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "bindprimary", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "bindsecondaries", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "bsdshellmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "cyrus", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "cp_app", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "erpcdmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "icradiusmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "icradius_mysqldest", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "icradius_mysqlsource", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "icradius_secrets", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "maildisablecatchall", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "mxmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "nsmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "arecords", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "cnamerecords", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "nismachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "qmailmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "radiusmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "sendmailconfigpath", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "sendmailmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "sendmailrestart", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "shellmachine", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "shellmachine-useradd", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "shellmachine-userdel", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "shellmachine-usermod", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "shellmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "radiusprepend", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "textradiusprepend", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "username_policy", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "vpopmailmachines", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "vpopmailrestart", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "safe-part_pkg", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "selfservice_server-quiet", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "signup_server-quiet", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "signup_server-email", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "vonage-username", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "vonage-password", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
-  { key => "vonage-fromnumber", section => "deprecated", description => "<b>DEPRECATED</b>", type => "text" },
+  {
+    'key'         => 'default_appointment_length',
+    'section'     => 'UI',
+    'description' => 'Default appointment length, in minutes (30 minute granularity).',
+    'type'        => 'text',
+  },
+
+  # for internal use only; test databases should declare this option and
+  # everyone else should pretend it doesn't exist
+  #{
+  #  'key'         => 'no_random_ids',
+  #  'section'     => '',
+  #  'description' => 'Replace random identifiers in UI code with a static string, for repeatable testing. Don\'t use in production.',
+  #  'type'        => 'checkbox',
+  #},
 
 );
 
 
 );