From 022bfd91eca7ae26f8f6ee125179f5c0ff4cbb72 Mon Sep 17 00:00:00 2001
From: Mark Wells
Date: Thu, 7 Aug 2014 13:50:51 -0700
Subject: [PATCH 1/1] merge new form 477 stuff, #24047
---
FS/FS/AccessRight.pm | 3 +
FS/FS/Conf.pm | 21 +-
FS/FS/Mason.pm | 5 +
FS/FS/Record.pm | 31 ++
FS/FS/Report/FCC_477.pm | 421 ++++++++++++++++++++-
FS/FS/Schema.pm | 97 +++++
FS/FS/Upgrade.pm | 19 +
FS/FS/deploy_zone.pm | 277 ++++++++++++++
FS/FS/deploy_zone_block.pm | 130 +++++++
FS/FS/deploy_zone_vertex.pm | 120 ++++++
FS/FS/part_pkg.pm | 78 ++++
FS/FS/part_pkg_fcc_option.pm | 180 +++++++++
FS/FS/state.pm | 133 +++++++
FS/MANIFEST | 11 +
FS/t/deploy_zone.t | 5 +
FS/t/deploy_zone_block.t | 5 +
FS/t/deploy_zone_vertex.t | 5 +
FS/t/part_pkg_fcc_option.t | 5 +
FS/t/state.t | 5 +
bin/convert-477-options | 140 +++++++
httemplate/browse/deploy_zone.html | 126 ++++++
httemplate/browse/part_pkg-fcc.html | 225 +++++++++++
httemplate/edit/deploy_zone-fixed.html | 95 +++++
httemplate/edit/deploy_zone-mobile.html | 90 +++++
httemplate/edit/elements/edit.html | 2 +
httemplate/edit/part_pkg.cgi | 36 +-
httemplate/edit/process/bulk-part_pkg-fcc.html | 43 +++
httemplate/edit/process/deploy_zone-fixed.html | 9 +
httemplate/edit/process/deploy_zone-mobile.html | 9 +
httemplate/edit/process/part_pkg.cgi | 8 +
httemplate/elements/deploy_zone_block.html | 47 +++
httemplate/elements/deploy_zone_vertex.html | 45 +++
httemplate/elements/input-fcc_options.html | 114 ++++++
httemplate/elements/menu.html | 4 +-
httemplate/elements/tr-input-fcc_options.html | 102 +++++
httemplate/misc/part_pkg_fcc_options.html | 221 +++++++++++
httemplate/search/477.html | 338 +++++++++++------
httemplate/search/old477/477.html | 135 +++++++
httemplate/search/{ => old477}/477partIA.html | 8 +-
httemplate/search/{ => old477}/477partIIA.html | 0
httemplate/search/{ => old477}/477partIIB.html | 0
httemplate/search/{ => old477}/477partIV.html | 0
httemplate/search/{ => old477}/477partV.html | 2 +-
.../search/{ => old477}/477partVI_census.html | 2 +-
httemplate/search/old477/report_477.html | 282 ++++++++++++++
httemplate/search/report_477.html | 285 ++------------
46 files changed, 3515 insertions(+), 404 deletions(-)
create mode 100644 FS/FS/deploy_zone.pm
create mode 100644 FS/FS/deploy_zone_block.pm
create mode 100644 FS/FS/deploy_zone_vertex.pm
create mode 100644 FS/FS/part_pkg_fcc_option.pm
create mode 100644 FS/FS/state.pm
create mode 100644 FS/t/deploy_zone.t
create mode 100644 FS/t/deploy_zone_block.t
create mode 100644 FS/t/deploy_zone_vertex.t
create mode 100644 FS/t/part_pkg_fcc_option.t
create mode 100644 FS/t/state.t
create mode 100755 bin/convert-477-options
create mode 100644 httemplate/browse/deploy_zone.html
create mode 100755 httemplate/browse/part_pkg-fcc.html
create mode 100644 httemplate/edit/deploy_zone-fixed.html
create mode 100644 httemplate/edit/deploy_zone-mobile.html
create mode 100644 httemplate/edit/process/bulk-part_pkg-fcc.html
create mode 100644 httemplate/edit/process/deploy_zone-fixed.html
create mode 100644 httemplate/edit/process/deploy_zone-mobile.html
create mode 100644 httemplate/elements/deploy_zone_block.html
create mode 100644 httemplate/elements/deploy_zone_vertex.html
create mode 100644 httemplate/elements/input-fcc_options.html
create mode 100644 httemplate/elements/tr-input-fcc_options.html
create mode 100644 httemplate/misc/part_pkg_fcc_options.html
mode change 100755 => 100644 httemplate/search/477.html
create mode 100644 httemplate/search/old477/477.html
rename httemplate/search/{ => old477}/477partIA.html (97%)
mode change 100755 => 100644
rename httemplate/search/{ => old477}/477partIIA.html (100%)
mode change 100755 => 100644
rename httemplate/search/{ => old477}/477partIIB.html (100%)
mode change 100755 => 100644
rename httemplate/search/{ => old477}/477partIV.html (100%)
mode change 100755 => 100644
rename httemplate/search/{ => old477}/477partV.html (98%)
mode change 100755 => 100644
rename httemplate/search/{ => old477}/477partVI_census.html (99%)
mode change 100755 => 100644
create mode 100644 httemplate/search/old477/report_477.html
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 7a0d0e994..59edc905f 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -357,6 +357,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, },
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 3ca032551..5ed78c924 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -3465,13 +3465,6 @@ and customer address. Include units.',
},
{
- 'key' => 'cust_pkg-show_fcc_voice_grade_equivalent',
- 'section' => 'UI',
- 'description' => "Show fields on package definitions for FCC Form 477 classification",
- 'type' => 'checkbox',
- },
-
- {
'key' => 'cust_pkg-large_pkg_size',
'section' => 'UI',
'description' => "In customer view, summarize packages with more than this many services. Set to zero to never summarize packages.",
@@ -3486,6 +3479,13 @@ and customer address. Include units.',
},
{
+ 'key' => 'part_pkg-show_fcc_options',
+ 'section' => 'UI',
+ 'description' => "Show fields on package definitions for FCC Form 477 classification",
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'svc_acct-edit_uid',
'section' => 'shell',
'description' => 'Allow UID editing.',
@@ -5759,6 +5759,13 @@ and customer address. Include units.',
],
},
+ {
+ 'key' => 'old_fcc_report',
+ 'section' => '',
+ 'description' => 'Use the old (pre-2014) FCC Form 477 report format.',
+ 'type' => 'checkbox',
+ },
+
{ key => "apacheroot", section => "deprecated", description => "DEPRECATED", type => "text" },
{ key => "apachemachine", section => "deprecated", description => "DEPRECATED", type => "text" },
{ key => "apachemachines", section => "deprecated", description => "DEPRECATED", type => "text" },
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 2db693627..660ae2597 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -363,7 +363,12 @@ if ( -e $addl_handler_use_file ) {
use FS::sched_avail;
use FS::export_batch;
use FS::export_batch_item;
+ use FS::part_pkg_fcc_option;
+ 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 ) {
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 88e54115d..734d61aaf 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -125,6 +125,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');
@@ -2312,6 +2314,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
diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm
index fd088148b..0f3dfb143 100644
--- a/FS/FS/Report/FCC_477.pm
+++ b/FS/FS/Report/FCC_477.pm
@@ -4,9 +4,15 @@ use base qw( FS::Report );
use strict;
use vars qw( @upload @download @technology @part2aoption @part2boption
%states
+ $DEBUG
);
use FS::Record qw( dbh );
+use Tie::IxHash;
+use Storable;
+
+$DEBUG = 0;
+
=head1 NAME
FS::Report::FCC_477 - Routines for FCC Form 477 reports
@@ -78,6 +84,7 @@ Documentation.
);
#from the select at http://www.ffiec.gov/census/default.aspx
+#though this is now in the database, also
%states = (
'01' => 'ALABAMA (AL)',
'02' => 'ALASKA (AK)',
@@ -161,8 +168,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 {
@@ -200,11 +205,413 @@ sub statenum2state {
my $num = shift;
$states{$num};
}
+### everything above this point is unmaintained ###
+
+
+=head1 THE "NEW" REPORT (October 2014 and later)
+
+=head2 METHODS
+
+=over 4
+
+=cut
+
+# functions for internal use
+
+sub join_optionnames {
+ join(' ', map { join_optionname($_) } @_);
+}
+
+sub join_optionnames_int {
+ join(' ', map { join_optionname_int($_) } @_);
+}
+
+sub join_optionname {
+ # Returns a FROM phrase to join a specific option into the query (via
+ # part_pkg). The option value will appear as a field with the same name
+ # as the option.
+ my $name = shift;
+ "LEFT JOIN (SELECT pkgpart, optionvalue AS $name FROM part_pkg_fcc_option".
+ " WHERE fccoptionname = '$name') AS t_$name".
+ " ON (part_pkg.pkgpart = t_$name.pkgpart)";
+}
+
+sub join_optionname_int {
+ # Returns a FROM phrase to join a specific option into the query (via
+ # part_pkg) and cast it to integer.. Note this does not convert nulls
+ # to zero.
+ my $name = shift;
+ "LEFT JOIN (SELECT pkgpart, CAST(optionvalue AS int) AS $name
+ FROM part_pkg_fcc_option".
+ " WHERE fccoptionname = '$name') AS t_$name".
+ " ON (part_pkg.pkgpart = t_$name.pkgpart)";
+}
+
+sub active_on {
+ # Returns a condition to limit packages to those that were setup before a
+ # certain date, and not canceled before that date.
+ #
+ # (Strictly speaking this should also exclude suspended packages but
+ # "suspended as of some past date" is a complicated query.)
+ my $date = shift;
+ "cust_pkg.setup <= $date AND ".
+ "(cust_pkg.cancel IS NULL OR cust_pkg.cancel > $date)";
+}
+
+sub is_fixed_broadband {
+ "is_broadband::int = 1 AND technology::int IN( 10, 11, 12, 20, 30, 40, 41, 42, 50, 60, 70, 90, 0 )"
+}
+
+sub is_mobile_broadband {
+ "is_broadband::int = 1 AND technology::int IN( 80, 81, 82, 83, 84, 85, 86, 87, 88)"
+}
+
+=item report SECTION, OPTIONS
+
+Returns the report section SECTION (see the C method for section
+name strings) as an arrayref of arrayrefs. OPTIONS may contain "date"
+(a timestamp value to run the report as of this date) and "agentnum"
+(to limit to a single agent).
+
+=cut
-#sub statenum2abbr {
-# my $num = shift;
-# $states{$num} =~ /\((\w\w)\)$/ or return '';
-# $1;
-#}
+sub report {
+ my $class = shift;
+ my $section = shift;
+ my %opt = @_;
+
+ my $method = $section.'_sql';
+ die "Report section '$section' is not implemented\n"
+ unless $class->can($method);
+ my $statement = $class->$method(%opt);
+
+ my $sth = dbh->prepare($statement);
+ $sth->execute or die $sth->errstr;
+ $sth->fetchall_arrayref;
+}
+
+sub fbd_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ warn $date;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ 'censusblock',
+ 'COALESCE(dbaname, agent.agent)',
+ 'technology',
+ 'CASE WHEN is_consumer IS NOT NULL THEN 1 ELSE 0 END',
+ 'adv_speed_down',
+ 'adv_speed_up',
+ 'CASE WHEN is_business IS NOT NULL THEN 1 ELSE 0 END',
+ 'cir_speed_down',
+ 'cir_speed_up',
+ );
+ my $from =
+ 'deploy_zone_block
+ JOIN deploy_zone USING (zonenum)
+ JOIN agent USING (agentnum)';
+ my @where = (
+ "zonetype = 'B'",
+ "active_date < $date",
+ "(expire_date > $date OR expire_date IS NULL)",
+ );
+ push @where, "agentnum = $agentnum" if $agentnum;
+
+ my $order_by = 'censusblock, dbaname, technology, is_consumer, is_business';
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ ORDER BY $order_by
+ ";
+}
+
+sub fbs_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ 'cust_location.censustract',
+ 'technology',
+ 'broadband_downstream',
+ 'broadband_upstream',
+ 'COUNT(*)',
+ 'COUNT(is_consumer)',
+ );
+ 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_broadband technology
+ is_consumer
+ )).
+ join_optionnames(qw(broadband_downstream broadband_upstream))
+ ;
+ my @where = (
+ active_on($date),
+ is_fixed_broadband()
+ );
+ push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+ my $group_by = 'cust_location.censustract, technology, '.
+ 'broadband_downstream, broadband_upstream ';
+ my $order_by = $group_by;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+
+}
+
+sub fvs_sql {
+ my $class = shift;
+ my %opt = @_;
+ 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;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+
+}
+
+sub lts_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ "state.fips",
+ "SUM(phone_vges)",
+ "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 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)",
+ "SUM(CASE WHEN media = 'Fiber' THEN phone_lines ELSE 0 END)",
+ "SUM(CASE WHEN media = 'Cable Modem' THEN phone_lines ELSE 0 END)",
+ "SUM(CASE WHEN media = 'Fixed Wireless' THEN phone_lines ELSE 0 END)",
+ );
+ my $from =
+ 'cust_pkg
+ JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
+ JOIN state USING (country, state)
+ JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
+ JOIN part_pkg USING (pkgpart) '.
+ join_optionnames_int(qw(
+ is_phone is_broadband
+ phone_vges phone_circuits phone_lines
+ is_consumer phone_longdistance
+ )).
+ join_optionnames('media', 'phone_localloop')
+ ;
+ my @where = (
+ active_on($date),
+ "is_phone = 1",
+ );
+ push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+ my $group_by = 'state.fips';
+ my $order_by = $group_by;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+}
+
+sub voip_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ "state.fips",
+ # OTT, OTT + consumer
+ "SUM(CASE WHEN (voip_lastmile IS NULL) THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile IS NULL AND is_consumer = 1) THEN 1 ELSE 0 END)",
+ # non-OTT: total, consumer, broadband bundle, media types
+ "SUM(CASE WHEN (voip_lastmile = 1) THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND is_consumer = 1) THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND is_broadband = 1) THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND media = 'Copper') THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND media = 'Cable Modem') THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND media = 'Fiber') THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND media = 'Fixed Wireless') THEN 1 ELSE 0 END)",
+ "SUM(CASE WHEN (voip_lastmile = 1 AND media NOT IN('Copper', 'Fiber', 'Cable Modem', 'Fixed Wireless') ) THEN 1 ELSE 0 END)",
+ );
+
+ my $from =
+ 'cust_pkg
+ JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
+ JOIN state USING (country, state)
+ JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
+ JOIN part_pkg USING (pkgpart) '.
+ join_optionnames_int(
+ qw( is_voip is_broadband is_consumer voip_lastmile)
+ ).
+ join_optionnames('media')
+ ;
+ my @where = (
+ active_on($date),
+ "is_voip = 1",
+ );
+ push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+ my $group_by = 'state.fips';
+ my $order_by = $group_by;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+}
+
+sub mbs_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ 'state.fips',
+ 'broadband_downstream',
+ 'broadband_upstream',
+ 'COUNT(*)',
+ 'COUNT(is_consumer)',
+ );
+ my $from =
+ 'cust_pkg
+ JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
+ JOIN state USING (country, state)
+ JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
+ JOIN part_pkg USING (pkgpart) '.
+ join_optionnames_int(qw(
+ is_broadband technology
+ is_consumer
+ )).
+ join_optionnames(qw(broadband_downstream broadband_upstream))
+ ;
+ my @where = (
+ active_on($date),
+ is_mobile_broadband()
+ );
+ push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+ my $group_by = 'state.fips, broadband_downstream, broadband_upstream ';
+ my $order_by = $group_by;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+}
+
+sub mvs_sql {
+ my $class = shift;
+ my %opt = @_;
+ my $date = $opt{date} || time;
+ my $agentnum = $opt{agentnum};
+
+ my @select = (
+ 'state.fips',
+ 'COUNT(*)',
+ 'COUNT(mobile_direct)',
+ );
+ my $from =
+ 'cust_pkg
+ JOIN cust_location ON (cust_pkg.locationnum = cust_location.locationnum)
+ JOIN state USING (country, state)
+ JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)
+ JOIN part_pkg USING (pkgpart) '.
+ join_optionnames_int(qw( is_mobile mobile_direct) )
+ ;
+ my @where = (
+ active_on($date),
+ 'is_mobile = 1'
+ );
+ push @where, "cust_main.agentnum = $agentnum" if $agentnum;
+ my $group_by = 'state.fips';
+ my $order_by = $group_by;
+
+ "SELECT ".join(', ', @select) . "
+ FROM $from
+ WHERE ".join(' AND ', @where)."
+ GROUP BY $group_by
+ ORDER BY $order_by
+ ";
+}
+
+=item parts
+
+Returns a Tie::IxHash reference of the internal short names used for the
+report sections ('fbd', 'mbs', etc.) to the full names.
+
+=cut
+
+tie our %parts, 'Tie::IxHash', (
+ fbd => 'Fixed Broadband Deployment',
+ fbs => 'Fixed Broadband Subscription',
+ fvs => 'Fixed Voice Subscription',
+ lts => 'Local Exchange Telephone Subscription',
+ voip => 'Interconnected VoIP Subscription',
+ mbd => 'Mobile Broadband Deployment',
+ mbsa => 'Mobile Broadband Service Availability',
+ mbs => 'Mobile Broadband Subscription',
+ mvd => 'Mobile Voice Deployment',
+ mvs => 'Mobile Voice Subscription',
+);
+
+sub parts {
+ Storable::dclone(\%parts);
+}
1;
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 473532180..64f8ac9cf 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3331,6 +3331,18 @@ sub tables_hashref {
'index' => [],
},
+ 'part_pkg_fcc_option' => {
+ 'columns' => [
+ 'num', 'serial', '', '', '', '',
+ 'fccoptionname', 'varchar', '', $char_d, '', '',
+ 'pkgpart', 'int', '', '', '', '',
+ 'optionvalue', 'varchar', 'NULL', $char_d, '', '',
+ ],
+ 'primary_key' => 'num',
+ 'unique' => [ [ 'fccoptionname', 'pkgpart' ] ],
+ 'index' => [],
+ },
+
'rate' => {
'columns' => [
'ratenum', 'serial', '', '', '', '',
@@ -4600,6 +4612,91 @@ sub tables_hashref {
],
},
+ # lookup table for states, similar to msa and lata
+ 'state' => {
+ 'columns' => [
+ 'statenum', 'int', '', '', '', '',
+ 'country', 'char', '', 2, '', '',
+ 'state', 'char', '', $char_d, '', '',
+ 'fips', 'char', '', 3, '', '',
+ ],
+ 'primary_key' => 'statenum',
+ 'unique' => [ [ 'country', 'state' ], ],
+ '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', '', '', '',
+ 'adv_speed_up', 'decimal', '', '10,3', '0', '',
+ 'adv_speed_down', 'decimal', '', '10,3', '0', '',
+ 'cir_speed_up', 'decimal', '', '10,3', '0', '',
+ 'cir_speed_down', 'decimal', '', '10,3', '0', '',
+ 'is_broadband', 'char', 'NULL', 1, '', '',
+ 'is_voice', 'char', 'NULL', 1, '', '',
+ 'is_consumer', 'char', 'NULL', 1, '', '',
+ 'is_business', 'char', 'NULL', 1, '', '',
+ 'active_date', @date_type, '', '',
+ 'expire_date', @date_type, '', '',
+ ],
+ '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', '', '',
+ ],
+ 'primary_key' => 'vertexnum',
+ 'unique' => [ ],
+ 'index' => [ ],
+ 'foreign_keys' => [
+ { columns => [ 'zonenum' ],
+ table => 'deploy_zone',
+ references => [ 'zonenum' ],
+ },
+ ],
+ },
+
+
+
+
+
# name type nullability length default local
#'new_table' => {
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index d212d451d..db24215a7 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -97,6 +97,22 @@ sub upgrade_config {
$conf->touch('cust_main-enable_spouse');
$conf->delete('cust_main-enable_spouse_birthdate');
}
+
+ # renamed/repurposed
+ if ( $conf->exists('cust_pkg-show_fcc_voice_grade_equivalent') ) {
+ $conf->touch('part_pkg-show_fcc_options');
+ $conf->delete('cust_pkg-show_fcc_voice_grade_equivalent');
+ warn "
+You have FCC Form 477 package options enabled.
+
+Starting with the October 2014 filing date, the FCC has redesigned
+Form 477 and introduced new service categories. See bin/convert-477-options
+to update your package configuration for the new report.
+
+If you need to continue using the old Form 477 report, turn on the
+'old_fcc_report' configuration option.
+";
+ }
}
sub upgrade_overlimit_groups {
@@ -340,6 +356,9 @@ sub upgrade_data {
#fix taxable line item links
'cust_bill_pkg_tax_location' => [],
+
+ #populate state FIPS codes if not already done
+ 'state' => [],
;
\%hash;
diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm
new file mode 100644
index 000000000..16f59c81d
--- /dev/null
+++ b/FS/FS/deploy_zone.pm
@@ -0,0 +1,277 @@
+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 and L.
+
+=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 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.
+
+=item is_broadband
+
+'Y' if this service includes broadband Internet.
+
+=item is_voice
+
+'Y' if this service includes voice communication.
+
+=item active_date
+
+The date this zone became active.
+
+=item expire_date
+
+The date this zone became inactive, if any.
+
+=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_textn('dbaname')
+ || $self->ut_enum('zonetype', [ 'B', 'P' ])
+ || $self->ut_number('technology')
+ || $self->ut_numbern('spectrum')
+ || $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')
+ || $self->ut_flag('is_broadband')
+ || $self->ut_flag('is_voice')
+ || $self->ut_numbern('active_date')
+ || $self->ut_numbern('expire_date')
+ ;
+ return $error if $error;
+
+ foreach(qw(adv_speed_down adv_speed_up cir_speed_down cir_speed_up)) {
+ if ($self->get('is_broadband')) {
+ if (!$self->get($_)) {
+ $self->set($_, 0);
+ }
+ } else {
+ $self->set($_, '');
+ }
+ }
+ if (!$self->get('active_date')) {
+ $self->set('active_date', time);
+ }
+
+ $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';
+ }
+}
+
+=item deploy_zone_block
+
+Returns the census block records in this zone, in order by census block
+number. Only appropriate to block-type zones.
+
+=item deploy_zone_vertex
+
+Returns the vertex records for this zone, in order by sequence number. Only
+appropriate to polygon-type zones.
+
+=cut
+
+sub deploy_zone_block {
+ my $self = shift;
+ qsearch({
+ table => 'deploy_zone_block',
+ hashref => { zonenum => $self->zonenum },
+ order_by => ' ORDER BY censusblock',
+ });
+}
+
+sub deploy_zone_vertex {
+ my $self = shift;
+ qsearch({
+ table => 'deploy_zone_vertex',
+ hashref => { zonenum => $self->zonenum },
+ order_by => ' ORDER BY vertexnum',
+ });
+}
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L
+
+=cut
+
+1;
+
diff --git a/FS/FS/deploy_zone_block.pm b/FS/FS/deploy_zone_block.pm
new file mode 100644
index 000000000..757af7e3d
--- /dev/null
+++ b/FS/FS/deploy_zone_block.pm
@@ -0,0 +1,130 @@
+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 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 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;
+
+ if ($self->get('censusblock') !~ /^(\d{15})$/) {
+ return "Illegal census block number (must be 15 digits)";
+ }
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L
+
+=cut
+
+1;
+
diff --git a/FS/FS/deploy_zone_vertex.pm b/FS/FS/deploy_zone_vertex.pm
new file mode 100644
index 000000000..078b32640
--- /dev/null
+++ b/FS/FS/deploy_zone_vertex.pm
@@ -0,0 +1,120 @@
+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_vertex inherits from
+FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item vertexnum
+
+primary key
+
+=item zonenum
+
+Foreign key to L.
+
+=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.
+
+=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')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 036daf705..f90e3eec5 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -17,6 +17,7 @@ use FS::cust_pkg;
use FS::agent_type;
use FS::type_pkgs;
use FS::part_pkg_option;
+use FS::part_pkg_fcc_option;
use FS::pkg_class;
use FS::agent;
use FS::part_pkg_msgcat;
@@ -303,6 +304,11 @@ sub insert {
}
}
+ if ( $options{fcc_options} ) {
+ warn " updating fcc options " if $DEBUG;
+ $self->set_fcc_options( $options{fcc_options} );
+ }
+
warn " committing transaction" if $DEBUG and $oldAutoCommit;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
@@ -545,6 +551,11 @@ sub replace {
}
}
+ if ( $options->{fcc_options} ) {
+ warn " updating fcc options " if $DEBUG;
+ $new->set_fcc_options( $options->{fcc_options} );
+ }
+
warn " committing transaction" if $DEBUG and $oldAutoCommit;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
@@ -705,6 +716,44 @@ sub propagate {
join("\n", @error);
}
+=item set_fcc_options HASHREF
+
+Sets the FCC options on this package definition to the values specified
+in HASHREF.
+
+=cut
+
+sub set_fcc_options {
+ my $self = shift;
+ my $pkgpart = $self->pkgpart;
+ my $options;
+ if (ref $_[0]) {
+ $options = shift;
+ } else {
+ $options = { @_ };
+ }
+
+ my %existing_num = map { $_->fccoptionname => $_->num }
+ qsearch('part_pkg_fcc_option', { pkgpart => $pkgpart });
+
+ local $FS::Record::nowarn_identical = 1;
+ # set up params for process_o2m
+ my $i = 0;
+ my $params = {};
+ foreach my $name (keys %$options ) {
+ $params->{ "num$i" } = $existing_num{$name} || '';
+ $params->{ "num$i".'_fccoptionname' } = $name;
+ $params->{ "num$i".'_optionvalue' } = $options->{$name};
+ $i++;
+ }
+
+ $self->process_o2m(
+ table => 'part_pkg_fcc_option',
+ fields => [qw( fccoptionname optionvalue )],
+ params => $params,
+ );
+}
+
=item pkg_locale LOCALE
Returns a customer-viewable string representing this package for the given
@@ -1216,6 +1265,35 @@ sub option {
'';
}
+=item fcc_option OPTIONNAME
+
+Returns the FCC 477 report option value for the given name, or the empty
+string.
+
+=cut
+
+sub fcc_option {
+ my ($self, $name) = @_;
+ my $part_pkg_fcc_option =
+ qsearchs('part_pkg_fcc_option', {
+ pkgpart => $self->pkgpart,
+ fccoptionname => $name,
+ });
+ $part_pkg_fcc_option ? $part_pkg_fcc_option->optionvalue : '';
+}
+
+=item fcc_options
+
+Returns all FCC 477 report options for this package, as a hash-like list.
+
+=cut
+
+sub fcc_options {
+ my $self = shift;
+ map { $_->fccoptionname => $_->optionvalue }
+ qsearch('part_pkg_fcc_option', { pkgpart => $self->pkgpart });
+}
+
=item bill_part_pkg_link
Returns the associated part_pkg_link records (see L).
diff --git a/FS/FS/part_pkg_fcc_option.pm b/FS/FS/part_pkg_fcc_option.pm
new file mode 100644
index 000000000..5c78e5f9e
--- /dev/null
+++ b/FS/FS/part_pkg_fcc_option.pm
@@ -0,0 +1,180 @@
+package FS::part_pkg_fcc_option;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use Storable qw(dclone);
+use Tie::IxHash;
+
+sub table { 'part_pkg_fcc_option'; }
+
+=head1 NAME
+
+FS::part_pkg_fcc_option - Object methods for part_pkg_fcc_option records
+
+=head1 SYNOPSIS
+
+ use FS::part_pkg_fcc_option;
+
+ $record = new FS::part_pkg_fcc_option \%hash;
+ $record = new FS::part_pkg_fcc_option { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pkg_fcc_option object represents an option that classifies a
+package definition on the FCC Form 477 report. FS::part_pkg_fcc_option
+inherits from FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item num
+
+primary key
+
+=item fccoptionname
+
+A string identifying a report option, as an element of a static data
+structure found within this module. See the C method.
+
+=item pkgpart
+
+L foreign key.
+
+=item optionvalue
+
+The value of the report option, as an integer. Boolean options use 1
+and NULL. Most other options have some kind of lookup table.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item check
+
+Checks all fields to make sure this is a valid FCC option. 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('num')
+ || $self->ut_alpha('fccoptionname')
+ || $self->ut_number('pkgpart')
+ || $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart')
+ || $self->ut_textn('optionvalue')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item media_types
+
+Returns a Tie::IxHash hashref of the media type strings (which are not
+part of the report definition, per se) to arrayrefs of the technology
+codes included in each one.
+
+=item technology_labels
+
+Returns a hashref relating each technology code to a label. Unlike the
+media type strings, the technology codes are part of the formal report
+definition.
+
+=cut
+
+tie our %media_types, 'Tie::IxHash', (
+ 'Copper' => [ 11, 12, 10, 20, 30 ],
+ 'Cable Modem' => [ 41, 42, 40 ],
+ 'Fiber' => [ 50 ],
+ 'Satellite' => [ 60 ],
+ 'Fixed Wireless' => [ 70 ],
+ 'Mobile Wireless' => [ 80, 81, 82, 83, 84, 85, 86, 87, 88 ],
+ 'Other' => [ 90, 0 ],
+);
+
+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'
+);
+
+tie our %spectrum_labels, 'Tie::IxHash', (
+ 90 => '700 MHz Band',
+ 91 => 'Cellular Band',
+ 92 => 'Specialized Mobile Radio (SMR) Band',
+ 93 => 'Advanced Wireless Services (AWS) 1 Band',
+ 94 => 'Broadband Personal Communications Service (PCS) Band',
+ 95 => 'Wireless Communications Service (WCS) Band',
+ 96 => 'Broadband Radio Service/Educational Broadband Service Band',
+ 97 => 'Satellite (e.g. L-band, Big LEO, Little LEO)',
+ 98 => 'Unlicensed (including broadcast television âwhite spacesâ) Bands',
+ 99 => '600 MHz',
+ 100 => 'H Block',
+ 101 => 'Advanced Wireless Services (AWS) 3 Band',
+ 102 => 'Advanced Wireless Services (AWS) 4 Band',
+ 103 => 'Other',
+);
+
+sub media_types {
+ Storable::dclone(\%media_types);
+}
+
+sub technology_labels {
+ Storable::dclone(\%technology_labels);
+}
+
+sub spectrum_labels {
+ Storable::dclone(\%spectrum_labels);
+}
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/state.pm b/FS/FS/state.pm
new file mode 100644
index 000000000..671a93b44
--- /dev/null
+++ b/FS/FS/state.pm
@@ -0,0 +1,133 @@
+package FS::state;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use Locale::SubCountry;
+
+=head1 NAME
+
+FS::state - Object methods for state/province records
+
+=head1 SYNOPSIS
+
+ use FS::state;
+
+ $record = new FS::state \%hash;
+ $record = new FS::state { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::state object represents a state, province, or other top-level
+subdivision of a sovereign nation. FS::state inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item statenum
+
+primary key
+
+=item country
+
+two-letter country code
+
+=item state
+
+state code/abbreviation/name (as used in cust_location.state)
+
+=item fips
+
+FIPS 10-4 code (not including country code)
+
+=back
+
+=head1 METHODS
+
+=cut
+
+sub table { 'state'; }
+
+# no external API; this table maintains itself
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('statenum')
+ || $self->ut_alpha('country')
+ || $self->ut_alpha('state')
+ || $self->ut_alpha('fips')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=cut
+
+sub _upgrade_data {
+ warn "Updating state and country codes...\n";
+ my %existing;
+ foreach my $state (qsearch('state')) {
+ $existing{$state->country} ||= {};
+ $existing{$state->country}{$state->state} = $state;
+ }
+ my $world = Locale::SubCountry::World->new;
+ foreach my $country_code ($world->all_codes) {
+ my $country = Locale::SubCountry->new($country_code);
+ next unless $country->has_sub_countries;
+ $existing{$country} ||= {};
+ foreach my $state_code ($country->all_codes) {
+ my $fips = $country->FIPS10_4_code($state_code);
+ # we really only need U.S. state codes at this point, so if there's
+ # no FIPS code, ignore it.
+ next if !$fips or $fips eq 'unknown' or $fips =~ /\W/;
+ my $this_state = $existing{$country_code}{$state_code};
+ if ($this_state) {
+ if ($this_state->fips ne $fips) { # this should never happen...
+ $this_state->set(fips => $fips);
+ my $error = $this_state->replace;
+ die "error updating $country_code/$state_code:\n$error\n" if $error;
+ }
+ delete $existing{$country_code}{$state_code};
+ } else {
+ $this_state = FS::state->new({
+ country => $country_code,
+ state => $state_code,
+ fips => $fips,
+ });
+ my $error = $this_state->insert;
+ die "error inserting $country_code/$state_code:\n$error\n" if $error;
+ }
+ }
+ # clean up states that no longer exist (does this ever happen?)
+ foreach my $state (values %{ $existing{$country_code} }) {
+ my $error = $state->delete;
+ die "error removing expired state ".$state->country.'/'.$state->state.
+ "\n$error\n" if $error;
+ }
+ } # foreach $country_code
+ '';
+}
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index a8dfbc1e1..0698ba2e1 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -762,5 +762,16 @@ FS/export_batch.pm
t/export_batch.t
FS/export_batch_item.pm
t/export_batch_item.t
+FS/part_pkg_fcc_option.pm
+t/part_pkg_fcc_option.t
+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
index 000000000..d220e81c7
--- /dev/null
+++ b/FS/t/deploy_zone.t
@@ -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
index 000000000..c3241b158
--- /dev/null
+++ b/FS/t/deploy_zone_block.t
@@ -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
index 000000000..78c079ffd
--- /dev/null
+++ b/FS/t/deploy_zone_vertex.t
@@ -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";
diff --git a/FS/t/part_pkg_fcc_option.t b/FS/t/part_pkg_fcc_option.t
new file mode 100644
index 000000000..8f781c866
--- /dev/null
+++ b/FS/t/part_pkg_fcc_option.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg_fcc_option;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/state.t b/FS/t/state.t
new file mode 100644
index 000000000..a21137fd5
--- /dev/null
+++ b/FS/t/state.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::state;
+$loaded=1;
+print "ok 1\n";
diff --git a/bin/convert-477-options b/bin/convert-477-options
new file mode 100755
index 000000000..99a6ea5e0
--- /dev/null
+++ b/bin/convert-477-options
@@ -0,0 +1,140 @@
+#!/usr/bin/perl
+
+my $user = shift;
+use FS::UID 'adminsuidsetup';
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::part_pkg_report_option;
+use Text::CSV;
+
+if (!$user) {
+ print "
+Usage: bin/convert-477-options
+
+This script will convert your per-package FCC 477 report options
+from the classic style (part IA, IB, IIA...) to the 2014 style.
+This is an approximate conversion, and you should review the
+resulting package settings for accuracy. In particular:
+ - Broadband speeds will be set to the lowest speed in their
+ tier.
+ - Broadband technologies for ADSL and cable modem will be set
+ to 'other ADSL' and 'other cable modem'. You should set
+ them to the specific ADSL or cable encapsulation in use.
+ - All packages will be set to 'business grade'. The 'consumer grade'
+ category did not exist in previous versions of the report.
+";
+ exit(1);
+}
+
+adminsuidsetup($user) or die "invalid user '$user'";
+$FS::UID::AutoCommit = 1;
+$FS::Record::nowarn_classload = 1;
+
+print "Configuring packages...\n";
+
+my @min_download_speed = ( 0.2, 0.768, 1.5, 3, 6, 10, 25, 100 );
+my @min_upload_speed = ( 0.1, @min_download_speed );
+my @media_type = ( 'Copper', 'Copper', 'Copper', 'Cable Modem',
+ 'Fiber', 'Satellite', 'Fixed Wireless', 'Mobile Wireless',
+ 'Other', 'Other' );
+my @technology = ( 10, 20, 30, 40,
+ 50, 60, 70, 80,
+ 90, 0 );
+
+my @phone_option = (
+ 'phone_longdistance:1', # LD carrier
+ 'phone_localloop:owned', # owned loops
+ 'phone_localloop:leased', # unswitched UNE loops
+ 'phone_localloop:resale', # UNE-P (is pretty much extinct...)
+ 'phone_localloop:resale', # UNE-P replacement
+ 'media:Fiber',
+ 'media:Cable Modem',
+ 'media:Fixed Wireless',
+);
+
+my @voip_option = (
+ '', #nomadic; no longer reported
+ 'media:Copper',
+ 'media:Fiber',
+ 'media:Cable Modem',
+ 'media:Fixed Wireless',
+ 'media:Other'
+);
+
+my %num_to_formkey; # o2m
+foreach ( qsearch('fcc477map', {}) ) {
+ push @{ $num_to_formkey{$_->formvalue} ||= [] }, $_->formkey;
+}
+
+sub report_option_to_fcc_option {
+ my $report_option_num = shift;
+ my $formkeys = $num_to_formkey{$report_option_num}
+ or return;
+ my @return;
+ foreach my $formkey (@$formkeys) {
+ if ($formkey =~ /^part1_column_option_(\d+)/) {
+ #download speed
+ push @return, (broadband_downstream => $min_download_speed[$1]);
+ } elsif ($formkey =~ /^part1_row_option_(\d+)/) {
+ #upload speed
+ push @return, (broadband_upstream => $min_upload_speed[$1]);
+ } elsif ($formkey =~ /^part1_technology_option_(\d+)/) {
+ #broadband tech
+ push @return,
+ (is_broadband => 1,
+ media => $media_type[$1],
+ technology => $technology[$1]);
+ } elsif ($formkey =~ /^part2a_row_option_(\d+)/) {
+ #local phone options
+ push @return,
+ (media => 'Copper', # sensible default
+ split(':', $phone_option[$1])
+ );
+ } elsif ($formkey =~ /^part2b_row_option_(\d+)/) {
+ #VoIP options (are all media types)
+ push @return, (split(':', $voip_option[$1]));
+ } else {
+ warn "can't parse option with formkey '$formkey'\n";
+ }
+ }
+ @return;
+}
+
+for my $part_pkg (qsearch('part_pkg', { freq => {op => '!=', value => '0'}})) {
+ my $pkgpart = $part_pkg->pkgpart;
+ #print "#$pkgpart\n";
+ my %report_opts = $part_pkg->options;
+ my @fcc_opts;
+ foreach my $optionname (keys(%report_opts)) {
+ $optionname =~ /^report_option_(\d+)$/ or next;
+ my $num = $1;
+ push @fcc_opts, report_option_to_fcc_option($num);
+ }
+ # other special stuff:
+ # FCC voice class (VoIP OTT, VoIP + broadband)
+ if ($part_pkg->fcc_voip_class == 1) {
+ push @fcc_opts, 'is_voip' => 1;
+ } elsif ( $part_pkg->fcc_voip_class == 2) {
+ push @fcc_opts, 'is_voip' => 1, 'is_broadband' => 1;
+ }
+ # DS0 equivalent lines
+ if ( $part_pkg->fcc_ds0s ) {
+ 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_sessions' => $part_pkg->fcc_ds0s;
+ } else {
+ push @fcc_opts, 'phone_lines' => $part_pkg->fcc_ds0s, 'is_phone' => 1;
+ }
+ }
+
+ my %fcc_opts = @fcc_opts;
+ #print map {"\t$_\t".$fcc_opts{$_}."\n"} keys %fcc_opts;
+ my $error = $part_pkg->process_fcc_options(\%fcc_opts);
+ if ( $error ) {
+ die "$error\n";
+ }
+ #print "\n";
+}
+
+print "Finished.\n";
+
diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html
new file mode 100644
index 000000000..ddfbde43d
--- /dev/null
+++ b/httemplate/browse/deploy_zone.html
@@ -0,0 +1,126 @@
+<& /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',
+&>
+Fixed Broadband Zones
+<& 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,
+&>
+Mobile Zones
+<& elements/browse.html,
+ name_singular => 'zone',
+ query => { table => 'deploy_zone',
+ hashref => { zonetype => 'P' },
+ },
+ count_query => "SELECT COUNT(*) FROM deploy_zone WHERE zonetype = 'P'",
+ agent_virt => 1,
+ header => [ '#',
+ 'Description',
+ 'Technology',
+ 'Spectrum',
+ 'Service Type',
+ 'Advertised Mbps',
+ 'Vertices', # number of vertices? not so useful
+ ],
+ fields => [ 'zonenum',
+ 'description',
+ sub { my $self = shift;
+ $tech_label->{$self->technology} },
+ sub { my $self = shift;
+ $spec_label->{$self->spectrum} },
+ sub { my $self = shift;
+ join( ' / ',
+ $self->is_voice ? 'voice' : (),
+ $self->is_broadband ? 'broadband' : (),
+ )
+ },
+ sub { my $self = shift;
+ join( ' / ', grep $_,
+ $self->adv_speed_down,
+ $self->adv_speed_up
+ )
+ },
+ sub { my $self = shift;
+ FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+ },
+ ],
+ sort_fields => [ 'zonenum',
+ 'description',
+ 'technology',
+ 'spectrum',
+ '(is_voice is not null, is_broadband is not null)',
+ '(adv_speed_down, adv_speed_up)',
+ ],
+ links => [ '', $link_mobile, ],
+ align => 'clllllr',
+ nohtmlheader => 1,
+ disable_maxselect => 1,
+ disable_total => 1,
+&>
+
+<& /elements/footer.html &>
+<%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;
+my $spec_label = FS::part_pkg_fcc_option->spectrum_labels;
+%init>
diff --git a/httemplate/browse/part_pkg-fcc.html b/httemplate/browse/part_pkg-fcc.html
new file mode 100755
index 000000000..4c9cea195
--- /dev/null
+++ b/httemplate/browse/part_pkg-fcc.html
@@ -0,0 +1,225 @@
+<& elements/browse.html,
+ 'title' => 'Package Definitions - FCC Options',
+ 'menubar' => \@menubar,
+ 'html_init' => $html_init,
+ 'html_form' => $html_form,
+ 'name' => 'package definitions',
+ 'disableable' => 1,
+ 'disabled_statuspos' => 4,
+ 'agent_virt' => 1,
+ 'agent_null_right' => $edit_global,
+ 'agent_pos' => 3,
+ '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 FCC report configuration';
+my $edit_global = 'Edit FCC report configuration for all agents';
+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;
+
+if ( $cgi->param('redirect') ) {
+ my $session = $cgi->param('redirect');
+ my $pref = $curuser->option("redirect$session");
+ die "unknown redirect session $session\n" unless length($pref);
+ $cgi = new CGI($pref);
+ $cgi->param('redirect', $session);
+}
+
+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);
+
+$cgi->param('classnum', $classnum) if length($classnum);
+
+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";
+
+# in case of error redirect
+if ( $cgi->param('redirect') ) {
+ push @header, '';
+ push @fields, sub {
+ my $part_pkg = shift;
+ my $pkgpart = $part_pkg->pkgpart;
+ '' . $cgi->param("error$pkgpart") || '' . ''
+ };
+ $align .= 'l';
+}
+
+my $html_init =
+ include('/elements/init_overlib.html') .
+ include('/elements/input-fcc_options.html', js_only => 1) .
+ include('.style');
+
+my $html_form = qq!!;
+
+my @menubar =
+ ( 'Package definitions' => $p.'browse/part_pkg.cgi' );
+
+%init>
+<%def .style>
+
+%def>
diff --git a/httemplate/edit/deploy_zone-fixed.html b/httemplate/edit/deploy_zone-fixed.html
new file mode 100644
index 000000000..1a79500ff
--- /dev/null
+++ b/httemplate/edit/deploy_zone-fixed.html
@@ -0,0 +1,95 @@
+<& 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' => '',
+ 'active_date' => 'Active since',
+ },
+ 'fields' => [
+ { field => 'zonetype',
+ type => 'hidden',
+ value => 'B'
+ },
+ { field => 'is_broadband',
+ type => 'hidden',
+ value => 'Y',
+ },
+ 'description',
+ { field => 'active_date',
+ type => 'fixed-date',
+ value => time,
+ },
+ { field => 'agentnum',
+ type => 'select-agent',
+ disable_empty => 1,
+ viewall_right => 'Edit FCC report configuration for all agents',
+ },
+ 'dbaname',
+ { field => 'technology',
+ type => 'select',
+ options => [ map { @$_ } values(%$media_types) ],
+ 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 $media_types = FS::part_pkg_fcc_option->media_types;
+delete $media_types->{'Mobile Wireless'}; # cause this is the fixed zone page
+
+my $m2_error_callback = sub {
+ my ($cgi, $deploy_zone) = @_;
+ my @blocknums = grep {
+ /^blocknum\d+/ and length($cgi->param($_.'_censusblock'))
+ } $cgi->param;
+
+ sort { $a->censusblock <=> $b->censusblock }
+ 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>
diff --git a/httemplate/edit/deploy_zone-mobile.html b/httemplate/edit/deploy_zone-mobile.html
new file mode 100644
index 000000000..8e985b1c9
--- /dev/null
+++ b/httemplate/edit/deploy_zone-mobile.html
@@ -0,0 +1,90 @@
+<& elements/edit.html,
+ 'name_singular' => 'deployment zone',
+ 'table' => 'deploy_zone',
+ 'post_url' => popurl(1).'process/deploy_zone-mobile.html',
+ 'labels' => {
+ 'description' => 'Description',
+ 'agentnum' => 'Agent',
+ 'dbaname' => 'Business name (if different from agent)',
+ 'technology' => 'Technology',
+ 'spectrum' => 'Spectrum',
+ 'is_broadband', => 'Broadband Internet',
+ 'adv_speed_up' => 'Upstream',
+ 'adv_speed_down' => 'Downstream',
+ 'is_voice', => 'Voice',
+ 'vertexnum' => '',
+ 'active_date' => 'Active since',
+ },
+ 'fields' => [
+ { field => 'zonetype',
+ type => 'hidden',
+ value => 'P'
+ },
+ 'description',
+ { field => 'active_date',
+ type => 'fixed-date',
+ value => time,
+ },
+ { field => 'agentnum',
+ type => 'select-agent',
+ disable_empty => 1,
+ viewall_right => 'Edit FCC report configuration for all agents',
+ },
+ 'dbaname',
+ { field => 'technology',
+ type => 'select',
+ options => $media_types->{'Mobile Wireless'},
+ labels => $technology_labels,
+ },
+ { field => 'spectrum',
+ type => 'select',
+ options => [ keys %$spectrum_labels ],
+ labels => $spectrum_labels,
+ },
+ { field => 'is_broadband', type => 'checkbox', value=>'Y' },
+ { field => 'is_voice', type => 'checkbox', value=>'Y' },
+ { type => 'tablebreak-tr-title',
+ value => 'Advertised minimum speed (Mbps)' },
+ 'adv_speed_down',
+ 'adv_speed_up',
+ { type => 'tablebreak-tr-title', value => 'Footprint'},
+ { field => 'vertexnum',
+ type => 'deploy_zone_vertex',
+ o2m_table => 'deploy_zone_vertex',
+ 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 $spectrum_labels = FS::part_pkg_fcc_option->spectrum_labels;
+my $media_types = FS::part_pkg_fcc_option->media_types;
+
+my $m2_error_callback = sub {
+ my ($cgi, $deploy_zone) = @_;
+ my @vertexnums = sort { $a <=> $b } grep {
+ /^vertexnum\d+/ and length($cgi->param($_.'_latitude'))
+ } $cgi->param;
+
+ map {
+ my $k = $_;
+ my $s = 0;
+ FS::deploy_zone_vertex->new({
+ vertexnum => scalar($cgi->param($k)),
+ zonenum => $deploy_zone->zonenum,
+ latitude => scalar($cgi->param($k.'_latitude')),
+ longitude => scalar($cgi->param($k.'_longitude')),
+ })
+ } @vertexnums;
+};
+
+%init>
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index cd97be959..3c9f8b2ab 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -307,6 +307,8 @@ Example:
% 'disable_empty' => $f->{'disable_empty'},
% #select-reason
% 'reason_class' => $f->{'reason_class'},
+% #select-agent
+% 'viewall_right' => $f->{'viewall_right'},
%
% #selectlayers
% 'layer_fields' => $f->{'layer_fields'},
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index d1b60ec20..a007a9255 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -219,20 +219,26 @@
},
{ field=>'pay_weight', type=>'text', size=>6 },
{ field=>'credit_weight', type=>'text', size=>6 },
-
- ( $conf->exists('cust_pkg-show_fcc_voice_grade_equivalent')
- ? (
- { type => 'tablebreak-tr-title',
- value => 'FCC Form 477 information',
- },
- { field=>'fcc_voip_class',
- type=>'select-voip_class',
- },
- { field=>'fcc_ds0s', type=>'text', size=>6 },
- )
- : ()
- ),
-
+ ($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 => 'columnend' },
{ 'type' => $report_option ? 'tablebreak-tr-title'
@@ -369,6 +375,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/bulk-part_pkg-fcc.html b/httemplate/edit/process/bulk-part_pkg-fcc.html
new file mode 100644
index 000000000..4a0fb2a22
--- /dev/null
+++ b/httemplate/edit/process/bulk-part_pkg-fcc.html
@@ -0,0 +1,43 @@
+% if ( keys %error ) {
+% foreach my $pkgpart (keys %error) {
+% # stuff all the errors back into $cgi
+% $cgi->param("error$pkgpart", $error{$pkgpart});
+% }
+% my $session = int(rand(4294967296)); #XXX
+% my $pref = new FS::access_user_pref({
+% 'usernum' => $FS::CurrentUser::CurrentUser->usernum,
+% 'prefname' => "redirect$session",
+% 'prefvalue' => $cgi->query_string,
+% 'expiration' => time + 3600, #1h? 1m?
+% });
+% my $pref_error = $pref->insert;
+% if ( $pref_error ) {
+% die "FATAL: couldn't even set redirect cookie: $pref_error".
+% " attempting to set redirect$session to ". $cgi->query_string."\n";
+% }
+<% $cgi->redirect($fsurl.'browse/part_pkg-fcc.html?redirect='.$session) %>
+% } else {
+<% $cgi->redirect($fsurl.'browse/part_pkg-fcc.html?classnum='.$classnum) %>
+% }
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $edit_acl = $curuser->access_right('Edit FCC report configuration');
+my $global_edit_acl = $curuser->access_right('Edit FCC report configuration for all agents');
+die "access denied" unless $edit_acl or $global_edit_acl;
+
+# non-atomic; report errors but allow successful changes to go through
+# not that I even know how you'd get an error doing this
+
+my %error;
+foreach my $param ($cgi->param) {
+ $param =~ /^pkgpart(\d+)$/ or next;
+ my $pkgpart = $1;
+ my $part_pkg = FS::part_pkg->by_key($pkgpart);
+ my $hashref = decode_json( $cgi->param($param) );
+ my $error = $part_pkg->set_fcc_options($hashref);
+ $error{$pkgpart} = $error if $error;
+}
+
+my $classnum = $cgi->param('classnum');
+
+%init>
diff --git a/httemplate/edit/process/deploy_zone-fixed.html b/httemplate/edit/process/deploy_zone-fixed.html
new file mode 100644
index 000000000..c14c81c58
--- /dev/null
+++ b/httemplate/edit/process/deploy_zone-fixed.html
@@ -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/edit/process/deploy_zone-mobile.html b/httemplate/edit/process/deploy_zone-mobile.html
new file mode 100644
index 000000000..c913c5cd6
--- /dev/null
+++ b/httemplate/edit/process/deploy_zone-mobile.html
@@ -0,0 +1,9 @@
+<& elements/process.html,
+ error_redirect => popurl(2).'deploy_zone-mobile.html?',
+ table => 'deploy_zone',
+ viewall_dir => 'browse',
+ process_o2m =>
+ { 'table' => 'deploy_zone_vertex',
+ 'fields' => [qw( latitude longitude )]
+ },
+&>
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
index 9eb10d276..8e8be853d 100755
--- a/httemplate/edit/process/part_pkg.cgi
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -114,6 +114,14 @@ my $args_callback = sub {
push @args, 'options' => \%options;
###
+ # fcc options
+ ###
+ my $fcc_options_string = $cgi->param('fcc_options_string');
+ if ($fcc_options_string) {
+ push @args, 'fcc_options' => decode_json($fcc_options_string);
+ }
+
+ ###
#pkg_svc
###
diff --git a/httemplate/elements/deploy_zone_block.html b/httemplate/elements/deploy_zone_block.html
new file mode 100644
index 000000000..9985944bd
--- /dev/null
+++ b/httemplate/elements/deploy_zone_block.html
@@ -0,0 +1,47 @@
+% unless ( $opt{'js_only'} ) {
+
+
+ Block
+
+ >
+
+ Year
+
+ >
+% }
+<%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/deploy_zone_vertex.html b/httemplate/elements/deploy_zone_vertex.html
new file mode 100644
index 000000000..b3c8b31ea
--- /dev/null
+++ b/httemplate/elements/deploy_zone_vertex.html
@@ -0,0 +1,45 @@
+% unless ( $opt{'js_only'} ) {
+
+
+ Latitude
+
+ >
+
+ Longitude
+
+ >
+% }
+<%init>
+
+my( %opt ) = @_;
+
+my $name = $opt{'element_name'} || $opt{'field'} || 'vertexnum';
+my $id = $opt{'id'} || 'vertexnum';
+
+my $curr_value = $opt{'curr_value'} || $opt{'value'};
+
+my $onchange = $opt{'onchange'};
+if ( $onchange ) {
+ $onchange =~ s/\(what\);/(this);/;
+ $onchange = 'onchange="'.$onchange.'"';
+}
+
+my $deploy_zone_vertex = $curr_value
+ ? FS::deploy_zone_vertex->by_key($curr_value)
+ : FS::deploy_zone_vertex->new;
+
+%init>
diff --git a/httemplate/elements/input-fcc_options.html b/httemplate/elements/input-fcc_options.html
new file mode 100644
index 000000000..85a647043
--- /dev/null
+++ b/httemplate/elements/input-fcc_options.html
@@ -0,0 +1,114 @@
+% unless ($opt{js_only}) {
+<& hidden.html, 'field' => $id, @_ &>
+%# <& input-text.html, 'id' => $id, @_ &> # XXX debugging
+
+
+% }
+% unless ($opt{html_only}) {
+% my $popup = $fsurl.'misc/part_pkg_fcc_options.html?id=';
+% my $popup_name = 'popup-'.time. "-$$-". rand() * 2**32;
+
+% }
+<%init>
+my %opt = @_;
+my $id = $opt{id} || $opt{field};
+%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index 8355d7a2c..7e329cf23 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -258,8 +258,8 @@ $report_packages{'Suspended customer packages'} = [ $fsurl.'search/cust_pkg.cgi
$report_packages{'Suspension summary'} = [ $fsurl.'search/cust_pkg_susp.html', 'Show suspension activity', ]
if $curuser->access_right('Summarize packages');
$report_packages{'Customer packages with unconfigured services'} = [ $fsurl.'search/cust_pkg.cgi?APKG_pkgnum', 'List packages which have provisionable services' ];
-$report_packages{'FCC Form 477 packages'} = [ $fsurl.'search/report_477.html', 'Summarize packages by census tract for particular types' ]
- if $conf->exists('cust_main-require_censustract');
+$report_packages{'FCC Form 477'} = [ $fsurl.'search/report_477.html' ]
+ if $conf->exists('part_pkg-show_fcc_options');
$report_packages{'Advanced package reports'} = [ $fsurl.'search/report_cust_pkg.html', 'by agent, date range, status, package definition' ];
tie my %report_inventory, 'Tie::IxHash',
diff --git a/httemplate/elements/tr-input-fcc_options.html b/httemplate/elements/tr-input-fcc_options.html
new file mode 100644
index 000000000..58f7247c4
--- /dev/null
+++ b/httemplate/elements/tr-input-fcc_options.html
@@ -0,0 +1,102 @@
+
+
+
+ <& hidden.html, 'id' => $id, @_ &>
+%# <& input-text.html, 'id' => $id, @_ &> # XXX debugging
+
+
+% # show some kind of useful summary of the FCC options here
+ |
+
+
+<%init>
+my %opt = @_;
+my $id = $opt{id} || $opt{field};
+%init>
diff --git a/httemplate/misc/part_pkg_fcc_options.html b/httemplate/misc/part_pkg_fcc_options.html
new file mode 100644
index 000000000..27b45e003
--- /dev/null
+++ b/httemplate/misc/part_pkg_fcc_options.html
@@ -0,0 +1,221 @@
+<& /elements/header-popup.html &>
+
+
+
+<%def .checkbox>
+% my $field = shift;
+% my $extra = shift || '';
+>
+%def>
+
+
+
+ <& .checkbox, 'is_phone' &>
+
+
+
+
+ <& .checkbox, 'is_voip' &>
+
+
+
+
+
+ <& .checkbox, 'voip_lastmile' &>
+
+
+
+
+ <& .checkbox, 'is_mobile' &>
+
+
+
+ <& .checkbox, 'mobile_direct' &>
+
+
+
+
+
+
+
+
+<& /elements/footer.html &>
+<%init>
+my $media_types = FS::part_pkg_fcc_option->media_types;
+my $technology_labels = FS::part_pkg_fcc_option->technology_labels;
+
+my $parent_id = $cgi->param('id');
+%init>
diff --git a/httemplate/search/477.html b/httemplate/search/477.html
old mode 100755
new mode 100644
index ecf21cfb1..fb85f1e09
--- a/httemplate/search/477.html
+++ b/httemplate/search/477.html
@@ -1,135 +1,239 @@
-% if ( $type eq 'xml' ) {
-% $filename = "fcc_477_$state" . '_' . time2str('%Y%m%d', $date) . '.xml';
-% http_header('Content-Type' => 'application/XML' ); # So saith RFC 4180
-% http_header('Content-Disposition' => 'attachment;filename="'.$filename.'"');
-
-
-% } else { #html
-<& /elements/header.html, "FCC Form 477 Results - $state" &>
-%# XXX when we stop supporting IE8, add this to freeside.css using :nth-child
-%# selectors, and remove it from everywhere else
+<& /elements/header.html, $title &>
-
-
-
- |
-
- Download full results
-% $cgi->param('_type', 'xml');
- as XML file
-
-% $cgi->param('_type', 'html-print');
- as printable copy
-
- |
-% $cgi->param('_type', $type );
-
-
-% } #html
-% foreach my $part ( @parts ) {
-% if ( $part{$part} ) {
-%
-% if ( $part eq 'V' ) {
-% next unless ( $part{'IIA'} || $part{'IIB'} );
-% }
-%
-% if ( $part eq 'VI_census' ) {
-% next unless $part{'IA'};
-% }
-%
-% my @reports = ();
-% if ( $part eq 'IA' ) {
-% for ( my $tech = 0; $tech < scalar(@technology_option); $tech++ ) {
-% next unless $technology_option[$tech];
-% my $url = &{$url_mangler}($part);
-% if ( $type eq 'xml' ) {
-<<% 'Part_IA_'. chr(65 + $tech) %>>
-% }
-<& "477part${part}.html",
- 'tech_code' => $tech,
- 'url' => $url,
- 'type' => $type,
- 'date' => $date,
-&>
-% if ( $type eq 'xml' ) {
-<% 'Part_IA_'. chr(65 + $tech) %>>
-% }
-% }
-% } else { # not part IA
-% if ( $type eq 'xml' ) {
-<<% 'Part_'. $part %>>
-% }
-% my $url = &{$url_mangler}($part);
-<& "477part${part}.html",
- 'url' => $url,
- 'date' => $date,
- 'filename' => $filename,
-&>
-% if ( $type eq 'xml' ) {
-<% 'Part_'. $part %>>
-% }
+% foreach my $partname (@partnames) {
+% $cgi->param('parts', $partname);
+% $cgi->param('type', 'csv');
+
+
+ <% $part_titles->{$partname} %>
+ Download
+
+% my $header = ".header_$partname";
+% my $data = $parts{$partname};
+
+ <& $header &>
+
+% foreach my $row (@$data) {
+
+% foreach my $item (@$row) {
+ <% $item %> |
% }
+
% }
-% }
-%
-% if ( $type eq 'xml' ) {
-
-% } else {
+
+% } # foreach $partname
<& /elements/footer.html &>
-% }
<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
-
die "access denied"
- unless $curuser->access_right('List packages');
-
-my $date = $cgi->param('date') ? parse_datetime($cgi->param('date'))
- : time;
-
-my $state = uc($cgi->param('state'));
-$state =~ /^[A-Z]{2}$/ or die "illegal state: $state";
+ unless $FS::CurrentUser::CurrentUser->access_right('List packages');
-my %part = map { $_ => 1 } grep { /^\w+$/ } $cgi->param('part');
-my $type = $cgi->param('_type') || 'html';
-my $filename;
-my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi,1);
+my %parts;
+# load from cache if possible
+my $session;
+if ( $cgi->param('session') =~ /^(\d+)$/ ) {
+ $session = $1;
+ %parts = %{ $m->cache->get($session) };
+} else {
+ $session = sprintf('%010d%06d', time, int(rand(1000000)));
+ $cgi->param('session', $session);
+}
-# save upload and download mappings
-my @download = $cgi->param('part1_column_option');
-my @upload = $cgi->param('part1_row_option');
-for(my $i=0; $i < scalar(@download); $i++) {
- &FS::Report::FCC_477::save_fcc477map("part1_column_option_$i",$download[$i]);
+my $agentnum;
+if ($cgi->param('agentnum') =~ /^(\d+)$/ ) {
+ $agentnum = $1;
}
-for(my $i=0; $i < scalar(@upload); $i++) {
- &FS::Report::FCC_477::save_fcc477map("part1_row_option_$i",$upload[$i]);
+my $date = parse_datetime($cgi->param('date')) || time;
+my @partnames = grep /^\w+$/, $cgi->param('parts');
+foreach my $partname (@partnames) {
+ $parts{$partname} ||= FS::Report::FCC_477->report( $partname,
+ date => $date,
+ agentnum => $agentnum
+ );
}
+$m->cache->set($session, \%parts, '1h');
-my @part2a_row_option = $cgi->param('part2a_row_option');
-for(my $i=0; $i < scalar(@part2a_row_option); $i++) {
- &FS::Report::FCC_477::save_fcc477map("part2a_row_option_$i",$part2a_row_option[$i]);
-}
+my $title = 'FCC Form 477 Data - ' . time2str('%b %o, %Y', $date);
-my @part2b_row_option = $cgi->param('part2b_row_option');
-for(my $i=0; $i < scalar(@part2b_row_option); $i++) {
- &FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]);
-}
+if ( $cgi->param('type') eq 'csv' ) {
+ 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 $part5_report_option = $cgi->param('part5_report_option');
-if ( $part5_report_option ) {
- FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option);
+ my $filename = time2str('%Y-%m-%d', $date) . '-'. $partname . '.csv';
+ http_header('Content-Type' => 'text/csv');
+ http_header('Content-Disposition' => qq(attachment;filename="$filename"));
+
+ $m->clear_buffer;
+
+ foreach my $row (@$data) {
+ $csv->combine(@$row);
+ $m->print($csv->string);
+ }
+ $m->abort;
}
-my $url_mangler = sub {
- my $part = shift;
- my $url = $cgi->url('-path_info' => 1, '-full' => 1);
- $url =~ s/477\./477part$part./;
- $url;
-};
-my @parts = qw( IA IIA IIB IV V VI_census );
+my $part_titles = FS::Report::FCC_477->parts;
%init>
+<%def .header_fbd>
+
+ Census Block |
+ DBA Name |
+ Technology |
+ Consumer? |
+ Advertised Speed (Mbps) |
+ Business? |
+ Contractual Speed (Mbps) |
+
+
+ Down |
+ Up |
+ Down |
+ Up |
+
+%def>
+<%def .header_fbs>
+
+ Census Tract |
+ Technology |
+ Speed (Mbps) |
+ Subscriptions |
+
+
+ Down |
+ Up |
+ Total |
+ Consumer |
+
+%def>
+<%def .header_fvs>
+
+ Census Tract |
+ VoIP? |
+ Lines/Subscriptions |
+
+
+ Total |
+ Consumer |
+
+%def>
+<%def .header_lts>
+
+ State |
+ Wholesale |
+ End User Lines |
+
+
+ VGEs |
+ UNE-Ls |
+
+ Total |
+ With Broadband |
+ Consumer |
+ Business |
+
+ Local Loop |
+
+ Special Media |
+
+
+
+ |
+ +LD |
+ |
+ +LD |
+
+ Owned |
+ UNE-L |
+ Resale |
+
+ Fiber |
+ Coaxial |
+ Wireless |
+
+%def>
+<%def .header_voip>
+
+ State |
+ VoIP OTT |
+ VoIP Non-OTT |
+
+
+ Total |
+ Consumer |
+
+ Total |
+ Consumer |
+ Bundled |
+ Media Type |
+
+
+ Copper |
+ Fiber |
+ Coaxial |
+ Wireless |
+ Other |
+
+%def>
+<%def .header_mbs>
+%# unimplemented
+
+ State |
+ Speed (Mbps) |
+ Subscriptions |
+
+
+ Down |
+ Up |
+ Total |
+ Consumer |
+
+%def>
+<%def .header_mvs>
+%# unimplemented
+
+ State |
+ Subscriptions |
+
+
+ Total |
+ Direct |
+
+%def>
+
diff --git a/httemplate/search/old477/477.html b/httemplate/search/old477/477.html
new file mode 100644
index 000000000..ecf21cfb1
--- /dev/null
+++ b/httemplate/search/old477/477.html
@@ -0,0 +1,135 @@
+% if ( $type eq 'xml' ) {
+% $filename = "fcc_477_$state" . '_' . time2str('%Y%m%d', $date) . '.xml';
+% http_header('Content-Type' => 'application/XML' ); # So saith RFC 4180
+% http_header('Content-Disposition' => 'attachment;filename="'.$filename.'"');
+
+
+% } else { #html
+<& /elements/header.html, "FCC Form 477 Results - $state" &>
+%# XXX when we stop supporting IE8, add this to freeside.css using :nth-child
+%# selectors, and remove it from everywhere else
+
+
+
+
+ |
+
+ Download full results
+% $cgi->param('_type', 'xml');
+ as XML file
+
+% $cgi->param('_type', 'html-print');
+ as printable copy
+
+ |
+% $cgi->param('_type', $type );
+
+
+% } #html
+% foreach my $part ( @parts ) {
+% if ( $part{$part} ) {
+%
+% if ( $part eq 'V' ) {
+% next unless ( $part{'IIA'} || $part{'IIB'} );
+% }
+%
+% if ( $part eq 'VI_census' ) {
+% next unless $part{'IA'};
+% }
+%
+% my @reports = ();
+% if ( $part eq 'IA' ) {
+% for ( my $tech = 0; $tech < scalar(@technology_option); $tech++ ) {
+% next unless $technology_option[$tech];
+% my $url = &{$url_mangler}($part);
+% if ( $type eq 'xml' ) {
+<<% 'Part_IA_'. chr(65 + $tech) %>>
+% }
+<& "477part${part}.html",
+ 'tech_code' => $tech,
+ 'url' => $url,
+ 'type' => $type,
+ 'date' => $date,
+&>
+% if ( $type eq 'xml' ) {
+<% 'Part_IA_'. chr(65 + $tech) %>>
+% }
+% }
+% } else { # not part IA
+% if ( $type eq 'xml' ) {
+<<% 'Part_'. $part %>>
+% }
+% my $url = &{$url_mangler}($part);
+<& "477part${part}.html",
+ 'url' => $url,
+ 'date' => $date,
+ 'filename' => $filename,
+&>
+% if ( $type eq 'xml' ) {
+<% 'Part_'. $part %>>
+% }
+% }
+% }
+% }
+%
+% if ( $type eq 'xml' ) {
+
+% } else {
+<& /elements/footer.html &>
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+ unless $curuser->access_right('List packages');
+
+my $date = $cgi->param('date') ? parse_datetime($cgi->param('date'))
+ : time;
+
+my $state = uc($cgi->param('state'));
+$state =~ /^[A-Z]{2}$/ or die "illegal state: $state";
+
+my %part = map { $_ => 1 } grep { /^\w+$/ } $cgi->param('part');
+my $type = $cgi->param('_type') || 'html';
+my $filename;
+my @technology_option = &FS::Report::FCC_477::parse_technology_option($cgi,1);
+
+# save upload and download mappings
+my @download = $cgi->param('part1_column_option');
+my @upload = $cgi->param('part1_row_option');
+for(my $i=0; $i < scalar(@download); $i++) {
+ &FS::Report::FCC_477::save_fcc477map("part1_column_option_$i",$download[$i]);
+}
+for(my $i=0; $i < scalar(@upload); $i++) {
+ &FS::Report::FCC_477::save_fcc477map("part1_row_option_$i",$upload[$i]);
+}
+
+my @part2a_row_option = $cgi->param('part2a_row_option');
+for(my $i=0; $i < scalar(@part2a_row_option); $i++) {
+ &FS::Report::FCC_477::save_fcc477map("part2a_row_option_$i",$part2a_row_option[$i]);
+}
+
+my @part2b_row_option = $cgi->param('part2b_row_option');
+for(my $i=0; $i < scalar(@part2b_row_option); $i++) {
+ &FS::Report::FCC_477::save_fcc477map("part2b_row_option_$i",$part2b_row_option[$i]);
+}
+
+my $part5_report_option = $cgi->param('part5_report_option');
+if ( $part5_report_option ) {
+ FS::Report::FCC_477::save_fcc477map('part5_report_option', $part5_report_option);
+}
+
+my $url_mangler = sub {
+ my $part = shift;
+ my $url = $cgi->url('-path_info' => 1, '-full' => 1);
+ $url =~ s/477\./477part$part./;
+ $url;
+};
+my @parts = qw( IA IIA IIB IV V VI_census );
+
+%init>
diff --git a/httemplate/search/477partIA.html b/httemplate/search/old477/477partIA.html
old mode 100755
new mode 100644
similarity index 97%
rename from httemplate/search/477partIA.html
rename to httemplate/search/old477/477partIA.html
index 97f8ac0aa..55e901bb3
--- a/httemplate/search/477partIA.html
+++ b/httemplate/search/old477/477partIA.html
@@ -80,13 +80,17 @@ die "access denied"
my %opt = @_;
my %search_hash;
-
+
for ( qw(agentnum state) ) {
$search_hash{$_} = $cgi->param($_) if $cgi->param($_);
-}
+} # note that separation by state is no longer required after July 2014
$search_hash{'country'} = 'US';
$search_hash{'classnum'} = [ $cgi->param('classnum') ];
+my $info = FS::part_pkg_fcc_option->info;
+
+
+
# arrays of report_option_ numbers, running parallel to
# the download and upload speed arrays
my @download_option = $cgi->param('part1_column_option');
diff --git a/httemplate/search/477partIIA.html b/httemplate/search/old477/477partIIA.html
old mode 100755
new mode 100644
similarity index 100%
rename from httemplate/search/477partIIA.html
rename to httemplate/search/old477/477partIIA.html
diff --git a/httemplate/search/477partIIB.html b/httemplate/search/old477/477partIIB.html
old mode 100755
new mode 100644
similarity index 100%
rename from httemplate/search/477partIIB.html
rename to httemplate/search/old477/477partIIB.html
diff --git a/httemplate/search/477partIV.html b/httemplate/search/old477/477partIV.html
old mode 100755
new mode 100644
similarity index 100%
rename from httemplate/search/477partIV.html
rename to httemplate/search/old477/477partIV.html
diff --git a/httemplate/search/477partV.html b/httemplate/search/old477/477partV.html
old mode 100755
new mode 100644
similarity index 98%
rename from httemplate/search/477partV.html
rename to httemplate/search/old477/477partV.html
index 2ffad2a27..80201f9d7
--- a/httemplate/search/477partV.html
+++ b/httemplate/search/old477/477partV.html
@@ -1,7 +1,7 @@
% if ( $cgi->param('_type') =~ /^xml$/ ) {
% }
-<& elements/search.html,
+<& /search/elements/search.html,
'html_init' => $html_init,
'name' => 'zip code',
'query' => $sql_query,
diff --git a/httemplate/search/477partVI_census.html b/httemplate/search/old477/477partVI_census.html
old mode 100755
new mode 100644
similarity index 99%
rename from httemplate/search/477partVI_census.html
rename to httemplate/search/old477/477partVI_census.html
index 2f3cf419a..efcf4ef1b
--- a/httemplate/search/477partVI_census.html
+++ b/httemplate/search/old477/477partVI_census.html
@@ -1,4 +1,4 @@
-<& elements/search.html,
+<& /search/elements/search.html,
'html_init' => 'Part VI
',
'html_foot' => $html_foot,
'name' => 'regions',
diff --git a/httemplate/search/old477/report_477.html b/httemplate/search/old477/report_477.html
new file mode 100644
index 000000000..a5dd70b7c
--- /dev/null
+++ b/httemplate/search/old477/report_477.html
@@ -0,0 +1,282 @@
+<% include('/elements/header.html', 'FCC Form 477 Report' ) %>
+
+
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+
+my @states = qsearch({
+ 'table' => 'cust_location',
+ 'select' => 'DISTINCT(state)',
+ 'hashref' => { 'country' => 'US' }, # 477 report isn't relevant elsewhere
+});
+
+%init>
diff --git a/httemplate/search/report_477.html b/httemplate/search/report_477.html
index a5dd70b7c..cbbd5d902 100755
--- a/httemplate/search/report_477.html
+++ b/httemplate/search/report_477.html
@@ -1,32 +1,30 @@
-<% include('/elements/header.html', 'FCC Form 477 Report' ) %>
-
+% if ( $conf->exists('old_fcc_report') ) {
+% $m->clear_buffer;
+% $m->print($cgi->redirect($fsurl . 'search/old477/report_477.html'));
+% $m->abort;
+% }
+<& /elements/header.html, 'FCC Form 477 Report' &>
+% if ( $curuser->access_right('Edit FCC report configuration') ) {
+Preparation
+
+% }
+
-<% include('/elements/footer.html') %>
+<& /elements/footer.html &>
<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
die "access denied"
- unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+ unless $curuser->access_right('List packages');
+
+my $conf = FS::Conf->new;
-my @states = qsearch({
- 'table' => 'cust_location',
- 'select' => 'DISTINCT(state)',
- 'hashref' => { 'country' => 'US' }, # 477 report isn't relevant elsewhere
-});
+my $part_titles = FS::Report::FCC_477->parts;
%init>
--
2.11.0