default to a session cookie instead of setting an explicit timeout, weird timezone...
[freeside.git] / httemplate / search / 477.html
old mode 100755 (executable)
new mode 100644 (file)
index 0f1502f..9a41261
-<% include( 'elements/search.html',
-                  'title'         => 'FCC Form 477 Results', 
-                  'html_init'     => $html_init,
-                  'name'          => 'regions',
-                  'query'         => [ @sql_query ],
-                  'count_query'   =>  $count_query,
-                  'order_by'      => 'ORDER BY censustract',
-                  'avoid_quote'   => 1,
-                  'no_csv_header' => 1,
-                  'header'        => [
-                                       'County code',
-                                       'Census tract code',
-                                       'Upload rate',
-                                       'Download rate',
-                                       'Technology code',
-                                       'Technology code other',
-                                       'Quantity',
-                                       'Percentage residential',
-                                     ],
-                  'fields'        => [
-                    sub { my $row = shift; substr($row->censustract, 2, 3) },
-                    sub { my $row = shift; substr($row->censustract, 5) },
-                    'upload',
-                    'download',
-                    sub { 7 },
-                    sub { '' },
-                    'quantity',
-                    sub { my $row = shift; sprintf "%.2f", $row->residential },
-                  ],
-                  'links'        => [
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                    [ $link, $link_suffix ],
-                  ],
-              )
-%>
-<%init>
-
-my $curuser = $FS::CurrentUser::CurrentUser;
+<& /elements/header.html, $title &>
+<STYLE TYPE="text/css">
+table.fcc477part {
+  border-collapse: collapse;
+  border: 1px #777 solid;
+  margin-bottom: 20px;
+}
+table.fcc477part td {
+  padding: 0px 4px;
+  border-left: 1px #777 solid;
+  border-right: 1px #777 solid;
+}
+table.fcc477part tbody td {
+  text-align: right;
+}
+table.fcc477part thead tr.head {
+  text-align: center;
+  vertical-align: top;
+  font-weight: bold;
+  border-top: 1px #777 solid;
+  border-bottom: 1px #777 solid;
+}
+table.fcc477part thead tr.subhead {
+  text-align: center;
+  font-weight: bold;
+  font-size: small;
+  border-top: 1px #777 solid;
+  border-bottom: 1px #777 solid;
+}
+.parttitle {
+  font-weight: bold;
+  font-size: large;
+  float: left;
+}
+.errortitle {
+  font-weight: bold;
+  color: #ff0000;
+}
+tr.error td {
+  background-color: #ffdddd;
+}
+tr.error td.error {
+  text-align: left;
+  border: none;
+}
+tr.error ul {
+  margin: 0px;
+  list-style-image: url("<% $fsurl %>images/cross.png");
+}
+a.download {
+  float: right;
+}
+</STYLE>
+% foreach my $partname (@partnames) {
+%   my $this_part = $parts{$partname};
+%   $cgi->param('parts', $partname);
+%   $cgi->param('type', 'csv');
+<table class="fcc477part">
+  <caption>
+    <span class="parttitle"><% $part_titles->{$partname} %></span>
+%   if ( $this_part->{num_errors} > 0 ) {
+%     # show error heading while it contains errors
+      <span class="errortitle">
+        <% emt('This section contains [quant,_1,error].', $this_part->{num_errors}) %>
+      </span>
+%   }
+    <a class="download" href="<% $cgi->self_url %>">Download</a>
+  </caption>
+%   my $header = ".header_$partname";
+%   $header .= '_old' if $partname eq 'fbd' && $date < 1569826800; #9/30/2019
+%              # ( halfway between the two filing "as of" dates when it changed
 
+%   my $data = $this_part->{data};
+%   my $error = $this_part->{error};
+  <thead>
+    <& $header &>
+  </thead>
+%   my $rownum = 0;
+%   foreach my $row (@$data) {
+%     my %eh; # error hash
+%     if ( $error->[$rownum] ) {
+%       %eh = %{ $error->[$rownum] };
+%     }
+  <tr<% keys(%eh) ? ' class="error"' : ''%>>
+%     my $first = 1;
+%     foreach my $item (@$row) {
+    <td>
+%     if ($first and $part_link{$partname}) {
+      <a href="<% $part_link{$partname} . "477rownum=$rownum" %>"><% $item || '(empty)' %></a>
+%       $first = 0;
+%     } else {
+      <% $item %>
+%     }
+    </td>
+%   } #foreach $item
+%   # display errors
+%   if ( keys %eh ) {
+    <td class="error"><ul>
+%     foreach my $key (sort keys %eh) {
+      <li><% $eh{$key} %></li>
+%     }
+    </ul></td>
+%   } # if there are errors
+  </tr>
+%   $rownum++;
+%   } #foreach $row
+</table>
+% } # foreach $partname
+<& /elements/footer.html &>
+<%init>
 die "access denied"
