new 477 report: deployment info, combined browse-edit UI, #24047
authorMark Wells <mark@freeside.biz>
Fri, 1 Aug 2014 05:54:08 +0000 (22:54 -0700)
committerMark Wells <mark@freeside.biz>
Fri, 1 Aug 2014 05:54:08 +0000 (22:54 -0700)
25 files changed:
FS/FS/AccessRight.pm
FS/FS/Mason.pm
FS/FS/Record.pm
FS/FS/Report/FCC_477.pm
FS/FS/Schema.pm
FS/FS/deploy_zone.pm [new file with mode: 0644]
FS/FS/deploy_zone_block.pm [new file with mode: 0644]
FS/FS/deploy_zone_vertex.pm [new file with mode: 0644]
FS/FS/part_pkg_fcc_option.pm
FS/MANIFEST
FS/t/deploy_zone.t [new file with mode: 0644]
FS/t/deploy_zone_block.t [new file with mode: 0644]
FS/t/deploy_zone_vertex.t [new file with mode: 0644]
bin/convert-477-options
httemplate/browse/deploy_zone.html [new file with mode: 0644]
httemplate/browse/part_pkg-fcc.html [new file with mode: 0755]
httemplate/edit/deploy_zone-fixed.html [new file with mode: 0644]
httemplate/edit/part_pkg.cgi
httemplate/edit/process/deploy_zone-fixed.html [new file with mode: 0644]
httemplate/elements/deploy_zone_block.html [new file with mode: 0644]
httemplate/elements/input-fcc_options.html [new file with mode: 0644]
httemplate/elements/tr-input-fcc_options.html
httemplate/misc/part_pkg_fcc_options.html
httemplate/search/477.html
httemplate/search/report_477.html

index 9b9642e..8d4d67b 100644 (file)
@@ -359,6 +359,9 @@ tie my %rights, 'Tie::IxHash',
 
     'Bulk edit package definitions',
 
+    'Edit FCC report configuration',
+    { rightname => 'Edit FCC report configuration for all agents', global=>1 },
+
     'Edit CDR rates',
     #{ rightname=>'Edit global CDR rates', global=>1, },
 
index 4f43c6b..93eca5e 100644 (file)
@@ -387,6 +387,9 @@ if ( -e $addl_handler_use_file ) {
   use FS::state;
   use FS::state;
   use FS::queue_stat;
+  use FS::deploy_zone;
+  use FS::deploy_zone_block;
+  use FS::deploy_zone_vertex;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 24f2a25..2005756 100644 (file)
@@ -124,6 +124,8 @@ FS::Record - Database record objects
     $error = $record->ut_floatn('column');
     $error = $record->ut_number('column');
     $error = $record->ut_numbern('column');
+    $error = $record->ut_decimal('column');
+    $error = $record->ut_decimaln('column');
     $error = $record->ut_snumber('column');
     $error = $record->ut_snumbern('column');
     $error = $record->ut_money('column');
@@ -2434,6 +2436,35 @@ sub ut_numbern {
   '';
 }
 
+=item ut_decimal COLUMN[, DIGITS]
+
+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]
+
+Check/untaint decimal numbers.  May be null.  If there is an error, returns
+the error, otherwise returns false.
+
+=cut
+
+sub ut_decimal {
+  my($self, $field, $digits) = @_;
+  $digits ||= '';
+  $self->getfield($field) =~ /^\s*(\d+(\.\d{0,$digits})?)\s*$/
+    or return "Illegal or empty (decimal) $field: ".$self->getfield($field);
+  $self->setfield($field, $1);
+  '';
+}
+
+sub ut_decimaln {
+  my($self, $field, $digits) = @_;
+  $self->getfield($field) =~ /^\s*(\d*(\.\d{0,$digits})?)\s*$/
+    or return "Illegal (decimal) $field: ".$self->getfield($field);
+  $self->setfield($field, $1);
+  '';
+}
+
 =item ut_money COLUMN
 
 Check/untaint monetary numbers.  May be negative.  Set to 0 if null.  If there
