git merge bs
authorIvan Kohler <ivan@freeside.biz>
Mon, 27 Nov 2017 20:17:49 +0000 (12:17 -0800)
committerIvan Kohler <ivan@freeside.biz>
Mon, 27 Nov 2017 20:17:49 +0000 (12:17 -0800)
1  2 
FS/FS/Record.pm
httemplate/search/elements/search.html

diff --combined FS/FS/Record.pm
@@@ -18,7 -18,6 +18,7 @@@ use DBIx::DBSchema 0.43; #0.43 for fore
  use Locale::Country;
  use Locale::Currency;
  use NetAddr::IP; # for validation
 +use Crypt::OpenSSL::RSA;
  use FS::UID qw(dbh datasrc driver_name);
  use FS::CurrentUser;
  use FS::Schema qw(dbdef);
@@@ -54,6 -53,8 +54,6 @@@ our $qsearch_qualify_columns = 1
  
  our $no_check_foreign = 1; #well, not inefficiently in perl by default anymore
  
 -my $rsa_module;
 -my $rsa_loaded;
  my $rsa_encrypt;
  my $rsa_decrypt;
  
@@@ -66,7 -67,7 +66,7 @@@ FS::UID->install_callback( sub 
  
    eval "use FS::Conf;";
    die $@ if $@;
-   $conf = FS::Conf->new; 
+   $conf = FS::Conf->new;
    $conf_encryption           = $conf->exists('encryption');
    $conf_encryptionmodule     = $conf->config('encryptionmodule');
    $conf_encryptionpublickey  = join("\n",$conf->config('encryptionpublickey'));
@@@ -103,7 -104,7 +103,7 @@@ FS::Record - Database record object
  
      $record  = qsearchs FS::Record 'table', \%hash;
      $record  = qsearchs FS::Record 'table', { 'column' => 'value', ... };
-     @records = qsearch  FS::Record 'table', \%hash; 
+     @records = qsearch  FS::Record 'table', \%hash;
      @records = qsearch  FS::Record 'table', { 'column' => 'value', ... };
  
      $table = $record->table;
@@@ -173,14 -174,14 +173,14 @@@ Creates a new record.  It doesn't stor
  L<"insert"> for that.
  
  Note that the object stores this hash reference, not a distinct copy of the
- hash it points to.  You can ask the object for a copy with the I<hash> 
+ hash it points to.  You can ask the object for a copy with the I<hash>
  method.
  
  TABLE can only be omitted when a dervived class overrides the table method.
  
  =cut
  
- sub new { 
+ sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = {};
      carp "warning: FS::Record::new called with table name ". $self->{'Table'}
        unless $nowarn_classload;
    }
-   
    $self->{'Hash'} = shift;
  
-   foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) { 
+   foreach my $field ( grep !defined($self->{'Hash'}{$_}), $self->fields ) {
      $self->{'Hash'}{$field}='';
    }
  
@@@ -488,6 -489,26 +488,26 @@@ sub qsearch 
      croak $error;
    }
  
+   # Determine how to format rows returned form a union query:
+   #
+   # * When all queries involved in the union are from the same table:
+   #   Return an array of FS::$table_name objects
+   #
+   # * When union query is performed on multiple tables,
+   #   Return an array of FS::Record objects
+   #   ! Note:  As far as I can tell, this functionality was broken, and
+   #   !        actually results in a crash.  Behavior is left intact
+   #   !        as-is, in case the results are in use somewhere
+   #
+   # * Union query is performed on multiple table,
+   #       and $union_options{classname_from_column} = 1
+   #   Return an array of FS::$classname objects, where $classname is
+   #   derived for each row from a static field inserted each returned
+   #   row of data.
+   #   e.g.: SELECT custnum,first,last,'cust_main' AS `__classname`'.
    my $table = $stable[0];
    my $pkey = '';
    $table = '' if grep { $_ ne $table } @stable;
    #below was refactored out to _from_hashref, this should use it at some point
  
    my @return;
