Merge branch 'FREESIDE_3_BRANCH' of ssh://git.freeside.biz/home/git/freeside into...
authorChristopher Burger <burgerc@freeside.biz>
Fri, 30 Mar 2018 00:08:18 +0000 (20:08 -0400)
committerChristopher Burger <burgerc@freeside.biz>
Fri, 30 Mar 2018 00:08:18 +0000 (20:08 -0400)
12 files changed:
FS/FS/Schema.pm
FS/FS/part_export/saisei.pm
FS/FS/part_svc.pm
FS/FS/tower.pm
FS/FS/tower_sector.pm
httemplate/edit/part_export.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/tower.html
httemplate/edit/tower.html
httemplate/elements/tower_sector.html
httemplate/elements/tr-tower_sectors.html [new file with mode: 0644]
httemplate/view/svc_export/run_script.cgi [new file with mode: 0644]

index d032254..1567b00 100644 (file)
@@ -3342,6 +3342,8 @@ sub tables_hashref {
         'height',     'decimal', 'NULL',      '', '', '', 
         'veg_height', 'decimal', 'NULL',      '', '', '', 
         'color',      'varchar', 'NULL',       6, '', '',
+        'up_rate_limit',        'int', 'NULL',      '', '', '',
+        'down_rate_limit',      'int', 'NULL',      '', '', '',
       ],
       'primary_key' => 'towernum',
       'unique'      => [ [ 'towername' ] ], # , 'agentnum' ] ],
@@ -3367,6 +3369,9 @@ sub tables_hashref {
         'east',         'decimal', 'NULL', '10,7', '', '',
         'south',        'decimal', 'NULL', '10,7', '', '',
         'north',        'decimal', 'NULL', '10,7', '', '',
+        'title',        'varchar', 'NULL', $char_d,'', '',
+        'up_rate_limit',          'int', 'NULL',      '', '', '',
+        'down_rate_limit',        'int', 'NULL',      '', '', '',
      ],
       'primary_key'  => 'sectornum',
       'unique'       => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
index c7ee6f6..922a347 100644 (file)
@@ -24,23 +24,51 @@ Saisei integration for Freeside
 
 This export offers basic svc_broadband provisioning for Saisei.
 
-This is a customer integration with Saisei.  This will setup a rate plan and tie
-the rate plan to a host via the Saisei API when the broadband service is provisioned.
-It will also untie the rate  plan via the API upon unprovisioning of the broadband service.
+This is a customer integration with Saisei.  This will setup a rate plan and tie 
+the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.  
+It will also untie the rate plan via the API upon unprovisioning of the broadband service.
+
+This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
+This will also create and modify a access point at Saisei as soon as the tower is created or modified.
+
+To use this export, follow the below instructions:
 
-This export will use the broadband service descriptive label for the Saisei rate plan name and
-will use the email from the first contact for the Saisei username that will be
-attached to this rate plan.  It will use the Saisei default Access Point.
+Add a new export and fill out required fields:
 
-Hostname or IP - Host name to Saisei API
-Port - <I>Port number to Saisei API
+Hostname or IP - <I>Host name to Saisei API
 User Name -  <I>Saisei API user name
 Password - <I>Saisei API password
 
+Create a broadband service.  The broadband service name will become the Saisei rate plan name.
+Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
+Attach above created Saisei export to this broadband service.
+
+Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
+Make sure you have set the up and down rate limit for the Tower and Sector.  This is required to be able to export the access point.
+
+Create a package for the above created broadband service, and order this package for a customer.
+
+When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point.
+This provisioned service will then be exported as a host to Saisei.
+
+when you un provision this service, the host entry at Saisei will be deleted.
+
+When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
+on export edit screen there will be a link to export Provisioned Services attached to this export.  Clicking on that will export all services 
+not currently exported to Saisei.
+
 This module also provides generic methods for working through the L</Saisei API>.
 
 =cut
 
+tie my %scripts, 'Tie::IxHash',
+  'export_provisioned_services'  => { component => '/elements/popup_link.html',
+                                      label     => 'Export provisioned services',
+                                      description => 'will export provisioned services of part service with Saisei export attached.',
+                                      html_label => '<b>Export Provisioned Services attached to this export.</b>',
+                                    },
+;
+
 tie my %options, 'Tie::IxHash',
   'port'             => { label => 'Port',
                           default => 5000 },
@@ -56,37 +84,63 @@ tie my %options, 'Tie::IxHash',
   'svc'             => 'svc_broadband',
   'desc'            => 'Export broadband service/account to Saisei',
   'options'         => \%options,
+  'scripts'         => \%scripts,
   'notes'           => <<'END',
 This is a customer integration with Saisei.  This will setup a rate plan and tie 
-the rate plan to a host via the Saisei API when the broadband service is provisioned.  
-It will also untie the rate  plan via the API upon unprovisioning of the broadband service.
-<P>This export will use the broadband service descriptive label for the Saisei rate plan name and
-will use the email from the first contact for the Saisei username that will be
-attached to this rate plan.  It will use the Saisei default Access Point.
+the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.  
+It will also untie the rate plan via the API upon unprovisioning of the broadband service.
+<P>
+This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
+This will also create and modify a access point at Saisei as soon as the tower is created or modified.
+<P>
+To use this export, follow the below instructions:
 <P>