index 79f00e3..bf4754d 100644 (file)
@@ -165,8 +165,6 @@ sub save_fcc477map {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  # lame (should be normal FS::Record access)
-
   my $sql = "delete from fcc477map where formkey = ?";
   my $sth = dbh->prepare($sql) or die dbh->errstr;
   $sth->execute($key) or do {
@@ -204,6 +202,8 @@ sub statenum2state {
   my $num = shift;
   $states{$num};
 }
+### everything above this point is unmaintained ###
+
 
 =head1 THE "NEW" REPORT (October 2014 and later)
 
@@ -259,15 +259,14 @@ sub is_fixed_broadband {
   ).")";
 }
 
-=item part6 OPTIONS
+=item report_fixed_broadband OPTIONS
 
-Returns Part 6 of the 2014 FCC 477 data, as an arrayref of arrayrefs.
-OPTIONS may contain:
+Returns the Fixed Broadband Subscription report (section 5.4), as an arrayref
+of an arrayrefs.  OPTIONS may contain:
 - date: a timestamp value to count active packages as of that date
 - agentnum: limit to customers of that agent
 
-Part 6 is the broadband subscription detail report.  Columns of the 
-report are:
+Columns of this report are:
 - census tract
 - technology code
 - downstream speed
@@ -278,7 +277,7 @@ report are:
 
 =cut
 
-sub part6 {
+sub report_fixed_broadband {
   my $class = shift;
   my %opt = shift;
   my $date = $opt{date} || time;
@@ -323,10 +322,67 @@ sub part6 {
   dbh->selectall_arrayref($statement);
 }
 
-=item part9 OPTIONS
+=item report_fixed_voice OPTIONS
+
+Returns the Fixed Voice Subscription Detail report (section 5.5).  OPTIONS
+are as above.  Columns are:
+
+- census tract
+- service type (0 for non-VoIP, 1 for VoIP)
+(the above columns form a key)
+- VGE lines/VoIP subscriptions in service
+- consumer grade VGE lines/VoIP subscriptions
+
+=cut
+
+sub report_fixed_voice {
+  my $class = shift;
+  my %opt = shift;
+  my $date = $opt{date} || time;
+  my $agentnum = $opt{agentnum};
+
+  my @select = (
+    'cust_location.censustract',
+    # VoIP indicator (0 for non-VoIP, 1 for VoIP)
+    'COALESCE(is_voip, 0)',
+    # number of lines/subscriptions
+    'SUM(CASE WHEN is_voip = 1 THEN 1 ELSE phone_lines END)',
+    # consumer grade lines/subscriptions
+    'SUM(CASE WHEN is_consumer = 1 THEN ( CASE WHEN is_voip = 1 THEN voip_sessions ELSE phone_lines END) ELSE 0 END)'
+  );
+
+  my $from = 'cust_pkg
+    JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
+    JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
+    JOIN part_pkg USING (pkgpart) '.
+    join_optionnames_int(qw(
+      is_phone is_voip is_consumer phone_lines voip_sessions
+      ))
+  ;
+
+  my @where = (
+    active_on($date),
+    "(is_voip = 1 OR is_phone = 1)",
+  );
+  push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+  my $group_by = 'cust_location.censustract, COALESCE(is_voip, 0)';
+  my $order_by = $group_by;
+
+  my $statement = "SELECT ".join(', ', @select) . "
+  FROM $from
+  WHERE ".join(' AND ', @where)."
+  GROUP BY $group_by
+  ORDER BY $order_by
+  ";
+
+  warn $statement if $DEBUG;
+  dbh->selectall_arrayref($statement);
+}
+
+=item report_local_phone OPTIONS
 
-Returns Part 9 of the 2014 FCC 477 data.  Part 9 is the Local Exchange 
-Telephone Subscription report.  Columns are:
+Returns the Local Exchange Telephone Subscription report (section 5.6).  
+OPTIONS are as above.  Each row is data for one state.  Columns are:
 
 - state FIPS code (key)
 - wholesale switched voice lines
@@ -346,7 +402,7 @@ Telephone Subscription report.  Columns are:
 
 =cut
 
-sub part9 {
+sub report_local_phone {
   my $class = shift;
   my %opt = shift;
   my $date = $opt{date} || time;
@@ -358,10 +414,10 @@ sub part9 {
     "SUM(phone_circuits)",
     "SUM(phone_lines)",
     "SUM(CASE WHEN is_broadband = 1 THEN phone_lines ELSE 0 END)",
-    "SUM(CASE WHEN is_consumer = 1 AND is_longdistance IS NULL THEN phone_lines ELSE 0 END)",
-    "SUM(CASE WHEN is_consumer = 1 AND is_longdistance = 1 THEN phone_lines ELSE 0 END)",
-    "SUM(CASE WHEN is_consumer IS NULL AND is_longdistance IS NULL THEN phone_lines ELSE 0 END)",
-    "SUM(CASE WHEN is_consumer IS NULL AND is_longdistance = 1 THEN phone_lines ELSE 0 END)",
+    "SUM(CASE WHEN is_consumer = 1 AND phone_longdistance IS NULL THEN phone_lines ELSE 0 END)",
+    "SUM(CASE WHEN is_consumer = 1 AND phone_longdistance = 1 THEN phone_lines ELSE 0 END)",
+    "SUM(CASE WHEN is_consumer IS NULL AND phone_longdistance IS NULL THEN phone_lines ELSE 0 END)",
+    "SUM(CASE WHEN is_consumer IS NULL AND phone_longdistance = 1 THEN phone_lines ELSE 0 END)",
     "SUM(CASE WHEN phone_localloop = 'owned' THEN phone_lines ELSE 0 END)",
     "SUM(CASE WHEN phone_localloop = 'leased' THEN phone_lines ELSE 0 END)",
     "SUM(CASE WHEN phone_localloop = 'resale' THEN phone_lines ELSE 0 END)",
@@ -378,7 +434,7 @@ sub part9 {
       join_optionnames_int(qw(
         is_phone is_broadband
         phone_vges phone_circuits phone_lines
-        is_consumer is_longdistance
+        is_consumer phone_longdistance
         )).
       join_optionnames('media', 'phone_localloop')
   ;
@@ -401,7 +457,26 @@ sub part9 {
   dbh->selectall_arrayref($statement);
 }
 
-sub part10 {
+=item report_voip OPTIONS
+
+Returns the Interconnected VoIP Subscription report (section 5.7).  
+OPTIONS are as above.  Columns are:
+
+- state FIPS code (key)
+- OTT subscriptions (non-last-mile)
+- OTT subscriptions sold to consumers
+- last-mile subscriptions
+- last-mile subscriptions sold to consumers
+- last-mile subscriptions bundled with broadband Internet
+- last-mile subscriptions over copper pairs
+- last-mile subscriptions over coaxial
+- last-mile subscriptions over fiber
+- last-mile subscriptions over fixed wireless
+- last-mile subscriptions over other media
+
+=cut
+
+sub report_voip {
   my $class = shift;
   my %opt = shift;
   my $date = $opt{date} || time;
@@ -453,54 +528,5 @@ sub part10 {
   dbh->selectall_arrayref($statement);
 }
 
-=item part11 OPTIONS
-
-Returns part 11 (voice subscription detail), as above.
-
-=cut
-
-sub part11 {
-  my $class = shift;
-  my %opt = shift;
-  my $date = $opt{date} || time;
-  my $agentnum = $opt{agentnum};
-
-  my @select = (
-    'cust_location.censustract',
-    # VoIP indicator (0 for non-VoIP, 1 for VoIP)
-    'COALESCE(is_voip, 0)',
-    # number of lines/subscriptions
-    'SUM(CASE WHEN is_voip = 1 THEN 1 ELSE phone_lines END)',
-    # consumer grade lines/subscriptions
-    'SUM(CASE WHEN is_consumer = 1 THEN ( CASE WHEN is_voip = 1 THEN 1 ELSE phone_lines END) ELSE 0 END)'
-  );
-
-  my $from = 'cust_pkg
-    JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
-    JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
-    JOIN part_pkg USING (pkgpart) '.
-    join_optionnames_int(qw(
-      is_phone is_voip is_consumer phone_lines
-      ))
-  ;
-
-  my @where = (
-    active_on($date),
-    "(is_voip = 1 OR is_phone = 1)",
-  );
-  push @where, "cust_main.agentnum = $agentnum" if $agentnum;
-  my $group_by = 'cust_location.censustract, COALESCE(is_voip, 0)';
-  my $order_by = $group_by;
-
-  my $statement = "SELECT ".join(', ', @select) . "
-  FROM $from
-  WHERE ".join(' AND ', @where)."
-  GROUP BY $group_by
-  ORDER BY $order_by
-  ";
-
-  warn $statement if $DEBUG;
-  dbh->selectall_arrayref($statement);
-}
 
 1;
index 27bd813..830b39a 100644 (file)
@@ -6672,6 +6672,75 @@ sub tables_hashref {
       'index' => [],
     },
 
+    # eventually link to tower/sector?
+    'deploy_zone' => {
+      'columns' => [
+        'zonenum',        'serial',  '',     '',      '', '',
+        'description',    'char',    'NULL', $char_d, '', '',
+        'agentnum',       'int',     '',     '',      '', '',
+        'dbaname',        'char',    'NULL', $char_d, '', '',
+        'zonetype',       'char',    '',     1,       '', '',
+        'technology',     'int',     '',     '',      '', '',
+        'spectrum',       'int',     'NULL', '',      '', '',
+        'servicetype',    'char',    '',     '12',    '', '',
+        'adv_speed_up',   'decimal', 'NULL', '10,3',  '', '',
+        'adv_speed_down', 'decimal', 'NULL', '10,3',  '', '',
+        'cir_speed_up',   'decimal', 'NULL', '10,3',  '', '',
+        'cir_speed_down', 'decimal', 'NULL', '10,3',  '', '',
+        'is_consumer',    'char',    'NULL', 1,       '', '',
+        'is_business',    'char',    'NULL', 1,       '', '',
+      ],
+      'primary_key' => 'zonenum',
+      'unique' => [],
+      'index'  => [ [ 'agentnum' ] ],
+      'foreign_keys' => [
+                          { columns     => [ 'agentnum' ],
+                            table       => 'agent',
+                            references  => [ 'agentnum' ],
+                          },
+                        ],
+    },
+
+    'deploy_zone_block' => {
+      'columns' => [
+        'blocknum',       'serial',  '',     '',      '', '',
+        'zonenum',        'int',     '',     '',      '', '',
+        'censusblock',    'char',    '',     15,      '', '',
+        'censusyear',     'char',    '',      4,      '', '',
+      ],
+      'primary_key' => 'blocknum',
+      'unique' => [],
+      'index'  => [ [ 'zonenum' ] ],
+      'foreign_keys' => [
+                          { columns     => [ 'zonenum' ],
+                            table       => 'deploy_zone',
+                            references  => [ 'zonenum' ],
+                          },
+                        ],
+    },
+
+    'deploy_zone_vertex' => {
+      'columns' => [
+        'vertexnum',      'serial',  '',     '',      '', '',
+        'zonenum',        'int',     '',     '',      '', '',
+        'latitude',       'decimal', '',     '10,7',  '', '', 
+        'longitude',      'decimal', '',     '10,7',  '', '', 
+        'sequence',       'int',     '',     '',      '', '',
+      ],
+      'primary_key' => 'vertexnum',
+      'unique' => [ [ 'zonenum', 'sequence' ] ],
+      'index'  => [ ],
+      'foreign_keys' => [
+                          { columns     => [ 'zonenum' ],
+                            table       => 'deploy_zone',
+                            references  => [ 'zonenum' ],
+                          },
+                        ],
+    },
+
+
+
+
 
     # name type nullability length default local
 
diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm
new file mode 100644 (file)
index 0000000..3caeda2
--- /dev/null
@@ -0,0 +1,221 @@
+package FS::deploy_zone;
+
+use strict;
+use base qw( FS::o2m_Common FS::Record );
+use FS::Record qw( qsearch qsearchs dbh );
+
+=head1 NAME
+
+FS::deploy_zone - Object methods for deploy_zone records
+
+=head1 SYNOPSIS
+
+  use FS::deploy_zone;
+
+  $record = new FS::deploy_zone \%hash;
+  $record = new FS::deploy_zone { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::deploy_zone object represents a geographic zone where a certain kind
+of service is available.  Currently we store this information to generate
+the FCC Form 477 deployment reports, but it may find other uses later.
+
+FS::deploy_zone inherits from FS::Record.  The following fields are currently
+supported:
+
+=over 4
+
+=item zonenum
+
+primary key
+
+=item description
+
+Optional text describing the zone.
+
+=item agentnum
+
+The agent that serves this zone.
+
+=item dbaname
+
+The name under which service is marketed in this zone.  If null, will 
+default to the agent name.
+
+=item zonetype
+
+The way the zone geography is defined: "B" for a list of census blocks
+(used by the FCC for fixed broadband service), "P" for a polygon (for 
+mobile services).  See L<FS::deploy_zone_block> and L<FS::deploy_zone_vertex>.
+
+=item technology
+
+The FCC technology code for the type of service available.
+
+=item spectrum
+
+For mobile service zones, the FCC code for the RF band.
+
+=item servicetype
+
+"broadband" or "voice"
+
+=item adv_speed_up
+
+For broadband, the advertised upstream bandwidth in the zone.  If multiple
+speed tiers are advertised, use the highest.
+
+=item adv_speed_down
+
+For broadband, the advertised downstream bandwidth in the zone.
+
+=item cir_speed_up
+
+For broadband, the contractually guaranteed upstream bandwidth, if that type
+of service is sold.
+
+=item cir_speed_down
+
+For broadband, the contractually guaranteed downstream bandwidth, if that 
+type of service is sold.
+
+=item is_consumer
+
+'Y' if this service is sold for consumer/household use.
+
+=item is_business
+
+'Y' if this service is sold to business or institutional use.  Not mutually
+exclusive with is_consumer.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new zone.  To add the zone to the database, see L<"insert">.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'deploy_zone'; }
+
+=item insert ELEMENTS
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  # clean up linked records
+  my $self = shift;
+  my $error = $self->process_o2m(
+    'table'   => $self->element_table,
+    'num_col' => 'zonenum',
+    'fields'  => 'zonenum',
+    'params'  => {},
+  ) || $self->SUPER::delete(@_);
+  
+  if ($error) {
+    dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+  '';
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid zone record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('zonenum')
+    || $self->ut_textn('description')
+    || $self->ut_number('agentnum')
+    || $self->ut_foreign_key('agentnum', 'agent', 'agentnum')
+    || $self->ut_alphan('dbaname')
+    || $self->ut_enum('zonetype', [ 'B', 'P' ])
+    || $self->ut_number('technology')
+    || $self->ut_numbern('spectrum')
+    || $self->ut_enum('servicetype', [ 'broadband', 'voice' ])
+    || $self->ut_decimaln('adv_speed_up', 3)
+    || $self->ut_decimaln('adv_speed_down', 3)
+    || $self->ut_decimaln('cir_speed_up', 3)
+    || $self->ut_decimaln('cir_speed_down', 3)
+    || $self->ut_flag('is_consumer')
+    || $self->ut_flag('is_business')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item element_table
+
+Returns the name of the table that contains the zone's elements (blocks or
+vertices).
+
+=cut
+
+sub element_table {
+  my $self = shift;
+  if ($self->zonetype eq 'B') {
+    return 'deploy_zone_block';
+  } elsif ( $self->zonetype eq 'P') {
+    return 'deploy_zone_vertex';
+  } else {
+    die 'unknown zonetype';
+  }
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/deploy_zone_block.pm b/FS/FS/deploy_zone_block.pm
new file mode 100644 (file)
index 0000000..58234b9
--- /dev/null
@@ -0,0 +1,126 @@
+package FS::deploy_zone_block;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::deploy_zone_block - Object methods for deploy_zone_block records
+
+=head1 SYNOPSIS
+
+  use FS::deploy_zone_block;
+
+  $record = new FS::deploy_zone_block \%hash;
+  $record = new FS::deploy_zone_block { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::deploy_zone_block object represents a census block that's part of
+a deployment zone.  FS::deploy_zone_block inherits from FS::Record.  The 
+following fields are currently supported:
+
+=over 4
+
+=item blocknum
+
+primary key
+
+=item zonenum
+
+L<FS::deploy_zone> foreign key for the zone.
+
+=item censusblock
+
+U.S. census block number (15 digits).
+
+=item censusyear
+
+The year of the census map where the block appeared or was last verified.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new block entry.  To add the recordto the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'deploy_zone_block'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('blocknum')
+    || $self->ut_number('zonenum')
+    || $self->ut_number('censusblock')
+    || $self->ut_number('censusyear')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/deploy_zone_vertex.pm b/FS/FS/deploy_zone_vertex.pm
new file mode 100644 (file)
index 0000000..a25bfde
--- /dev/null
@@ -0,0 +1,125 @@
+package FS::deploy_zone_vertex;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::deploy_zone_vertex - Object methods for deploy_zone_vertex records
+
+=head1 SYNOPSIS
+
+  use FS::deploy_zone_vertex;
+
+  $record = new FS::deploy_zone_vertex \%hash;
+  $record = new FS::deploy_zone_vertex { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::deploy_zone_vertex object represents a vertex of a polygonal 
+deployment zone (L<FS::deploy_zone>).  FS::deploy_zone_vertex inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item vertexnum
+
+primary key
+
+=item zonenum
+
+Foreign key to L<FS::deploy_zone>.
+
+=item latitude
+
+Latitude, as a decimal; positive values are north of the Equator.
+
+=item longitude
+
+Longitude, as a decimal; positive values are east of Greenwich.
+
+=item sequence
+
+The ordinal position of this vertex, starting with zero.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new vertex record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'deploy_zone_vertex'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid vertex.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('vertexnum')
+    || $self->ut_number('zonenum')
+    || $self->ut_coord('latitude')
+    || $self->ut_coord('longitude')
+    || $self->ut_number('sequence')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
index 0a288de..a090b96 100644 (file)
@@ -112,31 +112,31 @@ tie our %media_types, 'Tie::IxHash', (
   'Other'           => [ 90, 0 ],
 );
 
-our %technology_labels = (
-      10 => 'Other ADSL',
-      11 => 'ADSL2',
-      12 => 'VDSL',
-      20 => 'SDSL',
-      30 => 'Other Copper Wireline',
-      40 => 'Other Cable Modem',
-      41 => 'Cable - DOCSIS 1, 1.1, 2.0',
-      42 => 'Cable - DOCSIS 3.0',
-      50 => 'Fiber',
-      60 => 'Satellite',
-      70 => 'Terrestrial Fixed Wireless',
-      # mobile wireless
-      80 => 'Mobile - WCDMA/UMTS/HSPA',
-      81 => 'Mobile - HSPA+',
-      82 => 'Mobile - EVDO/EVDO Rev A',
-      83 => 'Mobile - LTE',
-      84 => 'Mobile - WiMAX',
-      85 => 'Mobile - CDMA',
-      86 => 'Mobile - GSM',
-      87 => 'Mobile - Analog',
-      88 => 'Other Mobile',
-
-      90 => 'Electric Power Line',
-      0  => 'Other'
+tie our %technology_labels, 'Tie::IxHash',  (
+  10 => 'Other ADSL',
+  11 => 'ADSL2',
+  12 => 'VDSL',
+  20 => 'SDSL',
+  30 => 'Other Copper Wireline',
+  40 => 'Other Cable Modem',
+  41 => 'Cable - DOCSIS 1, 1.1, 2.0',
+  42 => 'Cable - DOCSIS 3.0',
+  50 => 'Fiber',
+  60 => 'Satellite',
+  70 => 'Terrestrial Fixed Wireless',
+  # mobile wireless
+  80 => 'Mobile - WCDMA/UMTS/HSPA',
+  81 => 'Mobile - HSPA+',
+  82 => 'Mobile - EVDO/EVDO Rev A',
+  83 => 'Mobile - LTE',
+  84 => 'Mobile - WiMAX',
+  85 => 'Mobile - CDMA',
+  86 => 'Mobile - GSM',
+  87 => 'Mobile - Analog',
+  88 => 'Other Mobile',
+
+  90 => 'Electric Power Line',
+  0  => 'Other'
 );
 
 sub media_types {
@@ -144,7 +144,7 @@ sub media_types {
 }
 
 sub technology_labels {
-  +{ %technology_labels };
+  Storable::dclone(\%technology_labels);
 }
 
 =head1 BUGS
index 693904d..9a9573d 100644 (file)
@@ -812,3 +812,9 @@ FS/state.pm
 t/state.t
 FS/queue_stat.pm
 t/queue_stat.t
+FS/deploy_zone.pm
+t/deploy_zone.t
+FS/deploy_zone_block.pm
+t/deploy_zone_block.t
+FS/deploy_zone_vertex.pm
+t/deploy_zone_vertex.t
diff --git a/FS/t/deploy_zone.t b/FS/t/deploy_zone.t
new file mode 100644 (file)
index 0000000..d220e81
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::deploy_zone;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/deploy_zone_block.t b/FS/t/deploy_zone_block.t
new file mode 100644 (file)
index 0000000..c3241b1
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::deploy_zone_block;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/deploy_zone_vertex.t b/FS/t/deploy_zone_vertex.t
new file mode 100644 (file)
index 0000000..78c079f
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::deploy_zone_vertex;
+$loaded=1;
+print "ok 1\n";
index 2b8970a..99a6ea5 100755 (executable)
@@ -121,7 +121,7 @@ for my $part_pkg (qsearch('part_pkg', { freq => {op => '!=', value => '0'}})) {
     if ($part_pkg->fcc_voip_class) {
       # there's no such thing as a VoIP DS0 equivalent, but this is
       # what we used the field for
-      push @fcc_opts, 'voip_lines' => $part_pkg->fcc_ds0s;
+      push @fcc_opts, 'voip_sessions' => $part_pkg->fcc_ds0s;
     } else {
       push @fcc_opts, 'phone_lines' => $part_pkg->fcc_ds0s, 'is_phone' => 1;
     }
diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html
new file mode 100644 (file)
index 0000000..489a226
--- /dev/null
@@ -0,0 +1,72 @@
+<& /elements/header.html, 'Deployment zones' &>
+<& /elements/menubar.html,
+  'Add a new fixed broadband zone' => $p.'edit/deploy_zone-fixed.html',
+  'Add a new mobile zone'          => $p.'edit/deploy_zone-mobile.html',
+&>
+<P><FONT SIZE="+1"><B>Fixed Broadband Zones</B></FONT></P>
+<& elements/browse.html,
+  name_singular   => 'zone',
+  query           => { table      => 'deploy_zone',
+                       hashref    => { zonetype => 'B' },
+                     },
+  count_query     => "SELECT COUNT(*) FROM deploy_zone WHERE zonetype = 'B'",
+  agent_virt      => 1,
+  header          => [  '#',
+                        'Description',
+                        'Technology',
+                        'Market',
+                        'Advertised Mbps',
+                        'Contractual Mbps',
+                        'Census blocks',
+                     ],
+  fields          => [  'zonenum',
+                        'description',
+                        sub { my $self = shift;
+                              $tech_label->{$self->technology} },
+                        sub { my $self = shift;
+                              join( ' / ',
+                                $self->is_consumer ? 'consumer' : (),
+                                $self->is_business ? 'business' : ()
+                              )
+                            },
+                        sub { my $self = shift;
+                              join( ' / ', grep $_,
+                                $self->adv_speed_down,
+                                $self->adv_speed_up
+                              )
+                            },
+                        sub { my $self = shift;
+                              join( ' / ', grep $_,
+                                $self->cir_speed_down,
+                                $self->cir_speed_up
+                              )
+                            },
+                        sub { my $self = shift;
+                              FS::deploy_zone_block->count('zonenum = '.$self->zonenum)
+                            },
+                     ],
+  sort_fields     => [ 'zonenum',
+                       'description',
+                       'technology',
+                       'is_consumer is not null, is_business is not null',
+                       'adv_speed_down, adv_speed_up',
+                       'cir_speed_down, cir_speed_up',
+                     ],
+  links           => [  '', $link_fixed, ],
+  align           => 'clllllr',
+  nohtmlheader    => 1,
+  disable_maxselect => 1,
+  disable_total     => 1,
+&>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit FCC report configuration');
+my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents');
+die "access denied"
+  unless $acl_edit or $acl_edit_global;
+
+my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ];
+my $link_mobile= [ $p.'edit/deploy_zone-mobile.html', 'zonenum' ];
+
+my $tech_label = FS::part_pkg_fcc_option->technology_labels;
+</%init>
diff --git a/httemplate/browse/part_pkg-fcc.html b/httemplate/browse/part_pkg-fcc.html
new file mode 100755 (executable)
index 0000000..9462c32
--- /dev/null
@@ -0,0 +1,215 @@
+<& elements/browse.html,
+  'title'                 => 'Package Definitions - FCC Options',
+  'menubar'               => \@menubar,
+  'html_init'             => $html_init,
+  'html_form'             => $html_form,
+  'html_posttotal'        => $html_posttotal,
+  'name'                  => 'package definitions',
+  'disableable'           => 1,
+  'disabled_statuspos'    => 4,
+  'agent_virt'            => 1,
+  'agent_null_right'      => [ $edit, $edit_global ],
+  'agent_null_right_link' => $edit_global,
+  'agent_pos'             => 6,
+  'query'                 =>
+                            { 'select'    => $select,
+                              'table'     => 'part_pkg',
+                              'addl_from' => $addl_from,
+                              'hashref'   => \%hash,
+                              'extra_sql' => $extra_sql,
+                              'order_by'  => "ORDER BY $orderby"
+                            },
+  'count_query'           => $count_query,
+  'header'                => \@header,
+  'fields'                => \@fields,
+  'links'                 => \@links,
+  'align'                 => $align,
+  'link_field'            => 'pkgpart',
+  'html_foot'             => $html_foot,
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my $edit        = 'Edit package definitions';
+my $edit_global = 'Edit global package definitions';
+my $acl_edit        = $curuser->access_right($edit);
+my $acl_edit_global = $curuser->access_right($edit_global);
+
+die "access denied"
+  unless $acl_edit || $acl_edit_global;
+
+my $conf = new FS::Conf;
+
+my $orderby = 'pkgpart';
+my %hash = ();
+my $extra_count = '';
+
+my @where = ();
+
+# only ever show recurring packages here
+$hash{'freq'} = { op=>'!=', value=>'0' };
+$extra_count = " freq != '0' ";
+
+# filter by classnum
+my $classnum = '';
+if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
+  $classnum = $1;
+  push @where, $classnum ? "classnum =  $classnum"
+                         : "classnum IS NULL";
+}
+$cgi->delete('classnum');
+
+# filter by agent permissions
+push @where, FS::part_pkg->curuser_pkgs_sql
+  unless $acl_edit_global;
+
+my $extra_sql = scalar(@where)
+                ? ( scalar(keys %hash) ? ' AND ' : ' WHERE ' ).
+                  join( 'AND ', @where)
+                : '';
+
+# pull option values into the select
+my @optionnames = ( qw(
+  media
+  is_consumer
+  is_broadband technology broadband_upstream broadband_downstream
+  is_phone phone_wholesale phone_vges phone_circuits 
+  phone_lines phone_longdistance phone_localloop
+  is_voip voip_lastmile voip_sessions
+) );
+
+my $select = join(',',
+  'part_pkg.*',
+  '(SELECT classname FROM pkg_class WHERE pkg_class.classnum = part_pkg.classnum) AS classname', # grr, disableable...
+  @optionnames
+);
+
+my $addl_from = 
+  FS::Report::FCC_477::join_optionnames(@optionnames);
+
+#restore this so pagination works
+$cgi->param('classnum', $classnum) if length($classnum);
+
+#should hide this if there aren't any classes
+my $html_posttotal =
+  "<BR>( show class: ".
+  include('/elements/select-pkg_class.html',
+            #'curr_value'    => $classnum,
+            'value'         => $classnum, #insist on 0 :/
+            'onchange'      => 'filter_change()',
+            'pre_options'   => [ '-1' => 'all',
+                                 '0'  => '(none)', ],
+            'disable_empty' => 1,
+         ).
+  ' )';
+
+my $link = [ $p.'edit/part_pkg.cgi?', 'pkgpart' ];
+
+my @header = ( '#', 'Package', 'Comment' );
+my @fields = ( 'pkgpart', 'pkg', 'comment' ,);
+my $align = 'rll';
+my @links = ( $link, $link, '', );
+
+unless ( length($classnum) ) {
+  push @header, 'Class';
+  push @fields, 'classname';
+  $align .= 'l';
+}
+
+# still include the report_option classes, to help with migration
+# but not other plan options
+
+my %report_optionname_name = map { 'report_option_'.$_->num, $_->name }
+  qsearch('part_pkg_report_option', { disabled => '' });
+
+push @header, 'Report classes';
+
+push @fields, 
+              sub {
+                    my $part_pkg = shift;
+                    my %options = $part_pkg->options;
+                    # gather any options that are really report options,
+                    # convert them to their user-friendly names,
+                    # and sort them (I think?)
+                    my @report_options =
+                      sort { $a cmp $b }
+                      map { $report_optionname_name{$_} }
+                      grep { $options{$_}
+                             and exists($report_optionname_name{$_}) }
+                      keys %options;
+
+                    my @rows;
+                    foreach (@report_options) {
+                      push @rows, [
+                        { 'data'  => $_,
+                          'align' => 'center',
+                          'colspan' => 2
+                        }
+                      ];
+                    } # foreach @report_options
+                    \@rows;
+                  };
+
+$align .= 'cr';
+
+# --------
+# now the FCC option part
+# --------
+
+my @pkgparts;
+push @header, 'FCC report parameters';
+push @fields, sub {
+  my $part_pkg = shift;
+  my %hash = $part_pkg->fcc_options;
+  include('/elements/input-fcc_options.html',
+            id          => 'pkgpart'.$part_pkg->pkgpart,
+            curr_value  => encode_json(\%hash),
+            html_only   => 1
+  );
+};
+$align .= 'l';
+
+my $count_extra_sql = $extra_sql;
+$count_extra_sql =~ s/^\s*AND /WHERE /i;
+$extra_count = ( $count_extra_sql ? ' AND ' : ' WHERE ' ). $extra_count
+  if $extra_count;
+my $count_query = "SELECT COUNT(*) FROM part_pkg $count_extra_sql $extra_count";
+
+my $html_init = 
+  include('/elements/init_overlib.html') .
+  include('/elements/input-fcc_options.html', js_only => 1) .
+  include('.style');
+
+my $html_form = '';
+my $html_foot = '';
+# insert a checkbox column
+unshift @header, '';
+unshift @fields, sub {
+  '<INPUT TYPE="checkbox" NAME="pkgpart" VALUE=' . $_[0]->pkgpart .'>';
+};
+unshift @links, '';
+$align = 'c'.$align;
+
+
+$html_form = qq!<FORM ACTION="${p}edit/process/bulk-part_pkg-fcc.html" METHOD="POST">!;
+$html_foot = qq!
+  <INPUT TYPE="submit" VALUE="Save changes">
+  </FORM>!;
+
+my @menubar =
+  ( 'Package definitions' => $p.'browse/part_pkg.cgi' );
+
+</%init>
+<%def .style>
+<style>
+  ul.fcc_options {
+    text-align: left;
+  }
+  ul.fcc_options li {
+  }
+  button.edit_fcc_options {
+    float: right;
+  }
+</style>
+</%def>
diff --git a/httemplate/edit/deploy_zone-fixed.html b/httemplate/edit/deploy_zone-fixed.html
new file mode 100644 (file)
index 0000000..ecec9c4
--- /dev/null
@@ -0,0 +1,87 @@
+<& elements/edit.html,
+    'name_singular' => 'deployment zone',
+    'table'         => 'deploy_zone',
+    'post_url'      => popurl(1).'process/deploy_zone-fixed.html',
+    'labels'        => {
+        'description'     => 'Description',
+        'agentnum'        => 'Agent',
+        'dbaname'         => 'Business name (if different from agent)',
+        'technology'      => 'Technology',
+        'adv_speed_up'    => 'Upstream',
+        'adv_speed_down'  => 'Downstream',
+        'cir_speed_up'    => 'Upstream',
+        'cir_speed_down'  => 'Downstream',
+        'is_consumer'     => 'Consumer/mass market',
+        'is_business'     => 'Business/government',
+        'blocknum'        => '',
+    },
+    'fields'        => [
+        { field         => 'zonetype',
+          type          => 'hidden',
+          value         => 'B'
+        },
+        { field         => 'servicetype',
+          type          => 'hidden',
+          value         => 'broadband'
+        },
+        'description',
+        { field         => 'agentnum',
+          type          => 'select-agent',
+          disable_empty => 1,
+          viewall_right => 'Edit FCC report configuration for all agents',
+        },
+        'dbaname',
+        { field         => 'technology',
+          type          => 'select',
+          options       => [ keys(%$technology_labels) ],
+          labels        => $technology_labels,
+        },
+        { field         => 'is_consumer', type => 'checkbox', value=>'Y' },
+        { field         => 'is_business', type => 'checkbox', value=>'Y' },
+        { type => 'tablebreak-tr-title',
+          value => 'Advertised maximum speed (Mbps)' },
+        'adv_speed_down',
+        'adv_speed_up',
+        { type => 'tablebreak-tr-title',
+          value => 'Contractually guaranteed speed (Mbps)' },
+        'cir_speed_down',
+        'cir_speed_up',
+
+        { type => 'tablebreak-tr-title', value => 'Census blocks'},
+        { field => 'blocknum',
+          type              => 'deploy_zone_block',
+          o2m_table         => 'deploy_zone_block',
+          m2_label          => ' ',
+          m2_error_callback => $m2_error_callback,
+        },
+    ],
+
+&>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+die "access denied"
+  unless $curuser->access_right([
+    'Edit FCC report configuration',
+    'Edit FCC report configuration for all agents',
+  ]);
+
+my $technology_labels = FS::part_pkg_fcc_option->technology_labels;
+
+my $m2_error_callback = sub {
+  my ($cgi, $deploy_zone) = @_;
+  my @blocknums = grep {
+    /^blocknum\d+/ and length($cgi->param($_.'_censusblock'))
+  } $cgi->param;
+
+  map {
+    my $k = $_;
+    FS::deploy_zone_block->new({
+      blocknum    => scalar($cgi->param($k)),
+      zonenum     => $deploy_zone->zonenum,
+      censusblock => scalar($cgi->param($k.'_censusblock')),
+      censusyear  => scalar($cgi->param($k.'_censusyear')),
+    })
+  } @blocknums;
+};
+
+</%init>
index 9e55d9f..65eca6c 100755 (executable)
                        },
                      },
 
-                     { type  => 'tablebreak-tr-title',
-                       value => 'FCC Form 477 information',
-                     },
-                     { field => 'fcc_options_string',
-                       type  => 'input-fcc_options',
-                       curr_value_callback => sub {
-                         my ($cgi, $part_pkg, $fref) = @_;
-                         if ( $cgi->param('fcc_options_string') ) {
-                           # error redirect
-                           return $cgi->param('fcc_options_string');
-                         }
-                         my %hash;
-                         %hash = $part_pkg->fcc_options 
-                           if ($part_pkg->pkgpart);
-                         return encode_json(\%hash);
+                     ($fcc_opts ? (
+                       { type  => 'tablebreak-tr-title',
+                         value => 'FCC Form 477 information',
                        },
-                     },
+                       { field => 'fcc_options_string',
+                         type  => 'input-fcc_options',
+                         curr_value_callback => sub {
+                           my ($cgi, $part_pkg, $fref) = @_;
+                           if ( $cgi->param('fcc_options_string') ) {
+                             # error redirect
+                             return $cgi->param('fcc_options_string');
+                           }
+                           my %hash;
+                           %hash = $part_pkg->fcc_options 
+                             if ($part_pkg->pkgpart);
+                           return encode_json(\%hash);
+                         },
+                       },
+                       ) : ()
+                     ),
 
                      { type  => 'tablebreak-tr-title',
                        value => 'External Links', #better name?
@@ -405,6 +408,8 @@ my $agent_clone_extra_sql =
 my $conf = new FS::Conf;
 my $taxproducts = $conf->exists('enable_taxproducts');
 
+my $fcc_opts = $conf->exists('part_pkg-show_fcc_options');
+
 my @locales = grep { ! /^en_/i } $conf->config('available-locales'); #should filter from the default locale lang instead of en_
 my %locale_labels =  map {
   ( $_ => 'Package -- '. FS::Locales->description($_) )
diff --git a/httemplate/edit/process/deploy_zone-fixed.html b/httemplate/edit/process/deploy_zone-fixed.html
new file mode 100644 (file)
index 0000000..c14c81c
--- /dev/null
@@ -0,0 +1,9 @@
+<& elements/process.html, 
+    error_redirect => popurl(2).'deploy_zone-fixed.html?',
+    table       => 'deploy_zone',
+    viewall_dir => 'browse',
+    process_o2m => 
+      { 'table'  => 'deploy_zone_block',
+                     'fields' => [qw( censusblock censusyear )]
+      },
+&>
diff --git a/httemplate/elements/deploy_zone_block.html b/httemplate/elements/deploy_zone_block.html
new file mode 100644 (file)
index 0000000..9985944
--- /dev/null
@@ -0,0 +1,47 @@
+% unless ( $opt{'js_only'} ) {
+
+  <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
+  Block 
+  <INPUT TYPE  = "text"
+         NAME  = "<%$name%>_censusblock"
+         ID    = "<%$id%>_censusblock"
+         VALUE = "<% scalar($cgi->param($name.'_censusblock'))
+                      || $deploy_zone_block->censusblock
+                 %>"
+         SIZE  = 17
+         MAXLENGTH = 15
+         <% $onchange %>
+  >
+  &nbsp;
+  Year
+  <INPUT TYPE  = "text"
+         NAME  = "<%$name%>_censusyear" 
+         ID    = "<%$id%>_censusyear"
+         VALUE = "<% scalar($cgi->param($name.'_censusyear'))
+                      || $deploy_zone_block->censusyear
+                  %>"
+         SIZE  = 5
+         MAXLENGTH = 4
+         <% $onchange %>
+  >
+% }
+<%init>
+
+my( %opt ) = @_;
+
+my $name = $opt{'element_name'} || $opt{'field'} || 'blocknum';
+my $id = $opt{'id'} || 'blocknum';
+
+my $curr_value = $opt{'curr_value'} || $opt{'value'};
+
+my $onchange = $opt{'onchange'};
+if ( $onchange ) {
+  $onchange =~ s/\(what\);/(this);/;
+  $onchange = 'onchange="'.$onchange.'"';
+}
+
+my $deploy_zone_block = $curr_value
+  ? FS::deploy_zone_block->by_key($curr_value)
+  : FS::deploy_zone_block->new;
+
+</%init>
diff --git a/httemplate/elements/input-fcc_options.html b/httemplate/elements/input-fcc_options.html
new file mode 100644 (file)
index 0000000..b191e1c
--- /dev/null
@@ -0,0 +1,108 @@
+% unless ($opt{js_only}) {
+<& hidden.html, 'field' => $id, @_ &>
+%#    <& input-text.html, 'id' => $id, @_ &> # XXX debugging
+<UL ID="<%$id%>_display_fcc_options" CLASS="fcc_options">
+</UL>
+<button type="button" class="edit_fcc_button" data-target="<% $id %>">
+  Edit
+</button>
+% }
+% unless ($opt{html_only}) {
+%   my $popup = $fsurl.'misc/part_pkg_fcc_options.html?id=';
+%   my $popup_name = 'popup-'.time. "-$$-". rand() * 2**32;
+<SCRIPT TYPE="text/javascript">
+function edit_fcc_options() {
+  var id = this.dataset['target'];
+  overlib(
+    OLiframeContent( '<% $popup %>' + id,
+      760, 600, '<% $popup_name %>', 0, 'auto' ),
+    CAPTION, 'FCC Form 477 options', 
+    STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0,
+    DRAGGABLE, CLOSECLICK,
+    BGCOLOR, '#333399', CGCOLOR, '#333399',
+    CLOSETEXT, 'Close'
+  );
+}
+
+var technology_labels = <% encode_json(FS::part_pkg_fcc_option->technology_labels) %>;
+function show_fcc_options(id) {
+  var curr_values = JSON.parse(document.getElementById(id).value);
+  // hardcoded for the same reasons as misc/part_pkg_fcc_options
+  var out = '';
+  var tech = curr_values['technology'];
+  if ( tech ) {
+    if (technology_labels[tech]) {
+      tech = technology_labels[tech];
+    } else {
+      tech = 'Technology '+tech; // unknown?
+    }
+  }
+  var media = String.toLowerCase(curr_values['media'] || 'unknown media');
+  if ( curr_values['is_consumer'] ) {
+    out += '<li><strong>Consumer-grade</strong> service</li>>';
+  } else {
+    out += '<li><strong>Business-grade</strong> service</li>';
+  }
+  if ( curr_values['is_broadband'] ) {
+    out += '<li>Broadband via <strong>' + tech + '</strong>'
+        +  '<li><strong>' + curr_values['broadband_downstream']
+        +  'Mbps </strong> down / '
+        +  '<strong>' + curr_values['broadband_upstream']
+        +  'Mbps </strong> up</li>';
+  }
+  if ( curr_values['is_phone'] ) {
+    if ( curr_values['phone_wholesale'] ) {
+      out += '<li>Wholesale telephone</li>';
+      if ( curr_values['phone_vges'] ) {
+        out += '<li><strong>' + curr_values['phone_vges'] + '</strong>'
+            +  ' switched voice-grade lines</li>';
+      }
+      if ( curr_values['phone_circuits'] ) {
+        out += '<li><strong>' + curr_values['phone_circuits'] + '</strong>'
+            +  ' unswitched circuits</li>';
+      }
+    } else {
+      // enduser service
+      out += '<li>Local telephone over <strong>' + media + '</strong></li>'
+          +  '<li><strong>' + curr_values['phone_lines']
+          +  '</strong> voice-grade lines</li>';
+      if ( curr_values['phone_localloop'] == 'resale' ) {
+        out += '<li><strong>Resold</strong> from another carrier</li>>';
+      } else if ( curr_values['phone_localloop'] == 'leased' ) {
+        out += '<li>Using <strong>leased circuits</strong> from another carrier</li>';
+      } else if ( curr_values['phone_localloop'] == 'owned' ) {
+        out += '<li>Using <strong>our own circuits</strong></li>';
+      }
+      if ( curr_values['phone_longdistance'] ) {
+        out += '<li>Includes <strong>long-distance service</strong></li>';
+      }
+    }
+  } // is_phone
+  if ( curr_values['is_voip'] ) {
+    out += '<li><strong>VoIP</strong> telephone service</li>';
+    out += '<li><strong>' + curr_values['voip_sessions'] + 
+           '</strong> sessions allowed</li>';
+    if ( curr_values['voip_lastmile'] ) {
+      out += '<li><strong>Including</strong> last-mile connection</li>';
+    } else {
+      out += '<li>Using a <strong>separate</strong> last-mile connection</li>';
+    }
+  } // is_voip
+
+  var out_ul = document.getElementById(id + '_display_fcc_options');
+  out_ul.innerHTML = out;
+}
+<&| onload.js &>
+  var edit_fcc_buttons = document.getElementsByClassName('edit_fcc_button');
+  for(var i = 0; i < edit_fcc_buttons.length; i++) {
+    var button = edit_fcc_buttons[i];
+    show_fcc_options( button.dataset['target'] );
+    button.addEventListener('click', edit_fcc_options);
+  }
+</&>
+</SCRIPT>
+% }
+<%init>
+my %opt = @_;
+my $id = $opt{id} || $opt{field};
+</%init>
index 11cb4a9..58f7247 100644 (file)
@@ -41,9 +41,9 @@ function show_fcc_options() {
   }
   var media = String.toLowerCase(curr_values['media'] || 'unknown media');
   if ( curr_values['is_consumer'] ) {
-    out += '<li><strong>Consumer-grade</strong> service</li>>';
+    out += '<li><strong>Consumer-grade</strong></li>>';
   } else {
-    out += '<li><strong>Business-grade</strong> service</li>';
+    out += '<li><strong>Business-grade</strong></li>';
   }
   if ( curr_values['is_broadband'] ) {
     out += '<li>Broadband via <strong>' + tech + '</strong>'
index f743284..a5ecb12 100644 (file)
@@ -90,6 +90,8 @@
     <& .checkbox, 'is_voip' &>
     <LABEL FOR="is_voip">This package provides VoIP telephone service</LABEL>
     <FIELDSET ID="voip">
+      <LABEL FOR="voip_sessions">Number of simultaneous calls possible</LABEL>
+      <INPUT NAME="voip_sessions" ID="voip_sessions">
       <& .checkbox, 'voip_lastmile' &>
       <LABEL FOR="voip_lastmile">Do you also provide last-mile connectivity?</LABEL>
     </FIELDSET>
@@ -145,7 +147,7 @@ function save_changes() {
   }
   parent_input.value = JSON.stringify(data);
   // update the display
-  parent.show_fcc_options();
+  parent.show_fcc_options(parent_input.id);
   parent.cClick(); //overlib
 }
 
@@ -179,6 +181,11 @@ function enable_fieldset(fieldset_id) {
       form.elements['phone1'].disabled = (this.value == '');
     }
   );
+  addEventListener(form.elements['is_phone'], 'change', 
+    function() {
+      form.elements['phone_wholesale'].dispatchEvent( new Event('change') );
+    }
+  );
 
   // load data from the parent form and trigger handlers
   for(var i = 0; i < form.elements.length; i++) {
index 6849337..26bd9f3 100644 (file)
@@ -36,20 +36,19 @@ a.download {
   float: right;
 }
 </STYLE>
-% foreach my $partnum (@partnums) {
-%   $cgi->param('parts', $partnum);
+% foreach my $partname (@partnames) {
+%   $cgi->param('parts', $partname);
 %   $cgi->param('type', 'csv');
 <table class="fcc477part">
   <caption>
-    <span class="parttitle">Part <% $partnum %></span>
+    <span class="parttitle"><% $parttitle{$partname} %></span>
     <a class="download" href="<% $cgi->self_url %>">Download</a>
   </caption>
-%   my $header = ".header$partnum";
-%   my $data = $parts{$partnum};
+%   my $header = ".header_$partname";
+%   my $data = $parts{$partname};
   <thead>
     <& $header &>
   </thead>
-%   #XXX column headings
 %   foreach my $row (@$data) {
   <tr>
 %     foreach my $item (@$row) {
@@ -58,7 +57,7 @@ a.download {
   </tr>
 %   }
 </table>
-% } # foreach $partnum
+% } # foreach $partname
 <& /elements/footer.html &>
 <%init>
 die "access denied"
@@ -80,10 +79,10 @@ if ($cgi->param('agentnum') =~ /^(\d+)$/ ) {
   $agentnum = $1;
 }
 my $date = parse_datetime($cgi->param('date')) || time;
-my @partnums = grep /^\d+$/, $cgi->param('parts');
-foreach my $partnum (@partnums) {
-  my $method = "part$partnum";
-  $parts{$partnum} ||= FS::Report::FCC_477->$method(
+my @partnames = grep /^\w+$/, $cgi->param('parts');
+foreach my $partname (@partnames) {
+  my $method = "report_$partname";
+  $parts{$partname} ||= FS::Report::FCC_477->$method(
     date      => $date,
     agentnum  => $agentnum
   );
@@ -93,11 +92,11 @@ $m->cache->set($session, \%parts, '1h');
 my $title = 'FCC Form 477 Data - ' . time2str('%b %o, %Y', $date);
 
 if ( $cgi->param('type') eq 'csv' ) {
-  my $partnum = $partnums[0]; # ignore any beyond the first
-  my $data = $parts{$partnum};
+  my $partname = $partnames[0]; # ignore any beyond the first
+  my $data = $parts{$partname};
   my $csv = Text::CSV_XS->new({ eol => "\r\n" }); # i think
 
-  my $filename = time2str('%Y-%m-%d', $date) . '-part' . $partnum . '.csv';
+  my $filename = time2str('%Y-%m-%d', $date) . '-'. $partname . '.csv';
   http_header('Content-Type' => 'text/csv');
   http_header('Content-Disposition' => qq(attachment;filename="$filename"));
 
@@ -111,7 +110,7 @@ if ( $cgi->param('type') eq 'csv' ) {
 }
 
 </%init>
-<%def .header6>
+<%def .header_fixed_broadband>
   <TR CLASS="head">
     <TD ROWSPAN=2>Census Tract</TD>
     <TD ROWSPAN=2>Technology</TD>
@@ -125,30 +124,18 @@ if ( $cgi->param('type') eq 'csv' ) {
     <TD>Consumer</TD>
   </TR>
 </%def>
-<%def .header7>
+<%def .header_fixed_voice>
   <TR CLASS="head">
-    <TD ROWSPAN=2>State</TD>
-    <TD COLSPAN=2>Speed (Mbps)</TD>
-    <TD COLSPAN=2>Subscriptions</TD>
+    <TD ROWSPAN=2>Census Tract</TD>
+    <TD ROWSPAN=2>VoIP?</TD>
+    <TD COLSPAN=2>Lines/Subscriptions</TD>
   </TR>
   <TR CLASS="subhead">
-    <TD>Down</TD>
-    <TD>Up</TD>
     <TD>Total</TD>
     <TD>Consumer</TD>
   </TR>
 </%def>
-<%def .header8>
-  <TR CLASS="head">
-    <TD ROWSPAN=2>State</TD>
-    <TD COLSPAN=2>Subscriptions</TD>
-  </TR>
-  <TR CLASS="subhead">
-    <TD>Total</TD>
-    <TD>Direct</TD>
-  </TR>
-</%def>
-<%def .header9>
+<%def .header_local_phone>
   <TR CLASS="head">
     <TD ROWSPAN=3>State</TD>
     <TD COLSPAN=2>Wholesale</TD>
@@ -183,7 +170,7 @@ if ( $cgi->param('type') eq 'csv' ) {
     <TD>Wireless</TD>
   </TR>
 </%def>
-<%def .header10>
+<%def .header_voip>
   <TR CLASS="head">
     <TD ROWSPAN=2>State</TD>
     <TD COLSPAN=2>VoIP OTT</TD>
@@ -206,14 +193,29 @@ if ( $cgi->param('type') eq 'csv' ) {
     <TD>Other</TD>
   </TR>
 </%def>
-<%def .header11>
+<%def .header_mobile_broadband>
+%# unimplemented
   <TR CLASS="head">
-    <TD ROWSPAN=2>Census Tract</TD>
-    <TD ROWSPAN=2>VoIP?</TD>
-    <TD COLSPAN=2>Lines/Subscriptions</TD>
+    <TD ROWSPAN=2>State</TD>
+    <TD COLSPAN=2>Speed (Mbps)</TD>
+    <TD COLSPAN=2>Subscriptions</TD>
   </TR>
   <TR CLASS="subhead">
+    <TD>Down</TD>
+    <TD>Up</TD>
     <TD>Total</TD>
     <TD>Consumer</TD>
   </TR>
 </%def>
+<%def .header_mobile_voice>
+%# unimplemented
+  <TR CLASS="head">
+    <TD ROWSPAN=2>State</TD>
+    <TD COLSPAN=2>Subscriptions</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Total</TD>
+    <TD>Direct</TD>
+  </TR>
+</%def>
+
index e3ae69e..2a6878e 100755 (executable)
       'label'   => 'Enable parts',
       'field'   => 'parts',
       'labels'  => {
-        6   => 'Part 6 (Fixed Broadband Subscription)',
+        fixed_broadband => 'Fixed Broadband Subscription',
         #7   => 'Part 7 (Mobile Wireless Broadband Subscription),
         #8   => 'Part 8 (Mobile Local Telephone Subscription),
-        9   => 'Part 9 (Local Exchange Telephone Subscription)',
-        10  => 'Part 10 (Interconnected VoIP Subscription)',
-        11  => 'Part 11 (Voice Telephone Subscription Detail)',
+        fixed_voice     => 'Voice Telephone Subscription',
+        local_phone     => 'Local Exchange Telephone Subscription',
+        voip            => 'Interconnected VoIP Subscription',
       },
       options => [ 6, 9, 10, 11 ],
     &>