RT# 81961 Repair broken links in POD documentation
[freeside.git] / FS / FS / svc_acct.pm
index e67db43..efe6c73 100644 (file)
@@ -6,7 +6,12 @@ use base qw( FS::svc_Domain_Mixin
              FS::svc_CGPRule_Mixin
              FS::svc_Radius_Mixin
              FS::svc_Tower_Mixin
-             FS::svc_Common );
+             FS::svc_IP_Mixin
+             FS::Password_Mixin
+             FS::svc_Common
+           );
+
+use strict;
 use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $dir_prefix @shells $usernamemin
              $usernamemax $passwordmin $passwordmax
@@ -14,6 +19,7 @@ use vars qw( $DEBUG $me $conf $skip_fuzzyfiles
              $username_noperiod $username_nounderscore $username_nodash
              $username_uppercase $username_percent $username_colon
              $username_slash $username_equals $username_pound
+             $username_exclamation
              $password_noampersand $password_noexclamation
              $warning_template $warning_from $warning_subject $warning_mimetype
              $warning_cc
@@ -38,6 +44,7 @@ use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::Msgcat qw(gettext);
 use FS::UI::bytecount;
 use FS::UI::Web;
+use FS::PagedSearch qw( psearch ); # XXX in v4, replace with FS::Cursor
 use FS::part_pkg;
 use FS::part_svc;
 use FS::svc_acct_pop;
@@ -84,6 +91,7 @@ FS::UID->install_callback( sub {
   $username_slash = $conf->exists('username-slash');
   $username_equals = $conf->exists('username-equals');
   $username_pound = $conf->exists('username-pound');
+  $username_exclamation = $conf->exists('username-exclamation');
   $password_noampersand = $conf->exists('password-noexclamation');
   $password_noexclamation = $conf->exists('password-noexclamation');
   $dirhash = $conf->config('dirhash') || 0;
@@ -106,12 +114,11 @@ FS::UID->install_callback( sub {
   $smtpmachine = $conf->config('smtpmachine');
   $radius_password = $conf->config('radius-password') || 'Password';
   $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
-  @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
+  @pw_set = FS::svc_acct->pw_set;
 }
 );
 
 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '.', ',' );
 
 sub _cache {
   my $self = shift;
@@ -257,6 +264,7 @@ sub table_info {
     'display_weight' => 10,
     'cancel_weight'  => 50, 
     'ip_field' => 'slipip',
+    'manual_require' => 1,
     'fields' => {
         'dir'       => 'Home directory',
         'uid'       => {
@@ -280,6 +288,7 @@ sub table_info {
                          disable_default => 1,
                          disable_fixed => 1,
                          disable_select => 1,
+                         required => 1,
                        },
         'password_selfchange' => { label => 'Password modification',
                                    type  => 'checkbox',
@@ -291,27 +300,25 @@ sub table_info {
                          label => 'Quota', #Mail storage limit
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_quota'=> { 
                          label => 'File storage limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_maxnum'=> { 
                          label => 'Number of files limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
         'file_maxsize'=> { 
                          label => 'File size limit',
                          type => 'text',
                          disable_inventory => 1,
-                         disable_select => 1,
                        },
-        '_password' => 'Password',
+        '_password' => { label => 'Password',
+          #required => 1
+                       },
         'gid'       => {
                          label    => 'GID',
                         def_info => 'when blank, defaults to UID',
@@ -330,10 +337,12 @@ sub table_info {
         'domsvc'    => {
                          label     => 'Domain',
                          type      => 'select',
+                         select_svc => 1,
                          select_table => 'svc_domain',
                          select_key   => 'svcnum',
                          select_label => 'domain',
                          disable_inventory => 1,
+                         required => 1,
                        },
         'pbxsvc'    => { label => 'PBX',
                          type  => 'select-svc_pbx.html',
@@ -341,6 +350,15 @@ sub table_info {
                          disable_select => 1, #UI wonky, pry works otherwise
                        },
         'sectornum' => 'Tower sector',
+        'routernum' => 'Router/block',
+        'blocknum'  => {
+                         'label' => 'Address block',
+                         'type'  => 'select',
+                         'select_table' => 'addr_block',
+                          'select_key'   => 'blocknum',
+                         'select_label' => 'cidr',
+                         'disable_inventory' => 1,
+                       },
         'usergroup' => {
                          label => 'RADIUS groups',
                          type  => 'select-radius_group.html',
@@ -696,6 +714,9 @@ sub insert {
     'child_objects' => $self->child_objects,
     %options,
   );
+
+  $error ||= $self->insert_password_history;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -726,7 +747,7 @@ sub insert {
 
     #welcome email
     my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
-    unless ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts ) {
+    unless ($FS::svc_Common::noexport_hack or ( grep { $_ eq $self->svcpart } @welcome_exclude_svcparts )) {
         my $error = '';
         my $msgnum = $conf->config('welcome_msgnum', $agentnum);
         if ( $msgnum ) {
@@ -913,7 +934,19 @@ sub delete {
     }
   }
 
-  my $error = $self->SUPER::delete; # usergroup here
+  foreach my $svc_phone (
+    qsearch( 'svc_phone', { 'forward_svcnum' => $self->svcnum })
+  ) {
+    $svc_phone->set('forward_svcnum', '');
+    my $error = $svc_phone->replace;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->delete_password_history
+           || $self->SUPER::delete; # usergroup here
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -980,6 +1013,12 @@ sub replace {
   my $dbh = dbh;
 
   $error = $new->SUPER::replace($old, @_); # usergroup here
+
+  # don't need to record this unless the password was changed
+  if ( $old->_password ne $new->_password ) {
+    $error ||= $new->insert_password_history;
+  }
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error if $error;
@@ -1126,6 +1165,8 @@ sub check {
               || $self->ut_foreign_key( 'domsvc', 'svc_domain', 'svcnum' )
               || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx',    'svcnum' )
               || $self->ut_foreign_keyn('sectornum','tower_sector','sectornum')
+              || $self->ut_foreign_keyn('routernum','router','routernum')
+              || $self->ut_foreign_keyn('blocknum','addr_block','blocknum')
               || $self->ut_textn('sec_phrase')
               || $self->ut_snumbern('seconds')
               || $self->ut_snumbern('upbytes')
@@ -1161,6 +1202,15 @@ sub check {
   ;
   return $error if $error;
 
+  # assign IP address, etc.
+  if ( $conf->exists('svc_acct-ip_addr') ) {
+    my $error = $self->svc_ip_check;
+    return $error if $error;
+  } else { # I think this is correct
+    $self->routernum('');
+    $self->blocknum('');
+  }
+
   my $cust_pkg;
   local $username_letter = $username_letter;
   local $username_uppercase = $username_uppercase;
@@ -1181,7 +1231,7 @@ sub check {
 
   my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
 
-  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#]{$usernamemin,$ulen})$/i
+  $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:\/\=\#\!]{$usernamemin,$ulen})$/i
     or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
   $recref->{username} = $1;
 
@@ -1222,6 +1272,9 @@ sub check {
   unless ( $username_pound ) {
     $recref->{username} =~ /\#/ and return $uerror;
   }
+  unless ( $username_exclamation ) {
+    $recref->{username} =~ /\!/ and return $uerror;
+  }
 
 
   $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
@@ -1314,7 +1367,7 @@ sub check {
 
   unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
     if ( $recref->{slipip} eq '' ) {
-      $recref->{slipip} = '';
+      $recref->{slipip} = ''; # eh?
     } elsif ( $recref->{slipip} eq '0e0' ) {
       $recref->{slipip} = '0e0';
     } else {
@@ -1322,7 +1375,6 @@ sub check {
         or return "Illegal slipip: ". $self->slipip;
       $recref->{slipip} = $1;
     }
-
   }
 
   #arbitrary RADIUS stuff; allow ut_textn for now
@@ -1370,8 +1422,7 @@ sub check {
       $recref->{_password} = $1;
     } else {
       return gettext('illegal_password'). " $passwordmin-$passwordmax ".
-             FS::Msgcat::_gettext('illegal_password_characters').
-             ": ". $recref->{_password};
+             FS::Msgcat::_gettext('illegal_password_characters');
     }
 
     if ( $password_noampersand ) {
@@ -1384,6 +1435,7 @@ sub check {
   else {
     return "invalid password encoding ('".$recref->{_password_encoding}."'";
   }
+
   $self->SUPER::check;
 
 }
@@ -1878,12 +1930,14 @@ sub email {
   $self->username. '@'. $self->domain(@_);
 }
 
+
 =item acct_snarf
 
 Returns an array of FS::acct_snarf records associated with the account.
 
 =cut
 
+# unused as originally intended, but now by Communigate Pro "RPOP"
 sub acct_snarf {
   my $self = shift;
   qsearch({
@@ -2049,14 +2103,16 @@ sub _op_usage {
   die "Can't update $column for svcnum". $self->svcnum
     if $rv == 0;
 
-  #$self->snapshot; #not necessary, we retain the old values
-  #create an object with the updated usage values
-  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
-  #call exports
-  my $error = $new->replace($self);
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Error replacing: $error";
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #$self->snapshot; #not necessary, we retain the old values
+    #create an object with the updated usage values
+    my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+    #call exports
+    my $error = $new->replace($self);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error replacing: $error";
+    }
   }
 
   #overlimit_action eq 'cancel' handling
@@ -2252,15 +2308,17 @@ sub set_usage {
     die "Can't update usage for svcnum ". $self->svcnum
       if $rv == 0;
   }
-
-  #$self->snapshot; #not necessary, we retain the old values
-  #create an object with the updated usage values
-  my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
-  local($FS::Record::nowarn_identical) = 1;
-  my $error = $new->replace($self); #call exports
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Error replacing: $error";
+  
+  if ( $conf->exists('radius-chillispot-max') ) {
+    #$self->snapshot; #not necessary, we retain the old values
+    #create an object with the updated usage values
+    my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
+    local($FS::Record::nowarn_identical) = 1;
+    my $error = $new->replace($self); #call exports
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error replacing: $error";
+    }
   }
 
   if ( $reset ) {
@@ -2337,7 +2395,7 @@ sub is_rechargable {
 =item seconds_since TIMESTAMP
 
 Returns the number of seconds this account has been online since TIMESTAMP,
-according to the session monitor (see L<FS::Session>).
+according to the session monitor (see L<FS::session>).
 
 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
@@ -2350,60 +2408,6 @@ sub seconds_since {
   $self->cust_svc->seconds_since(@_);
 }
 
-=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
-
-Returns the numbers of seconds this account has been online between
-TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
-external SQL radacct table, specified via sqlradius export.  Sessions which
-started in the specified range but are still open are counted from session
-start to the end of the range (unless they are over 1 day old, in which case
-they are presumed missing their stop record and not counted).  Also, sessions
-which end in the range but started earlier are counted from the start of the
-range to session end.  Finally, sessions which start before the range but end
-after are counted for the entire range.
-
-TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
-L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.
-
-=cut
-
-#note: POD here, implementation in FS::cust_svc
-sub seconds_since_sqlradacct {
-  my $self = shift;
-  $self->cust_svc->seconds_since_sqlradacct(@_);
-}
-
-=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
-
-Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
-in this package for sessions ending between TIMESTAMP_START (inclusive) and
-TIMESTAMP_END (exclusive).
-
-TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
-L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.
-
-=cut
-
-#note: POD here, implementation in FS::cust_svc
-sub attribute_since_sqlradacct {
-  my $self = shift;
-  $self->cust_svc->attribute_since_sqlradacct(@_);
-}
-
-=item get_session_history TIMESTAMP_START TIMESTAMP_END
-
-Returns an array of hash references of this customers login history for the
-given time range.  (document this better)
-
-=cut
-
-sub get_session_history {
-  my $self = shift;
-  $self->cust_svc->get_session_history(@_);
-}
-
 =item last_login_text 
 
 Returns text describing the time of last login.
@@ -2415,65 +2419,99 @@ sub last_login_text {
   $self->last_login ? ctime($self->last_login) : 'unknown';
 }
 
-=item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
+=item psearch_cdrs OPTIONS
 
-=cut
+Returns a paged search (L<FS::PagedSearch>) for Call Detail Records
+associated with this service. For svc_acct, "associated with" means that
+either the "src" or the "charged_party" field of the CDR matches either
+the "username" field of the service or the username@domain label.
 
-sub get_cdrs {
-  my($self, $start, $end, %opt ) = @_;
-
-  my $did = $self->username; #yup
-
-  my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
-
-  my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
-
-  #SELECT $for_update * FROM cdr
-  #  WHERE calldate >= $start #need a conversion
-  #    AND calldate <  $end   #ditto
-  #    AND (    charged_party = "$did"
-  #          OR charged_party = "$prefix$did" #if length($prefix);
-  #          OR ( ( charged_party IS NULL OR charged_party = '' )
-  #               AND
-  #               ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
-  #             )
-  #        )
-  #    AND ( freesidestatus IS NULL OR freesidestatus = '' )
-
-  my $charged_or_src;
-  if ( length($prefix) ) {
-    $charged_or_src =
-      " AND (    charged_party = '$did' 
-              OR charged_party = '$prefix$did'
-              OR ( ( charged_party IS NULL OR charged_party = '' )
-                   AND
-                   ( src = '$did' OR src = '$prefix$did' )
-                 )
-            )
-      ";
-  } else {
-    $charged_or_src = 
-      " AND (    charged_party = '$did' 
-              OR ( ( charged_party IS NULL OR charged_party = '' )
-                   AND
-                   src = '$did'
-                 )
-            )
-      ";
+=cut
 
+sub psearch_cdrs {
+  my($self, %options) = @_;
+  my @fields;
+  my %hash;
+  my @where;
+
+  my $did = dbh->quote($self->username);
+  my $diddomain = dbh->quote($self->label);
+
+  my $prefix = $options{'default_prefix'} || ''; #convergent.au '+61'
+  my $prefixdid = dbh->quote($prefix . $self->username);
+
+  my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
+
+  if ( $options{inbound} ) {
+    # these will be selected under their DIDs
+    push @where, "FALSE";
+  }
+
+  my @orwhere;
+  if (!$options{'disable_charged_party'}) {
+    push @orwhere,
+      "charged_party = $did",
+      "charged_party = $prefixdid",
+      "charged_party = $diddomain"
+      ;
+  }
+  if (!$options{'disable_src'}) {
+    push @orwhere,
+      "src = $did AND charged_party IS NULL",
+      "src = $prefixdid AND charged_party IS NULL",
+      "src = $diddomain AND charged_party IS NULL"
+      ;
+  }
+  push @where, '(' . join(' OR ', @orwhere) . ')';
+
+  # $options{'status'} = '' is meaningful; for the rest of them it's not
+  if ( exists $options{'status'} ) {
+    $hash{'freesidestatus'} = $options{'status'};
+  }
+  if ( $options{'cdrtypenum'} ) {
+    $hash{'cdrtypenum'} = $options{'cdrtypenum'};
+  }
+  if ( $options{'calltypenum'} ) {
+    $hash{'calltypenum'} = $options{'calltypenum'};
+  }
+  if ( $options{'begin'} ) {
+    push @where, 'startdate >= '. $options{'begin'};
+  } 
+  if ( $options{'end'} ) {
+    push @where, 'startdate < '.  $options{'end'};
+  } 
+  if ( $options{'nonzero'} ) {
+    push @where, 'duration > 0';
+  } 
+
+  my $extra_sql = join(' AND ', @where);
+  if ($extra_sql) {
+    if (keys %hash) {
+      $extra_sql = " AND ".$extra_sql;
+    } else {
+      $extra_sql = " WHERE ".$extra_sql;
+    }
   }
-
-  qsearch(
-    'select'    => "$for_update *",
+  return psearch({
+    'select'    => '*',
     'table'     => 'cdr',
-    'hashref'   => {
-                     #( freesidestatus IS NULL OR freesidestatus = '' )
-                     'freesidestatus' => '',
-                   },
-    'extra_sql' => $charged_or_src,
+    'hashref'   => \%hash,
+    'extra_sql' => $extra_sql,
+    'order_by'  => "ORDER BY startdate $for_update",
+  });
+}
 
-  );
+=item get_cdrs (DEPRECATED)
+
+Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a 
+single list. Arguments are the same as for psearch_cdrs.
 
+=cut
+
+sub get_cdrs {
+  my $self = shift;
+  my $psearch = $self->psearch_cdrs(@_);
+  qsearch ( $psearch->{query} )
 }
 
 # sub radius_groups has moved to svc_Radius_Mixin
@@ -2766,6 +2804,25 @@ sub virtual_maildir {
   $self->domain. '/maildirs/'. $self->username. '/';
 }
 
+=item password_svc_check
+
+Override, for L<FS::Password_Mixin>.  Not really intended for other use.
+
+=cut
+
+sub password_svc_check {
+  my ($self, $password) = @_;
+  foreach my $field ( qw(username finger) ) {
+    foreach my $word (split(/\W+/,$self->get($field))) {
+      next unless length($word) > 2;
+      if ($password =~ /$word/i) {
+        return qq(Password contains account information '$word');
+      }
+    }
+  }
+  return '';
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -2805,108 +2862,39 @@ Arrayref of additional WHERE clauses, will be ANDed together.
 
 =cut
 
-sub search {
-  my ($class, $params) = @_;
+sub _search_svc {
+  my( $class, $params, $from, $where ) = @_;
 
-  my @where = ();
+  #these two should probably move to svc_Domain_Mixin ?
 
   # domain
   if ( $params->{'domain'} ) { 
     my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
     #preserve previous behavior & bubble up an error if $svc_domain not found?
-    push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+    push @$where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
   }
 
   # domsvc
   if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { 
-    push @where, "domsvc = $1";
-  }
-
-  #unlinked
-  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
-
-  #agentnum
-  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "cust_main.agentnum = $1";
-  }
-
-  #custnum
-  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
-    push @where, "custnum = $1";
+    push @$where, "domsvc = $1";
   }
 
-  #pkgpart
-  if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
-    #XXX untaint or sql quote
-    push @where,
-      'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
-  }
 
   # popnum
   if ( $params->{'popnum'} =~ /^(\d+)$/ ) { 
-    push @where, "popnum = $1";
+    push @$where, "popnum = $1";
   }
 
-  # svcpart
-  if ( $params->{'svcpart'} =~ /^(\d+)$/ ) { 
-    push @where, "svcpart = $1";
-  }
+
+  #and these in svc_Tower_Mixin, or maybe we never should have done svc_acct
+  # towers (or, as mark thought, never should have done svc_broadband)
 
   # sector and tower
   my @where_sector = $class->tower_sector_sql($params);
-  push @where, @where_sector if @where_sector;
-
-  # here is the agent virtualization
-  #if ($params->{CurrentUser}) {
-  #  my $access_user =
-  #    qsearchs('access_user', { username => $params->{CurrentUser} });
-  #
-  #  if ($access_user) {
-  #    push @where, $access_user->agentnums_sql('table'=>'cust_main');
-  #  }else{
-  #    push @where, "1=0";
-  #  }
-  #} else {
-    push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
-                   'table'      => 'cust_main',
-                   'null_right' => 'View/link unlinked services',
-                 );
-  #}
-
-  push @where, @{ $params->{'where'} } if $params->{'where'};
-
-  my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
-
-  my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
-                  ' LEFT JOIN part_svc  USING ( svcpart ) '.
-                  ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                  ' LEFT JOIN cust_main USING ( custnum ) ';
-
-  $addl_from .= ' LEFT JOIN tower_sector USING ( sectornum )'
-    if @where_sector;
-
-  my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
-  #if ( keys %svc_acct ) {
-  #  $count_query .= ' WHERE '.
-  #                    join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
-  #                                      keys %svc_acct
-  #                        );
-  #}
-
-  my $sql_query = {
-    'table'       => 'svc_acct',
-    'hashref'     => {}, # \%svc_acct,
-    'select'      => join(', ',
-                       'svc_acct.*',
-                       'part_svc.svc',
-                       'cust_main.custnum',
-                       FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
-                     ),
-    'addl_from'   => $addl_from,
-    'extra_sql'   => $extra_sql,
-    'order_by'    => $params->{'order_by'},
-    'count_query' => $count_query,
-  };
+  if ( @where_sector ) {
+    push @$where, @where_sector;
+    push @$from, ' LEFT JOIN tower_sector USING ( sectornum )';
+  }
 
 }