-   if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
+   if ($union_options{classname_from_column}) {
+     # todo
+     # I'm not implementing the cache for this use case, at least not yet
+     # -mjackson
+     for my $row (@stuff) {
+       my $table_class = $row->{__classname}
+         or die "`__classname` column must be set when ".
+                "using \$union_options{classname_from_column}";
+       push @return, new("FS::$table_class",$row);
+     }
+   }
+   elsif ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
      if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
        #derivied class didn't override new method, so this optimization is safe
        if ( $cache ) {
      # Check for encrypted fields and decrypt them.
     ## only in the local copy, not the cached object
      no warnings 'deprecated'; # XXX silence the warning for now
-     if ( $conf_encryption 
+     if ( $conf_encryption
           && eval '@FS::'. $table . '::encrypted_fields' ) {
        foreach my $record (@return) {
          foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
-           next if $field eq 'payinfo' 
-                     && ($record->isa('FS::payinfo_transaction_Mixin') 
+           next if $field eq 'payinfo'
+                     && ($record->isa('FS::payinfo_transaction_Mixin')
                          || $record->isa('FS::payinfo_Mixin') )
                      && $record->payby
                      && !grep { $record->payby eq $_ } @encrypt_payby;
@@@ -656,7 -691,7 +690,7 @@@ sub _query 
      push @statement, $statement;
  
      warn "[debug]$me $statement\n" if $DEBUG > 1 || $debug;
-  
  
      foreach my $field (
        grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields
@@@ -739,12 -774,12 +773,12 @@@ sub _from_hashref 
  
      # Check for encrypted fields and decrypt them.
     ## only in the local copy, not the cached object
-     if ( $conf_encryption 
+     if ( $conf_encryption
           && eval '@FS::'. $table . '::encrypted_fields' ) {
        foreach my $record (@return) {
          foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
-           next if $field eq 'payinfo' 
-                     && ($record->isa('FS::payinfo_transaction_Mixin') 
+           next if $field eq 'payinfo'
+                     && ($record->isa('FS::payinfo_transaction_Mixin')
                          || $record->isa('FS::payinfo_Mixin') )
                      && $record->payby
                      && !grep { $record->payby eq $_ } @encrypt_payby;
@@@ -771,7 -806,7 +805,7 @@@ sub get_real_fields 
    $alias_main ||= $table;
  
    ## could be optimized more for readability
-   return ( 
+   return (
      map {
  
        my $op = '=';
        }
  
      } @{ $real_fields }
-   );  
+   );
  }
  
  =item by_key PRIMARY_KEY_VALUE
@@@ -870,7 -905,7 +904,7 @@@ single SELECT spanning multiple tables
  method calls.  Interface will almost definately change in an incompatible
  fashion.
  
- Arguments: 
+ Arguments:
  
  =cut
  
@@@ -954,7 -989,7 +988,7 @@@ sub get 
    # to avoid "Use of unitialized value" errors
    if ( defined ( $self->{Hash}->{$field} ) ) {
      $self->{Hash}->{$field};
-   } else { 
+   } else {
      '';
    }
  }
@@@ -969,7 -1004,7 +1003,7 @@@ Sets the value of the column/field/key 
  
  =cut
  
- sub set { 
+ sub set {
    my($self,$field,$value) = @_;
    $self->{'modified'} = 1;
    $self->{'Hash'}->{$field} = $value;
@@@ -1028,7 -1063,7 +1062,7 @@@ sub AUTOLOAD 
      my %search = ( $foreign_column => $pkey_value );
  
      # FS::Record->$method() ?  they're actually just subs :/
-     if ( $method eq 'qsearchs' ) { 
+     if ( $method eq 'qsearchs' ) {
        return $pkey_value ? qsearchs( $table, \%search ) : '';
      } elsif ( $method eq 'qsearch' ) {
        return $pkey_value ? qsearch(  $table, \%search ) : ();
      $self->setfield($field,$value);
    } else {
      $self->getfield($field);
-   }    
+   }
  }
  
  # efficient (also, old, doesn't support FK stuff)
  #    $_[0]->setfield($field, $_[1]);
  #  } else {
  #    $_[0]->getfield($field);
- #  }    
+ #  }
  #}
  
  # get_fk_method(TABLE, FIELD)
@@@ -1174,7 -1209,7 +1208,7 @@@ sub hash 
    my($self) = @_;
    confess $self. ' -> hash: Hash attribute is undefined'
      unless defined($self->{'Hash'});
-   %{ $self->{'Hash'} }; 
+   %{ $self->{'Hash'} };
  }
  
  =item hashref
@@@ -1330,14 -1365,14 +1364,14 @@@ sub insert 
    }
  
    my $table = $self->table;
-   
    # Encrypt before the database
    if (    scalar( eval '@FS::'. $table . '::encrypted_fields')
         && $conf_encryption
    ) {
      foreach my $field (eval '@FS::'. $table . '::encrypted_fields') {
-       next if $field eq 'payinfo' 
-                 && ($self->isa('FS::payinfo_transaction_Mixin') 
+       next if $field eq 'payinfo'
+                 && ($self->isa('FS::payinfo_transaction_Mixin')
                      || $self->isa('FS::payinfo_Mixin') )
                  && $self->payby
                  && !grep { $self->payby eq $_ } @encrypt_payby;
      $statement .= 'DEFAULT VALUES';
  
    } else {
-   
      if ( $use_placeholders ) {
  
        @bind_values = map $self->getfield($_), @real_fields;
  
    local $SIG{HUP} = 'IGNORE';
    local $SIG{INT} = 'IGNORE';
-   local $SIG{QUIT} = 'IGNORE'; 
+   local $SIG{QUIT} = 'IGNORE';
    local $SIG{TERM} = 'IGNORE';
    local $SIG{TSTP} = 'IGNORE';
    local $SIG{PIPE} = 'IGNORE';
    # get inserted id from the database, if applicable & needed
    if ( $db_seq && ! $self->getfield($primary_key) ) {
      warn "[debug]$me retreiving sequence from database\n" if $DEBUG;
-   
      my $insertid = '';
  
      if ( driver_name eq 'Pg' ) {
      } else {
  
        dbh->rollback if $FS::UID::AutoCommit;
-       return "don't know how to retreive inserted ids from ". driver_name. 
+       return "don't know how to retreive inserted ids from ". driver_name.
               ", try using counterfiles (maybe run dbdef-create?)";
  
      }
  
    dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
  
-   # Now that it has been saved, reset the encrypted fields so that $new 
+   # Now that it has been saved, reset the encrypted fields so that $new
    # can still be used.
    foreach my $field (keys %{$saved}) {
      $self->setfield($field, $saved->{$field});
@@@ -1536,7 -1571,7 +1570,7 @@@ sub delete 
  
    local $SIG{HUP} = 'IGNORE';
    local $SIG{INT} = 'IGNORE';
-   local $SIG{QUIT} = 'IGNORE'; 
+   local $SIG{QUIT} = 'IGNORE';
    local $SIG{TERM} = 'IGNORE';
    local $SIG{TSTP} = 'IGNORE';
    local $SIG{PIPE} = 'IGNORE';
    my $rc = $sth->execute or return $sth->errstr;
    #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0";
    $h_sth->execute or return $h_sth->errstr if $h_sth;
-   
    dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
  
    #no need to needlessly destoy the data either (causes problems actually)
@@@ -1594,15 -1629,15 +1628,15 @@@ sub replace 
  
    my $error = $new->check;
    return $error if $error;
-   
    # Encrypt for replace
    my $saved = {};
    if (    scalar( eval '@FS::'. $new->table . '::encrypted_fields')
         && $conf_encryption
    ) {
      foreach my $field (eval '@FS::'. $new->table . '::encrypted_fields') {
-       next if $field eq 'payinfo' 
-                 && ($new->isa('FS::payinfo_transaction_Mixin') 
+       next if $field eq 'payinfo'
+                 && ($new->isa('FS::payinfo_transaction_Mixin')
                      || $new->isa('FS::payinfo_Mixin') )
                  && $new->payby
                  && !grep { $new->payby eq $_ } @encrypt_payby;
    #my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields;
    my %diff = map { ($new->getfield($_) ne $old->getfield($_))
                     ? ($_, $new->getfield($_)) : () } $old->fields;
-                    
    unless (keys(%diff) || $no_update_diff ) {
      carp "[warning]$me ". ref($new)."->replace ".
             ( $primary_key ? "$primary_key ".$new->get($primary_key) : '' ).
  
    my $statement = "UPDATE ". $old->table. " SET ". join(', ',
      map {
-       "$_ = ". _quote($new->getfield($_),$old->table,$_) 
+       "$_ = ". _quote($new->getfield($_),$old->table,$_)
      } real_fields($old->table)
    ). ' WHERE '.
      join(' AND ',
  
    local $SIG{HUP} = 'IGNORE';
    local $SIG{INT} = 'IGNORE';
-   local $SIG{QUIT} = 'IGNORE'; 
+   local $SIG{QUIT} = 'IGNORE';
    local $SIG{TERM} = 'IGNORE';
    local $SIG{TSTP} = 'IGNORE';
    local $SIG{PIPE} = 'IGNORE';
  
    dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
  
-   # Now that it has been saved, reset the encrypted fields so that $new 
+   # Now that it has been saved, reset the encrypted fields so that $new
    # can still be used.
    foreach my $field (keys %{$saved}) {
      $new->setfield($field, $saved->{$field});
@@@ -1731,7 -1766,7 +1765,7 @@@ non-custom fields, etc., and call this 
  
  =cut
  
- sub check { 
+ sub check {
      my $self = shift;
      foreach my $field ($self->virtual_fields) {
          my $error = $self->ut_textn($field);
  
  =item virtual_fields [ TABLE ]
  
- Returns a list of virtual fields defined for the table.  This should not 
+ Returns a list of virtual fields defined for the table.  This should not
  be exported, and should only be called as an instance or class method.
  
  =cut
@@@ -1836,8 -1871,8 +1870,8 @@@ format_types)
  
  =back
  
- PARAMS is a hashref (or base64-encoded Storable hashref) containing the 
- POSTed data.  It must contain the field "uploaded files", generated by 
+ PARAMS is a hashref (or base64-encoded Storable hashref) containing the
+ POSTed data.  It must contain the field "uploaded files", generated by
  /elements/file-upload.html and containing the list of uploaded files.
  Currently only supports a single file named "file".
  
@@@ -1852,7 -1887,7 +1886,7 @@@ sub process_batch_import 
    my %formats = %{ $opt->{formats} };
  
    warn Dumper($param) if $DEBUG;
-   
    my $files = $param->{'uploaded_files'}
      or die "No files provided.\n";
  
@@@ -2192,7 -2227,7 +2226,7 @@@ sub batch_import 
        next if $line =~ /^\s*$/; #skip empty lines
  
        $line = &{$row_callback}($line) if $row_callback;
-       
        next if $line =~ /^\s*$/; #skip empty lines
  
        $parser->parse($line) or do {
      foreach my $field ( @fields ) {
  
        my $value = shift @columns;
-      
        if ( ref($field) eq 'CODE' ) {
          #&{$field}(\%hash, $value);
          push @later, $field, $value;
@@@ -2370,7 -2405,7 +2404,7 @@@ sub _h_statement 
  
  =item unique COLUMN
  
- B<Warning>: External use is B<deprecated>.  
+ B<Warning>: External use is B<deprecated>.
  
  Replaces COLUMN in record with a unique number, using counters in the
  filesystem.  Used by the B<insert> method on single-field unique columns
@@@ -2541,7 -2576,7 +2575,7 @@@ sub ut_numbern 
  
  =item ut_decimal COLUMN[, DIGITS]
  
- Check/untaint decimal numbers (up to DIGITS decimal places.  If there is an 
+ Check/untaint decimal numbers (up to DIGITS decimal places.  If there is an
  error, returns the error, otherwise returns false.
  
  =item ut_decimaln COLUMN[, DIGITS]
@@@ -2706,7 -2741,7 +2740,7 @@@ error, returns the error, otherwise ret
  
  sub ut_alphan {
    my($self,$field)=@_;
-   $self->getfield($field) =~ /^(\w*)$/ 
+   $self->getfield($field) =~ /^(\w*)$/
      or return "Illegal (alphanumeric) $field: ". $self->getfield($field);
    $self->setfield($field,$1);
    '';
@@@ -2721,7 -2756,7 +2755,7 @@@ an error, returns the error, otherwise 
  
  sub ut_alphasn {
    my($self,$field)=@_;
-   $self->getfield($field) =~ /^([\w ]*)$/ 
+   $self->getfield($field) =~ /^([\w ]*)$/
      or return "Illegal (alphanumeric) $field: ". $self->getfield($field);
    $self->setfield($field,$1);
    '';
@@@ -3040,8 -3075,8 +3074,8 @@@ sub ut_name 
    $self->getfield($field) =~ /^([\p{Word} \,\.\-\']+)$/
      or return gettext('illegal_name'). " $field: ". $self->getfield($field);
    my $name = $1;
-   $name =~ s/^\s+//; 
-   $name =~ s/\s+$//; 
+   $name =~ s/^\s+//;
+   $name =~ s/\s+$//;
    $name =~ s/\s+/ /g;
    $self->setfield($field, $name);
    '';
@@@ -3122,7 -3157,7 +3156,7 @@@ see L<Locale::Country>
  sub ut_country {
    my( $self, $field ) = @_;
    unless ( $self->getfield($field) =~ /^(\w\w)$/ ) {
-     if ( $self->getfield($field) =~ /^([\w \,\.\(\)\']+)$/ 
+     if ( $self->getfield($field) =~ /^([\w \,\.\(\)\']+)$/
           && country2code($1) ) {
        $self->setfield($field,uc(country2code($1)));
      }
@@@ -3362,19 -3397,27 +3396,19 @@@ sub decrypt 
  }
  
  sub loadRSA {
 -    my $self = shift;
 -    #Initialize the Module
 -    $rsa_module = 'Crypt::OpenSSL::RSA'; # The Default
 +  my $self = shift;
  
 -    if ($conf_encryptionmodule && $conf_encryptionmodule ne '') {
 -      $rsa_module = $conf_encryptionmodule;
 -    }
 +  my $rsa_module = $conf_encryptionmodule || 'Crypt::OpenSSL::RSA';
  
 -    if (!$rsa_loaded) {
 -      eval ("require $rsa_module"); # No need to import the namespace
 -      $rsa_loaded++;
 -    }
 -    # Initialize Encryption
 -    if ($conf_encryptionpublickey && $conf_encryptionpublickey ne '') {
 -      $rsa_encrypt = $rsa_module->new_public_key($conf_encryptionpublickey);
 -    }
 -
 -    # Intitalize Decryption
 -    if ($conf_encryptionprivatekey && $conf_encryptionprivatekey ne '') {
 -      $rsa_decrypt = $rsa_module->new_private_key($conf_encryptionprivatekey);
 -    }
 +  # Initialize Encryption
 +  if ($conf_encryptionpublickey && $conf_encryptionpublickey ne '') {
 +    $rsa_encrypt = $rsa_module->new_public_key($conf_encryptionpublickey);
 +  }
 +    
 +  # Intitalize Decryption
 +  if ($conf_encryptionprivatekey && $conf_encryptionprivatekey ne '') {
 +    $rsa_decrypt = $rsa_module->new_private_key($conf_encryptionprivatekey);
 +  }
  }
  
  =item h_search ACTION
@@@ -3438,8 -3481,8 +3472,8 @@@ sub scalar_sql 
  
  =item count [ WHERE [, PLACEHOLDER ...] ]
  
- Convenience method for the common case of "SELECT COUNT(*) FROM table", 
- with optional WHERE.  Must be called as method on a class with an 
+ Convenience method for the common case of "SELECT COUNT(*) FROM table",
+ with optional WHERE.  Must be called as method on a class with an
  associated table.
  
  =cut
@@@ -3476,7 -3519,7 +3510,7 @@@ sub row_exists 
  
  =item real_fields [ TABLE ]
  
- Returns a list of the real columns in the specified table.  Called only by 
+ Returns a list of the real columns in the specified table.  Called only by
  fields() and other subroutines elsewhere in FS::Record.
  
  =cut
@@@ -3491,7 -3534,7 +3525,7 @@@ sub real_fields 
  
  =item pvf FIELD_NAME
  
- Returns the FS::part_virtual_field object corresponding to a field in the 
+ Returns the FS::part_virtual_field object corresponding to a field in the
  record (specified by FIELD_NAME).
  
  =cut
@@@ -3504,7 -3547,7 +3538,7 @@@ sub pvf 
      my $concat = [ "'cf_'", "name" ];
      return qsearchs({   table   =>  'part_virtual_field',
                          hashref =>  { dbtable => $self->table,
-                                       name    => $name 
+                                       name    => $name
                                      },
                          select  =>  'vfieldpart, dbtable, length, label, '.concat_sql($concat).' as name',
                      });
@@@ -3538,7 -3581,7 +3572,7 @@@ sub _quote 
      cluck "WARNING: Attempting to set non-null integer $table.$column null; ".
            "using 0 instead";
      0;
-   } elsif ( $value =~ /^\d+(\.\d+)?$/ && 
+   } elsif ( $value =~ /^\d+(\.\d+)?$/ &&
              ! $column_type =~ /(char|binary|text)$/i ) {
      $value;
    } elsif (( $column_type =~ /^bytea$/i || $column_type =~ /(blob|varbinary)/i )
@@@ -3602,7 -3645,7 +3636,7 @@@ the current database
  
  =cut
  
- sub str2time_sql { 
+ sub str2time_sql {
    my $driver = shift || driver_name;
  
    return 'UNIX_TIMESTAMP('      if $driver =~ /^mysql/i;
@@@ -3625,7 -3668,7 +3659,7 @@@ the current database
  
  =cut
  
- sub str2time_sql_closing { 
+ sub str2time_sql_closing {
    my $driver = shift || driver_name;
  
    return ' )::INTEGER ' if $driver =~ /^Pg/i;
@@@ -3699,7 -3742,7 +3733,7 @@@ sub concat_sql 
  
  =item group_concat_sql COLUMN, DELIMITER
  
- Returns an SQL expression to concatenate an aggregate column, using 
+ Returns an SQL expression to concatenate an aggregate column, using
  GROUP_CONCAT() for mysql and array_to_string() and array_agg() for Pg.
  
  =cut
@@@ -3717,7 -3760,7 +3751,7 @@@ sub group_concat_sql 
  
  =item midnight_sql DATE
  
- Returns an SQL expression to convert DATE (a unix timestamp) to midnight 
+ Returns an SQL expression to convert DATE (a unix timestamp) to midnight
  on that day in the system timezone, using the default driver name.
  
  =cut
@@@ -3789,4 -3832,3 +3823,3 @@@ http://poop.sf.net
  =cut
  
  1;
@@@ -9,14 -9,14 +9,14 @@@ Example
      ###
  
      'title'         => 'Page title',
-     
      'name_singular' => 'item',  #singular name for the records returned
         #OR#                     # (preferred, will be pluralized automatically)
      'name'          => 'items', #plural name for the records returned
                                  # (deprecated, will be singularlized
                                  #  simplisticly)
  
 -    #literal SQL query string (deprecated?) or qsearch hashref or arrayref
 +    #literal SQL query string (corner cases only) or qsearch hashref or arrayref
      #of qsearch hashrefs for a union of qsearches
      'query'       => {
                         'table'     => 'tablename',
                         'addl_from' => '', #'LEFT JOIN othertable USING ( key )',
                         'extra_sql' => '', #'AND otherstuff', #'WHERE onlystuff',
                         'order_by'  => 'ORDER BY something',
-    
                       },
                       # "select * from tablename";
++<<<<<<< HEAD
 +   
 +    #required (now even if 'query' is an SQL query string)
++=======
+     #required unless 'query' is an SQL query string (shouldn't be...)
++>>>>>>> 95144265eeb3ecd13b16708dbdd75dd3701f92ad
      'count_query' => 'SELECT COUNT(*) FROM tablename',
  
      ###
@@@ -47,7 -47,7 +52,7 @@@
      'header'      => [ '#',
                         'Item',
                         { 'label' => 'Another Item',
-                          
                         },
                       ],
  
      'redirect_empty' => sub { my( $cgi ) = @_;
                                popurl(2).'view/item.html';
                              },
-    
      ###
      # optional
      ###
-    
      # some HTML callbacks...
      'menubar'          => '', #menubar arrayref
      'html_init'        => '', #after the header/menubar and before the pager
      'html_foot'        => '', #at the bottom
      'html_posttotal'   => '', #at the bottom
                                # (these three can be strings or coderefs)
-     
      'count_addl' => [], #additional count fields listref of sprintf strings or coderefs
                          # [ $money_char.'%.2f total paid', ],
-    
      #second (smaller) header line, currently only for HTML
      'header2      => [ '#',
                         'Item',
                         { 'label' => 'Another Item',
-                          
                         },
                       ],
  
      #listref of column footers
      'footer'      => [],
-     
      #disabling things
      'disable_download'  => '', # set true to hide the CSV/Excel download links
      'disable_total'     => '', # set true to hide the total"
      'disable_nonefound' => '', # set true to disable the "No matching Xs found"
                                 # message
      'nohtmlheader'      => '', # set true to remove the header and menu bar
-  
      #handling "disabled" fields in the records
      'disableable' => 1,  # set set to 1 (or column position for "disabled"
                           # status col) to enable if this table has a "disabled"
      'agent_pos'             => 3, # optional position (starting from 0) to
                                    # insert an Agent column (query needs to be a
                                    # qsearch hashref and header & fields need to
 -                                  # be defined)cust_pkg_susp.html
 +                                  # be defined)
  
      # sort, link & display properties for fields
  
      'order_by_sql' => {              #to keep complex SQL expressions out of cgi order_by value,
        'fieldname' => 'sql snippet',  #  maps fields/sort_fields values to sql snippets
      }
-    
      #listref - each item is the empty string,
      #          or a listref of link and method name to append,
      #          or a listref of link and coderef to run and append
      #one letter for each column, left/right/center/none
      # or pass a listref with full values: [ 'left', 'right', 'center', '' ]
      'align'       => 'lrc.',
-    
      #listrefs of ( scalars or coderefs )
      # currently only HTML, maybe eventually Excel too
      'color'       => [],
      # Excel-specific listref of ( hashrefs or coderefs )
      # each hashref: http://search.cpan.org/dist/Spreadsheet-WriteExcel/lib/Spreadsheet/WriteExcel.pm#Format_methods_and_Format_properties
      'xls_format' => => [],
-    
  
      # miscellany
     'download_label' => 'Download this report',
-                         # defaults to 'Download full results' 
+                         # defaults to 'Download full results'
     'link_field'     => 'pkgpart'
                          # will create internal links for each row,
                          # with the value of this field as the NAME attribute
            )
  %>
  %
- % } 
+ % }
  <%init>
  
  my(%opt) = @_;
@@@ -304,10 -304,10 +309,10 @@@ if ( $opt{'agent_virt'} ) 
        $opt{$att} ||= [ map '', @{ $opt{'fields'} } ];
      }
  
-     splice @{ $opt{'header'} }, $pos, 0, 'Agent'; 
-     splice @{ $opt{'align'}  }, $pos, 0, 'c'; 
-     splice @{ $opt{'style'}  }, $pos, 0, ''; 
-     splice @{ $opt{'size'}   }, $pos, 0, ''; 
+     splice @{ $opt{'header'} }, $pos, 0, 'Agent';
+     splice @{ $opt{'align'}  }, $pos, 0, 'c';
+     splice @{ $opt{'style'}  }, $pos, 0, '';
+     splice @{ $opt{'size'}   }, $pos, 0, '';
      splice @{ $opt{'fields'} }, $pos, 0,
        sub { $_[0]->agentnum ? $_[0]->agent->agent : '(global)'; };
      splice @{ $opt{'color'}  }, $pos, 0, '';
@@@ -329,7 -329,7 +334,7 @@@ if ( $opt{'disableable'} ) 
  
      my $table = $query->{'table'};
  
-     $count_query .= 
+     $count_query .=
        ( $count_query =~ /\bWHERE\b/i ? ' AND ' : ' WHERE ' ).
        "( $table.disabled = '' OR $table.disabled IS NULL )";
  
        $opt{$att} ||= [ map '', @{ $opt{'fields'} } ];
      }
  
-     splice @{ $opt{'header'} }, $pos, 0, 'Status'; 
-     splice @{ $opt{'align'}  }, $pos, 0, 'c'; 
-     splice @{ $opt{'style'}  }, $pos, 0, 'b'; 
-     splice @{ $opt{'size'}   }, $pos, 0, ''; 
+     splice @{ $opt{'header'} }, $pos, 0, 'Status';
+     splice @{ $opt{'align'}  }, $pos, 0, 'c';
+     splice @{ $opt{'style'}  }, $pos, 0, 'b';
+     splice @{ $opt{'size'}   }, $pos, 0, '';
      splice @{ $opt{'fields'} }, $pos, 0,
        sub { shift->disabled ? 'DISABLED' : 'Active'; };
      splice @{ $opt{'color'}  }, $pos, 0,
@@@ -411,6 -411,7 +416,7 @@@ my $header = [ map { ref($_) ? $_->{'la
  my $rows;
  
  my ($order_by_key,$order_by_desc) = ($order_by =~ /^\s*(.*?)(\s+DESC)?\s*$/i);
+ my $union_order_by;
  $opt{'order_by_sql'} ||= {};
  $order_by_desc ||= '';
  $order_by = $opt{'order_by_sql'}{$order_by_key} . $order_by_desc
@@@ -421,6 -422,8 +427,8 @@@ if ( ref $query ) 
    if (ref($query) eq 'HASH') {
      @query = $query;
  
+     # Assemble peices of order_by information as SQL fragment,
+     # store as query->{order_by}
      if ( $order_by ) {
        if ( $query->{'order_by'} ) {
          if ( $query->{'order_by'} =~ /^(\s*ORDER\s+BY\s+)?(\S.*)$/is ) {
          $query->{'order_by'} = "ORDER BY $order_by";
        }
      }
      $query->{'order_by'} .= " $limit";
  
    } elsif (ref($query) eq 'ARRAY') {
-     # do we still use this? it was for the old 477 report.
+     # Presented query is a UNION query, with multiple query references
      @query = @{ $query };
+     # Assemble peices of order_by information as SQL fragment,
+     # store as $union_order_by.  Omit order_by/limit from individual
+     # $query hashrefs, because this is a union query
+     #
+     # ! Currently, order_by data is only fetched from $cgi->param('order_by')
+     # ! for union queries. If it eventually needs to be passed within query
+     # ! hashrefs, or as mason template options, would need implemented
+     $union_order_by = " ORDER BY $order_by " if $order_by;
+     $union_order_by .= " $limit " if $limit;
    } else {
-     die "invalid query reference";
+     die "invalid query reference ($query)";
    }
  
    #eval "use FS::$opt{'query'};";
    my @param = qw( select table addl_from hashref extra_sql order_by debug );
-   $rows = [ qsearch( [ map { my $query = $_;
-                              ({ map { $_ => $query->{$_} } @param });
-                            }
-                        @query
-                      ],
-                      #'order_by' => $opt{order_by}. " ". $limit,
-                    )
-           ]; 
+   if ($opt{classname_from_column}) {
+     # Perform a union of multiple queries, while using the
+     # classname_from_column qsearch union option
+     # Constrain hashkeys for each query from @param
+     @query = map{
+       my $query = $_;
+       my $new_query = {};
+       $new_query->{$_} = $query->{$_} for @param;
+       $new_query;
+     } @query;
+     $rows = [
+       qsearch(
+         \@query,
+         order_by => $union_order_by,
+         classname_from_column => 1,
+       )
+     ];
  
+   } else {
+     # default perform a query with qsearch
+     $rows = [ qsearch( [ map { my $query = $_;
+                                ({ map { $_ => $query->{$_} } @param });
+                              }
+                          @query
+                        ],
+                        #'order_by' => $opt{order_by}. " ". $limit,
+                      )
+             ];
+   }
  } else { # not ref $query; plain SQL (still used as of 07/2015)
  
    $query .= " $limit";