-  unless $curuser->access_right('List packages');
+  unless $FS::CurrentUser::CurrentUser->access_right('List packages');
 
-my %search_hash = ();
-my @sql_query = ();
-  
-for ( qw(agentnum magic classnum) ) {
-  $search_hash{$_} = $cgi->param($_) if $cgi->param($_);
+my %parts;
+my %part_link;
+# 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);
 }
 
-my @column_option = $cgi->param('column_option')
-  if $cgi->param('column_option');
-
-my @row_option = $cgi->param('row_option')
-  if $cgi->param('row_option');
-
-my $where = join(' OR ', map { "num = $_" } grep { /^\d+$/ } @column_option );
-my %column_option_name = $where ?
-                           ( map { $_->name => $_->num }
-                             qsearch({ 'table' => 'part_pkg_report_option',
-                                       'hashref' => {},
-                                       'extra_sql' => "WHERE $where",
-                                    })
-                           ) :
-                           ( 'all packages' => '' );
-
-$where = join(' OR ', map { "num = $_" } grep { /^\d+$/ } @row_option );
-my %row_option_name = $where ?
-                        ( map { $_->name => $_->num }
-                          qsearch({ 'table' => 'part_pkg_report_option',
-                                    'hashref' => {},
-                                    'extra_sql' => "WHERE $where",
-                                 })
-                        ) :
-                        ( 'all packages' => '' );
-
-@row_option = map { $row_option_name{$_} } sort keys %row_option_name;
-@column_option = map { $column_option_name{$_} } sort keys %column_option_name;
-
-#$search_hash{row_option} = join(',', @row_option) if @row_option;
-my $summary .= '<H2>Summary</H2>'. include('/elements/table.html');
-$summary .= '<TR><TH></TH>';
-foreach my $column ( sort keys %column_option_name ) {
-  $summary .= "<TH>$column</TH>";
+my $agentnum;
+if ($cgi->param('agentnum') =~ /^(\d+)$/ ) {
+  $agentnum = $1;
 }
-$summary .= "</TR>";
-
-my $total_count = 0;
-my $total_residential = 0;
-my $rowcount = 1;
-foreach my $row ( sort keys %row_option_name ) {
-
-  $summary .= "<TR><TH>$row</TH>";
-
-  my $columncount = 2;
-  foreach my $column ( sort keys %column_option_name ) {
-    my @report_option = ();
-    push @report_option, $row_option_name{$row}
-      if $row_option_name{$row};
-    push @report_option, $column_option_name{$column}
-      if $column_option_name{$column};
-    my $report_option = join(',', @report_option) if @report_option;
-
-    my $sql_query = FS::cust_pkg->search(
-      { %search_hash,
-        ($report_option ? ( 'report_option' => $report_option ) : () ),
-      }
-    );
-    my $extracolumns = "$rowcount AS upload, $columncount AS download";
-    my $percent = "CASE WHEN count(*) > 0 THEN 100-100*cast(count(cust_main.company) as numeric)/cast(count(*) as numeric) ELSE cast(0 as numeric) END AS residential";
-    $sql_query->{select} = "count(*) AS quantity, $extracolumns, censustract, $percent";
-    $sql_query->{extra_sql} =~ /^(.*)(ORDER BY pkgnum)(.*)$/s
-      or die "couldn't parse extra_sql";
-    $sql_query->{extra_sql} = "$1 GROUP BY censustract $3"; 
-
-    my $count_sql = delete($sql_query->{'count_query'});
-    $count_sql =~ s/ FROM/,count(cust_main.company) FROM/
-      or die "couldn't parse count_sql";
-
-    my $count_sth = dbh->prepare($count_sql)
-      or die "Error preparing $count_sql: ". dbh->errstr;
-    $count_sth->execute
-      or die "Error executing $count_sql: ". $count_sth->errstr;
-    my $count_arrayref = $count_sth->fetchrow_arrayref;
-    my $count = $count_arrayref->[0];
-    $total_count += $count;
-    my $residential = $count_arrayref->[1];
-    $total_residential += $residential;
-    my $percentage = sprintf('%.2f', $count ? 100-100*$residential/$count : 0);
-
-    $summary .= "<TD>$count<BR>$percentage% residential</TD>";
-    push @sql_query, $sql_query;
-    $columncount++;
-  }
+my $date = parse_datetime($cgi->param('date')) || time;
+my @partnames = grep /^\w+$/, $cgi->param('parts');
+my $ignore_quantity = ($cgi->param('ignore_quantity') ? 1 : 0);
 
-  $summary .= "</TR>";
-  $rowcount++;
+foreach my $partname (@partnames) {
+  $parts{$partname} ||= FS::Report::FCC_477->report( $partname,
+    date            => $date,
+    agentnum        => $agentnum,
+    ignore_quantity => $ignore_quantity,
+  ); # includes error, detail, and data parts
+  my $detail_table = FS::Report::FCC_477->part_table($partname);
+  if ($detail_table eq 'cust_pkg') {
+    my $link = popurl(1).'477_cust_pkg.html?477part='.$partname.";date=$date;";
+    if ($agentnum) {
+      $link .= "agentnum=$agentnum;";
+    }
+    $part_link{$partname} = $link;
+  } # don't include detail links to deploy_blocks, that's pointless
 }
+$m->cache->set($session, \%parts, '1h');
+
+my $title = 'FCC Form 477 Data - ' . time2str('%b %o, %Y', $date);
+
+if ( $cgi->param('type') eq 'csv' ) {
+  my $partname = $partnames[0]; # ignore any beyond the first
+  my $data = $parts{$partname}->{data};
+  my $csv = Text::CSV_XS->new({ eol => "\r\n" }); # i think
+
+  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 $part_titles = FS::Report::FCC_477->parts;
 
-my $total_percentage =
-  sprintf("%.2f", $total_count ? 100-100*$total_residential/$total_count : 0);
-
-my $html_init = '<H2>Totals</H2>'. include('/elements/table.html'). "<TR>";
-$html_init .= "<TH>Total Connections</TH>";
-$html_init .= "<TH>% owned loop</TH>";
-$html_init .= "<TH>% billed to end users</TH>";
-$html_init .= "<TH>% residential</TH>";
-$html_init .= "<TH>% residential &gt; 200kbps</TH>";
-$html_init .= "</TR><TR>";
-$html_init .= "<TD>$total_count</TD>";
-$html_init .= "<TD>100.00</TD>";
-$html_init .= "<TD>100.00</TD>";
-$html_init .= "<TD>$total_percentage</TD>";
-$html_init .= "<TD>$total_percentage</TD>";
-$html_init .= "</TR></TABLE><BR>";
-$html_init .= $summary;
-$html_init .= "</TABLE><BR><H2>Details</H2>";
-
-my $count_query = 'SELECT count(*) FROM ( ('.
-   join( ') UNION (',
-          map { my $extra = $_->{extra_sql};  my $addl = $_->{addl_from};
-                "SELECT censustract from cust_pkg $addl $extra";
-              }
-          @sql_query
-       ). ') ) AS foo';
-
-my $link = 'cust_pkg.cgi?'.
-           join(';', map{ "$_=". $search_hash{$_} } keys %search_hash). ';';
-my $link_suffix = sub { my $row = shift;
-                        my $result = 'censustract='. $row->censustract. ';';
-                        $result .= 'report_option='. @row_option[$row->upload - 1]
-                          if @row_option[$row->upload - 1];
-                        $result .= 'report_option='. @column_option[$row->download - 1]
-                          if @column_option[$row->download - 1];
-                        $result;
-                      };
 </%init>
+<%def .header_fbd_old>
+  <TR CLASS="head">
+    <TD ROWSPAN=2>Census Block</TD>
+    <TD ROWSPAN=2>DBA Name</TD>
+    <TD ROWSPAN=2>Technology</TD>
+    <TD ROWSPAN=2>Consumer?</TD>
+    <TD COLSPAN=2>Advertised Speed (Mbps)</TD>
+    <TD ROWSPAN=2>Business?</TD>
+    <TD COLSPAN=2>Contractual Speed (Mbps)</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Down</TD>
+    <TD>Up</TD>
+    <TD>Down</TD>
+    <TD>Up</TD>
+  </TR>
+</%def>
+<%def .header_fbd>
+  <TR CLASS="head">
+    <TD ROWSPAN=2>Census Block</TD>
+    <TD ROWSPAN=2>DBA Name</TD>
+    <TD ROWSPAN=2>Technology</TD>
+    <TD ROWSPAN=2>Consumer?</TD>
+    <TD COLSPAN=2>Advertised Speed (Mbps)</TD>
+    <TD ROWSPAN=2>Business?</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Down</TD>
+    <TD>Up</TD>
+  </TR>
+</%def>
+<%def .header_fbs>
+  <TR CLASS="head">
+    <TD ROWSPAN=2>Census Tract</TD>
+    <TD ROWSPAN=2>Technology</TD>
+    <TD COLSPAN=2>Speed (Mbps)</TD>
+    <TD COLSPAN=2>Subscriptions</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Down</TD>
+    <TD>Up</TD>
+    <TD>Total</TD>
+    <TD>Consumer</TD>
+  </TR>
+</%def>
+<%def .header_fvs>
+  <TR CLASS="head">
+    <TD ROWSPAN=2>Census Tract</TD>
+    <TD ROWSPAN=2>VoIP?</TD>
+    <TD COLSPAN=2>Lines/Subscriptions</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Total</TD>
+    <TD>Consumer</TD>
+  </TR>
+</%def>
+<%def .header_lts>
+  <TR CLASS="head">
+    <TD ROWSPAN=3>State</TD>
+    <TD COLSPAN=2>Wholesale</TD>
+    <TD COLSPAN=12>End User Lines</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD ROWSPAN=2>VGEs</TD>
+    <TD ROWSPAN=2>UNE-Ls</TD>
+
+    <TD ROWSPAN=2>Total</TD>
+    <TD ROWSPAN=2>With Broadband</TD>
+    <TD COLSPAN=2>Consumer</TD>
+    <TD COLSPAN=2>Business</TD>
+
+    <TD COLSPAN=3>Local Loop</TD>
+
+    <TD COLSPAN=3>Special Media</TD>
+  </TR>
+
+  <TR CLASS="subhead">
+    <TD> </TD>
+    <TD>+LD</TD>
+    <TD> </TD>
+    <TD>+LD</TD>
+
+    <TD>Owned</TD>
+    <TD>UNE-L</TD>
+    <TD>Resale</TD>
+
+    <TD>Fiber</TD>
+    <TD>Coaxial</TD>
+    <TD>Wireless</TD>
+  </TR>
+</%def>
+<%def .header_voip>
+  <TR CLASS="head">
+    <TD ROWSPAN=3>State</TD>
+    <TD COLSPAN=2>VoIP OTT</TD>
+    <TD COLSPAN=8>VoIP Non-OTT</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD ROWSPAN=2>Total</TD>
+    <TD ROWSPAN=2>Consumer</TD>
+
+    <TD ROWSPAN=2>Total</TD>
+    <TD ROWSPAN=2>Consumer</TD>
+    <TD ROWSPAN=2>Bundled</TD>
+    <TD COLSPAN=5>Media Type</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Copper</TD>
+    <TD>Fiber</TD>
+    <TD>Coaxial</TD>
+    <TD>Wireless</TD>
+    <TD>Other</TD>
+  </TR>
+</%def>
+<%def .header_mbs>
+%# unimplemented
+  <TR CLASS="head">
+    <TD ROWSPAN=2>State</TD>
+    <TD COLSPAN=2>Speed (Mbps)</TD>
+    <TD COLSPAN=2>Subscriptions</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Down</TD>
+    <TD>Up</TD>
+    <TD>Total</TD>
+    <TD>Consumer</TD>
+  </TR>
+</%def>
+<%def .header_mvs>
+%# unimplemented
+  <TR CLASS="head">
+    <TD ROWSPAN=2>State</TD>
+    <TD COLSPAN=2>Subscriptions</TD>
+  </TR>
+  <TR CLASS="subhead">
+    <TD>Total</TD>
+    <TD>Direct</TD>
+  </TR>
+</%def>
+