-Required Fields:
+<OL>
+<LI>
+Add a new export and fill out required fields:
 <UL>
 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
 <LI>Port - <I>Port number to Saisei API</I></LI>
 <LI>User Name -  <I>Saisei API user name</I></LI>
 <LI>Password - <I>Saisei API password</I></LI>
 </UL>
+</LI>
+<P>
+<LI>
+Create a broadband service.  The broadband service name will become the Saisei rate plan name.
+Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
+Attach above created Saisei export to this broadband service.
+</LI>
+<P>
+<LI>
+Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
+Make sure you have set the up and down rate limit for the Tower and Sector.  This is required to be able to export the access point.
+</LI>
+<P>
+<LI>
+Create a package for the above created broadband service, and order this package for a customer.
+</LI>
+<P>
+<LI>
+When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point.
+This provisioned service will then be exported as a host to Saisei.
+<P>
+when you un provision this service, the host entry at Saisei will be deleted.
+</LI>
+</OL>
+<P>
+When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
+on export edit screen there will be a link to export Provisioned Services attached to this export.  Clicking on that will export all services 
+not currently exported to Saisei.
 END
 );
 
 sub _export_insert {
   my ($self, $svc_broadband) = @_;
-  my $rateplan_name = $svc_broadband->{Hash}->{description};
-   $rateplan_name =~ s/\s/_/g;
 
-
-  # load needed info from our end
-  my $cust_main = $svc_broadband->cust_main;
-  return "Could not load service customer" unless $cust_main;
-  my $conf = new FS::Conf;
-
-  # get policy list
-  my $policies = $self->api_get_policies();
+  my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
+  my $rateplan_name = $service_part->{Hash}->{svc};
+  $rateplan_name =~ s/\s/_/g;
 
   # check for existing rate plan
   my $existing_rateplan;
@@ -94,24 +148,18 @@ sub _export_insert {
 
   # if no existing rate plan create one and modify it.
   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
-  $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+  $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+  return $self->api_error if $self->{'__saisei_error'};
 
   # set rateplan to existing one or newly created one.
   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
 
-  my @email = map { $_->emailaddress } FS::Record::qsearch({
-        'table'     => 'contact',
-        'select'    => 'emailaddress',
-        'addl_from' => ' JOIN contact_email USING (contactnum)',
-        'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
-    });
-  my $username = $email[0];
-  my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last};
+  my $username = $svc_broadband->{Hash}->{svcnum};
+  my $description = $svc_broadband->{Hash}->{description};
 
   if (!$username) {
     $self->{'__saisei_error'} = 'no username - can not export';
-    warn "No email found $username\n" if $self->option('debug');
-    return;
+    return $self->api_error;
   }
   else {
     # check for existing user.
@@ -120,42 +168,75 @@ sub _export_insert {
  
     # if no existing user create one.
     $self->api_create_user($username, $description) unless $existing_user;
+    return $self->api_error if $self->{'__saisei_error'};
 
     # set user to existing one or newly created one.
     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
 
-    ## add access point ?
-    ## tie host to user
-    $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
+    ## add access point
+    my $tower_sector = FS::Record::qsearchs({
+      'table'     => 'tower_sector',
+      'select'    => 'tower.towername,
+                      tower.up_rate_limit as tower_upratelimit,
+                      tower.down_rate_limit as tower_downratelimit,
+                      tower_sector.sectorname,
+                      tower_sector.up_rate_limit as sector_upratelimit,
+                      tower_sector.down_rate_limit as sector_downratelimit ',
+      'addl_from' => 'LEFT JOIN tower USING ( towernum )',
+      'hashref'   => {
+                        'sectornum' => $svc_broadband->{Hash}->{sectornum},
+                     },
+    });
+
+    my $tower_name = $tower_sector->{Hash}->{towername};
+    $tower_name =~ s/\s/_/g;
+
+    my $tower_opt = {
+      'tower_name'           => $tower_name,
+      'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
+      'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
+    };
+
+    my $tower_ap = process_tower($self, $tower_opt);
+    return $self->api_error if $self->{'__saisei_error'};
+
+    my $sector_name = $tower_sector->{Hash}->{sectorname};
+    $sector_name =~ s/\s/_/g;
+
+    my $sector_opt = {
+      'tower_name'            => $tower_name,
+      'sector_name'           => $sector_name,
+      'sector_uprate_limit'   => $tower_sector->{Hash}->{sector_upratelimit},
+      'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
+    };
+    my $accesspoint = process_sector($self, $sector_opt);
+    return $self->api_error if $self->{'__saisei_error'};
+
+    ## tie host to user add sector name as access point.
+    $self->api_add_host_to_user(
+      $user->{collection}->[0]->{name},
+      $rateplan->{collection}->[0]->{name},
+      $svc_broadband->{Hash}->{ip_addr},
+      $accesspoint->{collection}->[0]->{name},
+    ) unless $self->{'__saisei_error'};
   }
 
-  return '';
+  return $self->api_error;
 
 }
 
 sub _export_replace {
-  my ($self, $svc_phone) = @_;
+  my ($self, $svc_broadband) = @_;
   return '';
 }
 
 sub _export_delete {
   my ($self, $svc_broadband) = @_;
 
-  my $cust_main = $svc_broadband->cust_main;
-  return "Could not load service customer" unless $cust_main;
-  my $conf = new FS::Conf;
-
-  my $rateplan_name = $svc_broadband->{Hash}->{description};
+  my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
+  my $rateplan_name = $service_part->{Hash}->{svc};
   $rateplan_name =~ s/\s/_/g;
-
-  my @email = map { $_->emailaddress } FS::Record::qsearch({
-        'table'     => 'contact',
-        'select'    => 'emailaddress',
-        'addl_from' => ' JOIN contact_email USING (contactnum)',
-        'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
-    });
-  my $username = $email[0]; 
+  my $username = $svc_broadband->{Hash}->{svcnum};
 
   ## tie host to user
   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
@@ -164,15 +245,81 @@ sub _export_delete {
 }
 
 sub _export_suspend {
-  my ($self, $svc_phone) = @_;
+  my ($self, $svc_broadband) = @_;
   return '';
 }
 
 sub _export_unsuspend {
-  my ($self, $svc_phone) = @_;
+  my ($self, $svc_broadband) = @_;
   return '';
 }
 
+sub export_partsvc {
+  my ($self, $svc_part) = @_;
+
+  my $rateplan_name = $svc_part->{Hash}->{svc};
+  $rateplan_name =~ s/\s/_/g;
+  my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
+  my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
+
+  my $temp_svc = $svc_part->{Hash};
+  my $svc_broadband = {};
+  map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
+
+  # check for existing rate plan
+  my $existing_rateplan;
+  $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
+
+  # Modify the existing rate plan with new service data.
+  $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
+
+  # if no existing rate plan create one and modify it.
+  $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
+  $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+
+  return $self->api_error;
+
+}
+
+sub export_tower_sector {
+  my ($self, $tower) = @_;
+
+  #modify tower or create it.
+  my $tower_name = $tower->{Hash}->{towername};
+  $tower_name =~ s/\s/_/g;
+  my $tower_opt = {
+    'tower_name'           => $tower_name,
+    'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
+    'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
+    'modify_existing'      => '1', # modify an existing access point with this info
+  };
+
+  my $tower_access_point = process_tower($self, $tower_opt);
+
+  #get list of all access points
+  my $hash_opt = {
+      'table'     => 'tower_sector',
+      'select'    => '*',
+      'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
+  };
+
+  #for each one modify or create it.
+  foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
+    my $sector_name = $tower_sector->{Hash}->{sectorname};
+    $sector_name =~ s/\s/_/g;
+    my $sector_opt = {
+      'tower_name'            => $tower_name,
+      'sector_name'           => $sector_name,
+      'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
+      'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
+      'modify_existing'       => '1', # modify an existing access point with this info
+    };
+    my $sector_access_point = process_sector($self, $sector_opt);
+  }
+
+  return $self->api_error;
+}
+
 =head1 Saisei API
 
 These methods allow access to the Saisei API using the credentials
@@ -191,6 +338,7 @@ Returns empty on failure;  retrieve error messages using L</api_error>.
 
 sub api_call {
   my ($self,$method,$path,$params) = @_;
+
   $self->{'__saisei_error'} = '';
   my $auth_info = $self->option('username') . ':' . $self->option('password');
   $params ||= {};
@@ -218,7 +366,8 @@ sub api_call {
     }
   }
   else {
-    $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent();
+    $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
+    unless ($method eq "GET");
     warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
     return; 
   }
@@ -229,7 +378,7 @@ sub api_call {
 
 =head2 api_error
 
-Returns the error string set by L</PortaOne API> methods,
+Returns the error string set by L</Saisei API> methods,
 or a blank string if most recent call produced no errors.
 
 =cut
@@ -253,7 +402,7 @@ sub api_get_policies {
   $self->{'__saisei_error'} = "Did not receive any global policies"
     unless $get_policies;
 
-  return $get_policies;
+  return $get_policies->{collection};
 }
 
 =head2 api_get_rateplan
@@ -268,8 +417,6 @@ sub api_get_rateplan {
 
   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
   return if $self->api_error;
-  $self->{'__saisei_error'} = "Did not receive any rateplan info"
-    unless $get_rateplan;
 
   return $get_rateplan;
 }
@@ -286,8 +433,6 @@ sub api_get_user {
 
   my $get_user = $self->api_call("GET", "/users/$user");
   return if $self->api_error;
-  $self->{'__saisei_error'} = "Did not receive any user info"
-    unless $get_user;
 
   return $get_user;
 }
@@ -300,14 +445,29 @@ Gets user info for specific access point.
 
 sub api_get_accesspoint {
   my $self = shift;
-  my $accesspoint;
+  my $accesspoint = shift;
 
   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
   return if $self->api_error;
-  $self->{'__saisei_error'} = "Did not receive any user info"
-    unless $get_accesspoint;
 
-  return;
+  return $get_accesspoint;
+}
+
+=head2 api_get_host
+
+Gets user info for specific host.
+
+=cut
+
+sub api_get_host {
+  my $self = shift;
+  my $ip = shift;
+
+  my $get_host = $self->api_call("GET", "/hosts/$ip");
+
+  return if $self->api_error;
+
+  return $get_host;
 }
 
 =head2 api_create_rateplan
@@ -319,6 +479,9 @@ Creates a rateplan.
 sub api_create_rateplan {
   my ($self, $svc, $rateplan) = @_;
 
+  $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
+  $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
+
   my $new_rateplan = $self->api_call(
       "PUT", 
       "/rate_plans/$rateplan",
@@ -326,22 +489,26 @@ sub api_create_rateplan {
         'downstream_rate' => $svc->{Hash}->{speed_down},
         'upstream_rate' => $svc->{Hash}->{speed_up},
       },
-  );
+  ) unless $self->{'__saisei_error'};
 
   $self->{'__saisei_error'} = "Rate Plan not created"
-    unless $new_rateplan; # should never happen
+    unless ($new_rateplan || $self->{'__saisei_error'});
+
   return $new_rateplan;
 
 }
 
 =head2 api_modify_rateplan
 
-Modify a rateplan.
+Modify a new rateplan.
 
 =cut
 
 sub api_modify_rateplan {
-  my ($self,$policies,$svc,$rateplan_name) = @_;
+  my ($self,$svc,$rateplan_name) = @_;
+
+  # get policy list
+  my $policies = $self->api_get_policies();
 
   foreach my $policy (@$policies) {
     my $policyname = $policy->{name};
@@ -357,8 +524,8 @@ sub api_modify_rateplan {
       },
     );
 
-    $self->{'__saisei_error'} = "Rate Plan not modified"
-      unless $modified_rateplan; # should never happen
+    $self->{'__saisei_error'} = "Rate Plan not modified after create"
+      unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
     
   }
 
@@ -366,6 +533,31 @@ sub api_modify_rateplan {
  
 }
 
+=head2 api_modify_existing_rateplan
+
+Modify a existing rateplan.
+
+=cut
+
+sub api_modify_existing_rateplan {
+  my ($self,$svc,$rateplan_name) = @_;
+
+  my $modified_rateplan = $self->api_call(
+    "PUT",
+    "/rate_plans/$rateplan_name",
+    {
+      'downstream_rate' => $svc->{Hash}->{speed_down},
+      'upstream_rate' => $svc->{Hash}->{speed_up},
+    },
+  );
+
+    $self->{'__saisei_error'} = "Rate Plan not modified"
+      unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
+
+  return;
+
+}
+
 =head2 api_create_user
 
 Creates a user.
@@ -384,7 +576,7 @@ sub api_create_user {
   );
 
   $self->{'__saisei_error'} = "User not created"
-    unless $new_user; # should never happen
+    unless ($new_user || $self->{'__saisei_error'}); # should never happen
 
   return $new_user;
 
@@ -397,19 +589,70 @@ Creates a access point.
 =cut
 
 sub api_create_accesspoint {
-  my ($self,$accesspoint) = @_;
+  my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
 
   # this has not been tested, but should work, if needed.
-  #my $new_accesspoint = $self->api_call(
-  #    "PUT", 
-  #    "/access_points/$accesspoint",
-  #    {
-  #      'description' => 'my description',
-  #    },
-  #);
-
-  #$self->{'__saisei_error'} = "Access point not created"
-  #  unless $new_accesspoint; # should never happen
+  my $new_accesspoint = $self->api_call(
+      "PUT",
+      "/access_points/$accesspoint",
+      {
+         'downstream_rate_limit' => $downratelimit,
+         'upstream_rate_limit' => $upratelimit,
+      },
+  );
+
+  $self->{'__saisei_error'} = "Access point not created"
+    unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
+  return;
+
+}
+
+=head2 api_modify_accesspoint
+
+Modify a new access point.
+
+=cut
+
+sub api_modify_accesspoint {
+  my ($self, $accesspoint, $uplink) = @_;
+
+  my $modified_accesspoint = $self->api_call(
+    "PUT",
+    "/access_points/$accesspoint",
+    {
+      'uplink' => $uplink, # name of attached access point
+    },
+  );
+
+  $self->{'__saisei_error'} = "Rate Plan not modified"
+    unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
+
+  return;
+
+}
+
+=head2 api_modify_existing_accesspoint
+
+Modify a existing accesspoint.
+
+=cut
+
+sub api_modify_existing_accesspoint {
+  my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
+
+  my $modified_accesspoint = $self->api_call(
+    "PUT",
+    "/access_points/$accesspoint",
+    {
+      'downstream_rate_limit' => $downratelimit,
+      'upstream_rate_limit' => $upratelimit,
+#      'uplink' => $uplink, # name of attached access point
+    },
+  );
+
+    $self->{'__saisei_error'} = "Access point not modified"
+      unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
+
   return;
 
 }
@@ -421,7 +664,7 @@ ties host to user, rateplan and default access point.
 =cut
 
 sub api_add_host_to_user {
-  my ($self,$user, $rateplan, $ip) = @_;
+  my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
 
   my $new_host = $self->api_call(
       "PUT", 
@@ -429,11 +672,12 @@ sub api_add_host_to_user {
       {
         'user'      => $user,
         'rate_plan' => $rateplan,
+        'access_point' => $accesspoint,
       },
   );
 
   $self->{'__saisei_error'} = "Host not created"
-    unless $new_host; # should never happen
+    unless ($new_host || $self->{'__saisei_error'}); # should never happen
 
   return $new_host;
 
@@ -466,12 +710,114 @@ sub api_delete_host_to_user {
   );
 
   $self->{'__saisei_error'} = "Host not created"
-    unless $delete_host; # should never happen
+    unless ($delete_host || $self->{'__saisei_error'}); # should never happen
 
   return $delete_host;
 
 }
 
+sub process_tower {
+  my ($self, $opt) = @_;
+
+  my $existing_tower_ap;
+  my $tower_name = $opt->{tower_name};
+
+  #check if tower has been set up as an access point.
+  $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
+
+  # modify the existing accesspoint if changing tower .
+  $self->api_modify_existing_accesspoint (
+    $tower_name,
+    '', # tower does not have a uplink on sectors.
+    $opt->{tower_uprate_limit},
+    $opt->{tower_downrate_limit},
+  ) if $existing_tower_ap && $opt->{modify_existing};
+
+  #if tower does not exist as an access point create it.
+  $self->api_create_accesspoint(
+      $tower_name,
+      $opt->{tower_uprate_limit},
+      $opt->{tower_downrate_limit}
+  ) unless $existing_tower_ap;
+
+  my $accesspoint = $self->api_get_accesspoint($tower_name);
+
+  return $accesspoint;
+}
+
+sub process_sector {
+  my ($self, $opt) = @_;
+
+  my $existing_sector_ap;
+  my $sector_name = $opt->{sector_name};
+
+  #check if sector has been set up as an access point.
+  $existing_sector_ap = $self->api_get_accesspoint($sector_name);
+
+  # modify the existing accesspoint if changing sector .
+  $self->api_modify_existing_accesspoint (
+    $sector_name,
+    $opt->{tower_name},
+    $opt->{sector_uprate_limit},
+    $opt->{sector_downrate_limit},
+  ) if $existing_sector_ap && $opt->{modify_existing};
+
+  #if sector does not exist as an access point create it.
+  $self->api_create_accesspoint(
+    $sector_name,
+    $opt->{sector_uprate_limit},
+    $opt->{sector_downrate_limit},
+  ) unless $existing_sector_ap;
+
+  # Attach newly created sector to it's tower.
+  $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
+
+  # set access point to existing one or newly created one.
+  my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
+
+  return $accesspoint;
+}
+
+sub export_provisioned_services {
+  my $job = shift;
+  my $param = shift;
+
+  my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
+  or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
+  bless $part_export;
+
+  my @svcparts = FS::Record::qsearch({
+    'table' => 'export_svc',
+    'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
+    'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
+  });
+  my $part_count = scalar @svcparts;
+
+  my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
+
+  my @svcs = FS::Record::qsearch({
+    'table' => 'cust_svc',
+    'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
+    'extra_sql' => " WHERE svcpart in ('".$parts."')",
+  }) unless !$parts;
+
+  my $svc_count = scalar @svcs;
+
+  my %status = {};
+  for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
+
+  my $process_count=0;
+  foreach my $svc (@svcs) {
+    if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
+    ## check if service exists as host if not export it.
+    _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});
+    $process_count++;
+  }
+
+  return;
+
+}
+
 =head1 SEE ALSO
 
 L<FS::part_export>
