tower tracking, RT#15538
[freeside.git] / FS / FS / svc_broadband.pm
index d9a25b4..35de8b3 100755 (executable)
@@ -2,14 +2,15 @@ package FS::svc_broadband;
 
 use strict;
 use vars qw(@ISA $conf);
+use NetAddr::IP;
 use FS::Record qw( qsearchs qsearch dbh );
 use FS::svc_Common;
 use FS::cust_svc;
 use FS::addr_block;
 use FS::part_svc_router;
-use NetAddr::IP;
+use FS::tower_sector;
 
-@ISA = qw( FS::svc_Common );
+@ISA = qw( FS::svc_Radius_Mixin FS::svc_Common );
 
 $FS::UID::callback{'FS::svc_broadband'} = sub { 
   $conf = new FS::Conf;
@@ -70,6 +71,8 @@ customer's router will have the same address for both its internal and external
 interfaces thus saving address space.  This has been found to work on most NAT
 routers available.
 
+=item plan_id
+
 =back
 
 =head1 METHODS
@@ -93,8 +96,10 @@ sub table_info {
     'longname_plural' => 'Fixed (username-less) broadband services',
     'display_weight' => 50,
     'cancel_weight'  => 70,
+    'ip_field' => 'ip_addr',
     'fields' => {
-      'description' => 'Descriptive label for this particular device.',
+      'svcnum'      => 'Service',
+      'description' => 'Descriptive label for this particular device',
       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
@@ -105,12 +110,151 @@ sub table_info {
                          'select_label' => 'cidr',
                          'disable_inventory' => 1,
                        },
+     'plan_id' => 'Service Plan Id',
+     'performance_profile' => 'Peformance Profile',
+     'authkey'      => 'Authentication key',
+     'mac_addr'     => 'MAC address',
+     'latitude'     => 'Latitude',
+     'longitude'    => 'Longitude',
+     'altitude'     => 'Altitude',
+     'vlan_profile' => 'VLAN profile',
+     'usergroup'    => { 
+                         label => 'RADIUS groups',
+                         type  => 'select-radius_group.html',
+                         #select_table => 'radius_group',
+                         #select_key   => 'groupnum',
+                         #select_label => 'groupname',
+                         disable_inventory => 1,
+                         multiple => 1,
+                       },
     },
   };
 }
 
 sub table { 'svc_broadband'; }
 
+sub table_dupcheck_fields { ( 'mac_addr' ); }
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF.
+
+Parameters:
+
+=over 4
+
+=item unlinked - set to search for all unlinked services.  Overrides all other options.
+
+=item agentnum
+
+=item custnum
+
+=item svcpart
+
+=item ip_addr
+
+=item pkgpart - arrayref
+
+=item routernum - arrayref
+
+=item order_by
+
+=back
+
+=cut
+
+sub search {
+  my ($class, $params) = @_;
+  my @where = ();
+  my @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 )',
+  );
+
+  # based on FS::svc_acct::search, probably the most mature of the bunch
+  #unlinked
+  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
+  
+  #agentnum
+  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where, "cust_main.agentnum = $1";
+  }
+  push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
+    'null_right' => 'View/link unlinked services',
+    'table' => 'cust_main'
+  );
+
+  #custnum
+  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where, "custnum = $1";
+  }
+
+  #pkgpart, now properly untainted, can be arrayref
+  for my $pkgpart ( $params->{'pkgpart'} ) {
+    if ( ref $pkgpart ) {
+      my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
+      push @where, "cust_pkg.pkgpart IN ($where)" if $where;
+    }
+    elsif ( $pkgpart =~ /^(\d+)$/ ) {
+      push @where, "cust_pkg.pkgpart = $1";
+    }
+  }
+
+  #routernum, can be arrayref
+  for my $routernum ( $params->{'routernum'} ) {
+    push @from, 'LEFT JOIN addr_block USING ( blocknum )';
+    if ( ref $routernum and grep { $_ } @$routernum ) {
+      my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
+      push @where, "addr_block.routernum IN ($where)" if $where;
+    }
+    elsif ( $routernum =~ /^(\d+)$/ ) {
+      push @where, "addr_block.routernum = $1";
+    }
+  }
+  #svcnum
+  if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
+    push @where, "svcnum = $1";
+  }
+
+  #svcpart
+  if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
+    push @where, "svcpart = $1";
+  }
+
+  #ip_addr
+  if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
+    push @where, "ip_addr = '$1'";
+  }
+
+  #custnum
+  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
+    push @where, "custnum = $1";
+  }
+  
+  my $addl_from = join(' ', @from);
+  my $extra_sql = '';
+  $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
+  my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
+  return( {
+      'table'   => 'svc_broadband',
+      'hashref' => {},
+      'select'  => join(', ',
+        'svc_broadband.*',
+        'part_svc.svc',
+        'cust_main.custnum',
+        FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
+      ),
+      'extra_sql' => $extra_sql,
+      'addl_from' => $addl_from,
+      'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
+      'count_query' => $count_query,
+    } );
+}
+
 =item search_sql STRING
 
 Class method which returns an SQL fragment to search for the given string.
