From 022bfd91eca7ae26f8f6ee125179f5c0ff4cbb72 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Thu, 7 Aug 2014 13:50:51 -0700 Subject: [PATCH] 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; + 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!
+ ( 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, + ). + ' ) +

' . + qq!!; + +my $html_foot = qq! + +
!; + +my @menubar = + ( 'Package definitions' => $p.'browse/part_pkg.cgi' ); + + +<%def .style> + + 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; +}; + + 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; +}; + + 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'); + + 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; + + 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; + + 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}; + 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}; + 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 || ''; +> + + +
+% # The option structure is hardcoded. The FCC rules changed enough from +% # the original 477 report to the 2013 revision that any data-driven +% # mechanism for expressing the original rules would likely fail to +% # accommodate the new ones. Therefore, we assume that whatever the FCC +% # does NEXT will also require a rewrite of this form, and we'll deal with +% # that when it arrives. +

+ + +

+

+ <& .checkbox, 'is_consumer' &> + +

+

+ <& .checkbox, 'is_broadband' &> + +

+ + +
+ + +
+ + +
+

+

+ <& .checkbox, 'is_phone' &> + +

+ + +
+ + +
+ + <& .checkbox, 'phone_longdistance' &> +
+ + +
+
+ + +
+ + +
+
+

+

+ <& .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'); + 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 &> - - - - - -% $cgi->param('_type', $type ); - -
- Download full results
-% $cgi->param('_type', 'xml'); - as XML file
- -% $cgi->param('_type', 'html-print'); - as printable copy - -
-% } #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' ) { -> -% } -% } -% } 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' ) { -> -% } +% foreach my $partname (@partnames) { +% $cgi->param('parts', $partname); +% $cgi->param('type', 'csv'); + + +% my $header = ".header_$partname"; +% my $data = $parts{$partname}; + + <& $header &> + +% foreach my $row (@$data) { + +% foreach my $item (@$row) { + % } + % } -% } -% -% if ( $type eq 'xml' ) { - -% } else { +
+ <% $part_titles->{$partname} %> + Download +
<% $item %>
+% } # 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; +<%def .header_fbd> + + Census Block + DBA Name + Technology + Consumer? + Advertised Speed (Mbps) + Business? + Contractual Speed (Mbps) + + + Down + Up + Down + Up + + +<%def .header_fbs> + + Census Tract + Technology + Speed (Mbps) + Subscriptions + + + Down + Up + Total + Consumer + + +<%def .header_fvs> + + Census Tract + VoIP? + Lines/Subscriptions + + + Total + Consumer + + +<%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 .header_voip> + + State + VoIP OTT + VoIP Non-OTT + + + Total + Consumer + + Total + Consumer + Bundled + Media Type + + + Copper + Fiber + Coaxial + Wireless + Other + + +<%def .header_mbs> +%# unimplemented + + State + Speed (Mbps) + Subscriptions + + + Down + Up + Total + Consumer + + +<%def .header_mvs> +%# unimplemented + + State + Subscriptions + + + Total + Direct + + + 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 + + + + + + +% $cgi->param('_type', $type ); + +
+ Download full results
+% $cgi->param('_type', 'xml'); + as XML file
+ +% $cgi->param('_type', 'html-print'); + as printable copy + +
+% } #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' ) { +> +% } +% } +% } 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' ) { +> +% } +% } +% } +% } +% +% 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 ); + + 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/tr-select-agent.html', + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'disable_empty' => 0, + ) + %> + +% # not tr-select-state, we only want to choose from among those that +% # have customers + <& /elements/tr-select-table.html, + 'label' => 'State', + 'field' => 'state', + 'table' => 'cust_location', + 'name_col' => 'state', + 'value_col' => 'state', + 'disable_empty' => 1, + 'records' => \@states, + &> + + <& /elements/tr-input-date-field.html, { + 'label' => 'As of date', + 'name' => 'date', + 'value' => '', + 'format' => '%m/%d/%Y' + } &> + + <% include( '/elements/tr-select-pkg_class.html', + 'multiple' => 1, + 'empty_label' => '(empty class)', + ) + %> + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IA?', + 'field' => 'part', + 'id' => 'enableIA', + 'value' => 'IA', + 'onchange' => 'partchange(this); toggleVI();', + ) + %> + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IIA?', + 'field' => 'part', + 'id' => 'enableIIA', + 'value' => 'IIA', + 'onchange' => 'partchange(this); toggleV();', + ) + %> + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IIB?', + 'field' => 'part', + 'id' => 'enableIIB', + 'value' => 'IIB', + 'onchange' => 'partchange(this); toggleV();', + ) + %> + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part IV?', + 'field' => 'part', + 'id' => 'enableIV', #unused + 'value' => 'IV', + 'onchange' => 'partchange(this)', + ) + %> + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part V?', + 'field' => 'part', + 'value' => 'V', + 'id' => 'enableV', + 'onchange' => 'partchange(this)', + 'postfix' => + ' (requires Part IIA or IIB)', + ) + %> + + + + + + + <% include( '/elements/tr-checkbox.html', + 'label' => 'Enable part VI?', + 'field' => 'part', + 'id' => 'enableVI', + 'value' => 'VI_census', + 'postfix' => + ' (requires part IA)', + ) + %> + +
+ Search options +
+ +
+ + +
+ +<% 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 +}); + + 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/tr-select-agent.html', - 'curr_value' => scalar( $cgi->param('agentnum') ), - 'disable_empty' => 0, - ) - %> - -% # not tr-select-state, we only want to choose from among those that -% # have customers - <& /elements/tr-select-table.html, - 'label' => 'State', - 'field' => 'state', - 'table' => 'cust_location', - 'name_col' => 'state', - 'value_col' => 'state', - 'disable_empty' => 1, - 'records' => \@states, + <& /elements/tr-select-agent.html, + 'curr_value' => scalar( $cgi->param('agentnum') ), + 'disable_empty' => 0, &> <& /elements/tr-input-date-field.html, { @@ -36,247 +34,28 @@ 'format' => '%m/%d/%Y' } &> - <% include( '/elements/tr-select-pkg_class.html', - 'multiple' => 1, - 'empty_label' => '(empty class)', - ) - %> - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part IA?', - 'field' => 'part', - 'id' => 'enableIA', - 'value' => 'IA', - 'onchange' => 'partchange(this); toggleVI();', - ) - %> - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part IIA?', - 'field' => 'part', - 'id' => 'enableIIA', - 'value' => 'IIA', - 'onchange' => 'partchange(this); toggleV();', - ) - %> - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part IIB?', - 'field' => 'part', - 'id' => 'enableIIB', - 'value' => 'IIB', - 'onchange' => 'partchange(this); toggleV();', - ) - %> - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part IV?', - 'field' => 'part', - 'id' => 'enableIV', #unused - 'value' => 'IV', - 'onchange' => 'partchange(this)', - ) - %> - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part V?', - 'field' => 'part', - 'value' => 'V', - 'id' => 'enableV', - 'onchange' => 'partchange(this)', - 'postfix' => - ' (requires Part IIA or IIB)', - ) - %> - - - - - - - <% include( '/elements/tr-checkbox.html', - 'label' => 'Enable part VI?', - 'field' => 'part', - 'id' => 'enableVI', - 'value' => 'VI_census', - 'postfix' => - ' (requires part IA)', - ) - %> - + <& /elements/tr-checkbox-multiple.html, + 'label' => 'Enable parts', + 'field' => 'parts', + 'labels' => $part_titles, + 'options' => [ keys %$part_titles ] + &>
- Search options + Report options
-
- +
+
-<% 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; -- 2.11.0