index 60889c6..81652c1 100644 (file)
@@ -514,6 +514,18 @@ sub part_export_dsl_pull {
     grep $_->can('dsl_pull'), $self->part_export;
 }
 
+=item part_export_partsvc
+
+Returns a list of any exports (see L<FS::part_export>) for this service that
+are capable of pushing a change after part svc is changed.
+
+=cut
+
+sub part_export_partsvc {
+    my $self = shift;
+    grep $_->can('export_partsvc'), $self->part_export;
+}
+
 =item cust_svc [ PKGPART ] 
 
 Returns a list of associated customer services (FS::cust_svc records).
@@ -905,6 +917,11 @@ sub process {
   );
 
   die "$error\n" if $error;
+
+  foreach my $part_svc_export ( $new->part_export_partsvc ) {
+    $error = $part_svc_export->export_partsvc($new);
+  }
+  return $error if $error;
 }
 
 =item process_bulk_cust_svc
index 5497c72..d835f7b 100644 (file)
@@ -44,6 +44,14 @@ Tower name
 
 Disabled flag, empty or 'Y'
 
+=item up_rate_limit
+
+Up Rate limit for towner
+
+=item down_rate_limit
+
+Down Rate limit for tower
+
 =back
 
 =head1 METHODS
@@ -97,6 +105,8 @@ sub check {
     || $self->ut_floatn('height')
     || $self->ut_floatn('veg_height')
     || $self->ut_alphan('color')
+    || $self->ut_numbern('up_rate_limit')
+    || $self->ut_numbern('down_rate_limit')
   ;
   return $error if $error;
 
index 6ccfe55..350fce1 100644 (file)
@@ -2,7 +2,7 @@ package FS::tower_sector;
 
 use strict;
 use base qw( FS::Record );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( dbh qsearch qsearchs );
 use FS::tower;
 use FS::svc_broadband;
 use Class::Load qw(load_class);
@@ -93,6 +93,18 @@ The coverage map, as a PNG.
 
 The coordinate boundaries of the coverage map.
 
+=item title
+
+The sector title.
+
+=item up_rate_limit
+
+Up rate limit for sector.
+
+=item down_rate_limit
+
+down rate limit for sector.
+
 =back
 
 =head1 METHODS
@@ -155,6 +167,8 @@ sub check {
     || $self->ut_numbern('downtilt')
     || $self->ut_floatn('sector_range')
     || $self->ut_numbern('margin')
+    || $self->ut_numbern('up_rate_limit')
+    || $self->ut_numbern('down_rate_limit')
     || $self->ut_anything('image')
     || $self->ut_sfloatn('west')
     || $self->ut_sfloatn('east')
@@ -260,6 +274,25 @@ sub queue_generate_coverage {
 
 =over 4
 
+=item part_export_svc_broadband
+
+Returns all svc_broadband exports.
+
+=cut
+
+sub part_export_svc_broadband {
+  my $info = $FS::part_export::exports{'svc_broadband'} or return;
+  my @exporttypes = map { dbh->quote($_) } keys %$info or return;
+  qsearch({
+    'table'     => 'part_export',
+    'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
+  });
+}
+
+=back
+
+=over 4
+
 =item process_generate_coverage JOB, PARAMS
 
 Queueable routine to fetch the sector coverage map from the tower mapping
index 5411feb..f6ec208 100644 (file)
@@ -290,6 +290,20 @@ my $widget = new HTML::Widgets::SelectLayers(
     $html .= ' CHECKED' if $part_export->no_suspend eq 'Y';
     $html .= '></TD></TR>';
 
+    foreach my $script ( keys %{$exports->{$layer}{scripts}} ) {
+      $html .= '<TR><TD ALIGN="left" COLSPAN=2>' .
+        include('/elements/progress-init.html',
+              $part_export->exporttype,
+              [ $script.'_exportnum', $script.'_script' ],
+              rooturl().'view/svc_export/run_script.cgi',
+              rooturl().'edit/part_export.cgi?'.$part_export->{Hash}->{exportnum},
+              $script,
+        ) .
+        '<INPUT TYPE="hidden" NAME="'.$script.'_exportnum" VALUE="'.$part_export->{Hash}->{exportnum}.'">
+         <INPUT TYPE="hidden" NAME="'.$script.'_script" VALUE="'.$script.'">
+        <A HREF="#" onClick="'.$script.'process();">'.$exports->{$layer}{scripts}{$script}->{html_label}.'</A></TD></TR>';
+    }
+
     $html .= '</TABLE>';
 
     # false laziness with config_element above
index 60aaf74..e96fa0c 100644 (file)
@@ -459,6 +459,14 @@ foreach my $value ( @values ) {
 
 }
 
+if ($class eq "FS::tower") {
+  foreach my $part_svc_broadband_export ( FS::tower_sector->part_export_svc_broadband ) {
+    if ($part_svc_broadband_export and $part_svc_broadband_export->can('export_tower_sector')) {
+      $error = $part_svc_broadband_export->export_tower_sector($new);
+    }
+  }
+}
+
 # set up redirect URLs
 
 my $redirect;
index d14ac56..ba7309c 100644 (file)
@@ -5,7 +5,7 @@
                      'fields' => [qw(
                        sectorname ip_addr height freq_mhz direction width
                        downtilt v_width margin
-                       sector_range
+                       sector_range up_rate_limit down_rate_limit
                      )],
                    },
 &>
index 377a33e..b9fea77 100644 (file)
@@ -12,6 +12,8 @@
                         'altitude',
                         'height',
                         'veg_height',
+                        'up_rate_limit',
+                        'down_rate_limit',
                         { field             => 'sectornum',
                           type              => 'tower_sector',
                           o2m_table         => 'tower_sector',
@@ -30,6 +32,8 @@
                         'height'          => 'Height (feet)',
                         'veg_height'      => 'Vegetation height (feet)',
                         'color'           => 'Color',
+                        'up_rate_limit'   => 'Up Rate Limit(Kbps)',
+                        'down_rate_limit' => 'Down Rate Limit(Kbps)',
                       },
 &>
 <%init>
@@ -38,7 +42,8 @@ my $m2_error_callback = sub { # reconstruct the list
   my ($cgi, $object) = @_;
 
   my @fields = qw(
-    sectorname ip_addr height freq_mhz direction width tilt v_width margin sector_range
+    sectorname ip_addr height freq_mhz direction width tilt v_width margin 
+    sector_range up_rate_limit down_rate_limit
   );
 
   map {
index 9871775..722c5d7 100644 (file)
@@ -61,6 +61,8 @@ tie my %label, 'Tie::IxHash',
   'v_width'      => 'Vert. width',
   'sector_range' => 'Range',
   'margin'       => 'Signal margin (dB)',
+  'up_rate_limit' => 'Up rate limit',
+  'down_rate_limit' => 'Down rate limit',
 ;
 
 my @fields = keys %label;
diff --git a/httemplate/elements/tr-tower_sectors.html b/httemplate/elements/tr-tower_sectors.html
new file mode 100644 (file)
index 0000000..8acedb8
--- /dev/null
@@ -0,0 +1,310 @@
+<%shared>
+# kind of a hack...
+my ($export) = FS::tower_sector->part_export;
+my $antenna_types; # will be an ordered hash
+if ($export and $export->can('get_antenna_types')) {
+  $antenna_types = $export->get_antenna_types;
+}
+</%shared>
+<%init>
+my %opt = @_;
+my $tower = $opt{'object'};
+my $towernum = $tower->towernum;
+my $cgi = $opt{'cgi'};
+
+my $tabcounter = 0;
+
+my @fields = qw(
+  sectorname ip_addr height freq_mhz direction width downtilt v_width
+  db_high db_low sector_range
+  power line_loss antenna_gain hardware_typenum up_rate_limit down_rate_limit
+);
+
+my @sectors;
+if ( $cgi->param('error') ) {
+  foreach my $k ($cgi->param) {
+    if ($k =~ /^sectornum\d+$/) {
+      my $sectornum = $cgi->param($k);
+      my $sector = FS::tower_sector->new({
+        'sectornum' => $sectornum,
+        'towernum'  => $towernum,
+        map { $_ => scalar($cgi->param($k.'_'.$_)) } @fields,
+      });
+      push @sectors, $sector if length($sector->sectorname);
+    }
+  }
+} elsif ( $towernum ) {
+  @sectors = $tower->tower_sector;
+} # else new mode, no sectors yet
+
+my $id = $opt{id} || $opt{field} || 'sectornum';
+
+</%init>
+<& tablebreak-tr-title.html, value => 'Sectors' &>
+
+<style>
+  .ui-tabs-nav a {
+    padding: 6px 9px;
+    font-weight: bold;
+  }
+  .ui-tabs-nav li {
+    border-top-left-radius: 0.5em;
+    border-top-right-radius: 0.5em;
+  }
+  .ui-tabs-active li {
+    border-bottom-color: #fff;
+  }
+  .ui-tabs {
+    font-weight: bold;
+  }
+  .ui-tabs label {
+    padding-top: 3px;
+    width: 140px;
+    display: inline-block;
+    text-align: right;
+  }
+  .ui-tabs input, .ui-spinner {
+    border: 1px solid #666;
+    border-radius: 2px;
+    font-size: 13.3px;
+    text-align: right;
+    font-weight: normal;
+    padding: 1px;
+  }
+  .ui-tabs input { /* but not spinner, messes it up */
+    margin-left: 1px;
+    margin-right: 1px;
+  }
+  .ui-tabs input:focus {
+    border-color: #7e0079;
+    background-color: #ffffdd;
+  }
+  .ui-spinner input { /* use the spinner's border and padding */
+    border: none;
+    text-align: left;
+  }
+  .ui-tabs p {
+    margin-top: 8px;
+    margin-bottom: 8px;
+  }
+
+</style>
+
+
+<tr>
+  <td colspan=2>
+%# prototypes
+    <div style="display: none">
+<& .tab, id => $id . '_P' &>
+<& .panel, id => $id . '_P' &>
+    </div>
+
+%# main container
+    <div id="<% $id %>_tabs">
+      <ul>
+% foreach my $sector (@sectors) {
+<& .tab, sector => $sector, id => $id . $tabcounter &>
+%   $tabcounter++;
+% }
+      </ul>
+
+% $tabcounter = 0;
+% foreach my $sector (@sectors) {
+<& .panel, sector => $sector, id => $id . $tabcounter &>
+%   $tabcounter++;
+% }
+    </div>
+  </td>
+</tr>
+<script>
+$(function() {
+  var tabcounter = <% $tabcounter %>;
+  var id = <% $id |js_string %>;
+  //create tab bar
+  var tabs = $( '#'+id+'_tabs' ).tabs();
+
+  function changedSectorName() {
+    var this_panel = $(this).closest('div');
+    var this_tab = tabs.find('#' + this_panel.prop('id') + '_tab');
+    // if this is the last panel, make a new one
+    if (this_panel.next().length == 0) {
+      addSector();
+    }
+    // and update the current tab's text with the sector name
+    this_tab.find('a').text($(this).val());
+  }
+
+  var tab_proto = $('#'+id+'_P_tab');
+  var panel_proto = $('#'+id+'_P');
+
+  function addSector() {
+    var new_tab = tab_proto.clone();
+    var new_panel = panel_proto.clone();
+    // replace proto placeholder with the counter value, in all id and
+    // name properties in new_panel and its children
+    new_panel.add( new_panel.find('*') ).each(function() {
+      this.id = this.id.replace('_P', tabcounter);
+      if (this.name) {
+        this.name = this.name.replace('_P', tabcounter);
+      }
+    });
+    tabcounter++;
+    // and set the handler up on it
+    new_panel.find('.input-sectorname').on('change', changedSectorName);
+    
+    // also update the tab itself
+    new_tab.find('a').prop('href', '#' + new_panel.prop('id'));
+    new_tab.prop('id', new_panel.prop('id') + '_tab');
+
+    tabs.append(new_panel);
+    tabs.children('ul:first').append(new_tab);
+
+    tabs.tabs('refresh');
+  }
+
+  $('.dbspinner').spinner({ step: 5 });
+
+  $('.input-sectorname').on('change', changedSectorName);
+  addSector();
+
+});
+</script>
+<%def .tab>
+% my %opt = @_;
+% my $sector = $opt{sector};
+% my $id = $opt{id};
+% my $title = $sector ? $sector->sectorname : mt('Add new');
+      <li id="<% $id %>_tab">
+        <a href="#<% $id %>"><% $title |h %></a>
+      </li>
+</%def>
+<%def .panel>
+% my %opt = @_;
+% my $sector = $opt{sector} || FS::tower_sector->new({});
+% my $id = $opt{id}; # sectornumX
+<div id="<% $id %>">
+% # no id on this one, the panel gets the "sectornumX" id
+  <input type="hidden" name="<% $id %>" value="<% $sector->sectornum |h %>">
+  <p>
+    <label><% emt('Sector name') %></label>
+    <input style="text-align: left"
+           class="input-sectorname"
+           id="<% $id %>_sectorname"
+           name="<% $id %>_sectorname"
+           value="<% $sector->sectorname |h %>">
+
+    <label><% emt('IP address') %></label>
+    <input style="text-align: left"
+           id="<% $id %>_ip_addr"
+           name="<% $id %>_ip_addr"
+           value="<% $sector->ip_addr |h %>">
+  </p>
+  <p>
+    <label for="<% $id %>_height"><% emt('Antenna height') %></label>
+    <input size="3"
+           id="<% $id %>_height"
+           name="<% $id %>_height"
+           value="<% $sector->height |h %>">
+    <% emt('feet above ground') %>
+  </p>
+  <p>
+    <label for="<% $id %>_direction"><% emt('Azimuth') %></label>
+    <input size="3"
+           id="<% $id %>_direction"
+           name="<% $id %>_direction"
+           value="<% $sector->direction |h %>">&deg;
+    <label for="<% $id %>_downtilt"><% emt('Down tilt') %></label>
+    <input size="2"
+           id="<% $id %>_downtilt"
+           name="<% $id %>_downtilt"
+           value="<% $sector->downtilt |h %>">&deg;
+  </p>
+
+  <p>
+    <label for="<% $id %>_freq_mhz"><% emt('Frequency') %></label>
+    <input size="4"
+           id="<% $id %>_freq_mhz"
+           name="<% $id %>_freq_mhz"
+           value="<% $sector->freq_mhz |h %>">
+    <% emt('MHz') %>
+  </p>
+
+  <p>
+    <label for="<% $id %>_power"><% emt('Transmit power') %></label>
+    <input size="3"
+           id="<% $id %>_power"
+           name="<% $id %>_power"
+           value="<% $sector->power |h %>">
+    <% emt('dBm') %><br>
+    <label for="<% $id %>_antenna_gain">+ </label>
+    <input size="3"
+           id="<% $id %>_antenna_gain"
+           name="<% $id %>_antenna_gain"
+           value="<% $sector->antenna_gain |h %>">
+    <% emt('dB antenna gain') %><br>
+    <label for="<% $id %>_line_loss">&ndash; </label>
+    <input size="3"
+           id="<% $id %>_line_loss"
+           name="<% $id %>_line_loss"
+           value="<% $sector->line_loss |h %>">
+    <% emt('dB line loss') %>
+
+% if ( $antenna_types ) {
+  <p>
+    <label for="<% $id %>_hardware_typenum"><% emt('Antenna type') %></label>
+    <& /elements/select.html,
+      field   => $id.'_hardware_typenum',
+      options => [ '', keys %$antenna_types ],
+      labels  => $antenna_types,
+      curr_value => $sector->hardware_typenum,
+    &>
+  </p>
+% }
+% # this next section might not be necessary if you enter an antenna type
+  <p> 
+    <label for="<% $id %>_width"><% emt('Horizontal beam') %></label>
+    <input size="3"
+           id="<% $id %>_width"
+           name="<% $id %>_width"
+           value="<% $sector->width |h %>">&deg;
+    <label for="<% $id %>_v_width"><% emt('Vertical beam') %></label>
+    <input size="2"
+           id="<% $id %>_v_width"
+           name="<% $id %>_v_width"
+           value="<% $sector->v_width |h %>">&deg;
+  </p>
+
+  <label><% emt('Signal margin') %></label>
+  <div style="display: inline-block; vertical-align: top">
+      <input class="dbspinner"
+             size="4"
+             id="<% $id %>_db_high"
+             name="<% $id %>_db_high"
+             value="<% $sector->db_high |h %>">
+      <% emt('dB (high quality)') %>
+      <br>
+
+      <input class="dbspinner"
+             size="4"
+             id="<% $id %>_db_low"
+             name="<% $id %>_db_low"
+             value="<% $sector->db_low |h %>">
+      <% emt('dB (low quality)') %>
+  </div>
+  <p>
+  <label><% emt('Up Rate (Kbps)') %></label>
+    <input style="text-align: left"
+           id="<% $id %>_up_rate_limit"
+           name="<% $id %>_up_rate_limit"
+           value="<% $sector->up_rate_limit |h %>">
+  </p>
+  <p>
+    <label><% emt('Down Rate (Kbps)') %></label>
+    <input style="text-align: left"
+           id="<% $id %>_down_rate_limit"
+           name="<% $id %>_down_rate_limit"
+           value="<% $sector->down_rate_limit |h %>">
+  </p>
+
+</div>
+</%def>
diff --git a/httemplate/view/svc_export/run_script.cgi b/httemplate/view/svc_export/run_script.cgi
new file mode 100644 (file)
index 0000000..ba58bbd
--- /dev/null
@@ -0,0 +1,31 @@
+<% $server->process %>
+<%init>
+
+my @args = $cgi->param('arg');
+my %param = ();
+  while ( @args ) {
+    my( $field, $value ) = splice(@args, 0, 2);
+    unless ( exists( $param{$field} ) ) {
+      $param{$field} = $value;
+    } elsif ( ! ref($param{$field}) ) {
+      $param{$field} = [ $param{$field}, $value ];
+    } else {
+      push @{$param{$field}}, $value;
+    }
+  }
+
+my $exportnum;
+my $method;
+for (grep /^*_script$/, keys %param) { 
+       $exportnum = $param{$param{$_}.'_exportnum'};
+       $method = $param{$param{$_}.'_script'};
+}
+
+my $part_export = qsearchs('part_export', { 'exportnum'=> $exportnum, } )
+       or die "unknown exportnum $exportnum";
+
+my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$method;
+
+my $server = new FS::UI::Web::JSRPC $class, $cgi;
+
+</%init>
\ No newline at end of file