@@ -202,26 +346,33 @@ sub check {
 
   return $x unless ref($x);
 
+  my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
+  my $lat_lower = $nw_coords ? 1 : -90;
+  my $lon_upper = $nw_coords ? -1 : 180;
+
+  # remove delimiters
+  my $mac_addr = uc($self->get('mac_addr'));
+  $mac_addr =~ s/[-: ]//g;
+  $self->set('mac_addr', $mac_addr);
+
   my $error =
     $self->ut_numbern('svcnum')
     || $self->ut_numbern('blocknum')
+    || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
     || $self->ut_textn('description')
-    || $self->ut_number('speed_up')
-    || $self->ut_number('speed_down')
+    || $self->ut_numbern('speed_up')
+    || $self->ut_numbern('speed_down')
     || $self->ut_ipn('ip_addr')
     || $self->ut_hexn('mac_addr')
     || $self->ut_hexn('auth_key')
-    || $self->ut_coordn('latitude', -90, 90)
-    || $self->ut_coordn('longitude', -180, 180)
+    || $self->ut_coordn('latitude', $lat_lower, 90)
+    || $self->ut_coordn('longitude', -180, $lon_upper)
     || $self->ut_sfloatn('altitude')
     || $self->ut_textn('vlan_profile')
+    || $self->ut_textn('plan_id')
   ;
   return $error if $error;
 
-  #redundant, but better error message
-  return "MAC already in use"
-    if scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) );
-
   if($self->speed_up < 0) { return 'speed_up must be positive'; }
   if($self->speed_down < 0) { return 'speed_down must be positive'; }
 
@@ -248,7 +399,19 @@ sub check {
     }
   }
 
+  $error = $self->_check_ip_addr;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+sub _check_ip_addr {
+  my $self = shift;
+
   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
+
+    return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
+
     return "Must supply either address or block"
       unless $self->blocknum;
     my $next_addr = $self->addr_block->next_free_addr;
@@ -257,6 +420,7 @@ sub check {
     } else {
       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
     }
+
   }
 
   if (not($self->blocknum)) {
@@ -290,9 +454,21 @@ sub check {
     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
   }
 
-  $self->SUPER::check;
+  '';
 }
 
+sub _check_duplicate {
+  my $self = shift;
+
+  return "MAC already in use"
+    if ( $self->mac_addr &&
+         scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
+       );
+
+  '';
+}
+
+
 =item NetAddr
 
 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
@@ -305,6 +481,16 @@ sub NetAddr {
   new NetAddr::IP ($self->ip_addr);
 }
 
+=item tower_sector
+
+=cut
+
+sub tower_sector {
+  my $self = shift;
+  return '' unless $self->sectornum;
+  qsearchs('tower_sector', { sectornum => $self->sectornum });
+}
+
 =item addr_block
 
 Returns the FS::addr_block record (i.e. the address block) for this broadband service.