Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Mon, 5 Oct 2015 21:43:05 +0000 (14:43 -0700)
committerIvan Kohler <ivan@freeside.biz>
Mon, 5 Oct 2015 21:43:05 +0000 (14:43 -0700)
25 files changed:
FS/FS/API.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/Template_Mixin.pm
FS/FS/cust_bill.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/deploy_zone.pm
FS/FS/deploy_zone_block.pm
FS/FS/o2m_Common.pm
FS/FS/part_pkg_fcc_option.pm
FS/bin/freeside-reexport
httemplate/browse/deploy_zone.html
httemplate/edit/deploy_zone-fixed.html
httemplate/edit/deploy_zone-mobile.html
httemplate/edit/process/deploy_zone-fixed.html
httemplate/edit/process/deploy_zone-mobile.html
httemplate/edit/process/elements/process.html
httemplate/elements/polygon.html [new file with mode: 0644]
httemplate/elements/select-terms.html
httemplate/elements/tr-polygon.html [new file with mode: 0644]
httemplate/misc/process/deploy_zone-block_lookup.cgi [new file with mode: 0644]
httemplate/misc/process/payment.cgi
httemplate/search/report_tax.cgi

index 7ee0802..9dbbc3c 100644 (file)
@@ -24,7 +24,7 @@ This module implements a backend API for advanced back-office integration.
 In contrast to the self-service API, which authenticates an end-user and offers
 functionality to that end user, the backend API performs a simple shared-secret
 authentication and offers full, administrator functionality, enabling
-integration with other back-office systems.  Only ccess this API from a secure 
+integration with other back-office systems.  Only access this API from a secure 
 network from other backoffice machines. DON'T use this API to create customer 
 portal functionality.
 
index 6332dd7..98b87ad 100644 (file)
@@ -1142,37 +1142,6 @@ sub do_process_payment {
 
   my $payby = delete $validate->{'payby'};
 
-  my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
-    'quiet'       => 1,
-    'manual'      => 1,
-    'selfservice' => 1,
-    'paynum_ref'  => \$paynum,
-    %$validate,
-  );
-  return { 'error' => $error } if $error;
-
-  #no error, so order the fee package if applicable...
-  my $conf = new FS::Conf;
-  my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
-  my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
-  
-  if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
-
-    my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
-
-    $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
-    return { 'error' => "payment processed successfully, but error ordering fee: $error" }
-      if $error;
-
-    #and generate an invoice for it now too
-    $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
-    return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" }
-      if $error;
-
-  }
-
-  $cust_main->apply_payments;
-
   if ( $validate->{'save'} ) {
     my $new = new FS::cust_main { $cust_main->hash };
     if ($payby eq 'CARD' || $payby eq 'DCRD') {
@@ -1193,7 +1162,7 @@ sub do_process_payment {
                     stateid stateid_state );
       $new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' );
     }
-    $new->set( 'payinfo' => $cust_main->card_token || $validate->{'payinfo'} );
+    $new->payinfo( $validate->{'payinfo'} ); #to properly set paymask
     $new->set( 'paydate' => $validate->{'paydate'} );
     my $error = $new->replace($cust_main);
     if ( $error ) {
@@ -1201,18 +1170,48 @@ sub do_process_payment {
       #return { 'error' => $error };
       #XXX just warn verosely for now so i can figure out how these happen in
       # the first place, eventually should redirect them to the "change
-      #address" page but indicate the payment did process??
+      #address" page but indicate if the payment processed?
       delete($validate->{'payinfo'}); #don't want to log this!
       warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
            "NEW: ". Dumper($new)."\n".
            "OLD: ". Dumper($cust_main)."\n".
            "PACKET: ". Dumper($validate)."\n";
-    #} else {
-      #not needed...
-      #$cust_main = $new;
+    } else {
+      $cust_main = $new;
     }
   }
 
+  my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
+    'quiet'       => 1,
+    'manual'      => 1,
+    'selfservice' => 1,
+    'paynum_ref'  => \$paynum,
+    %$validate,
+  );
+  return { 'error' => $error } if $error;
+
+  #no error, so order the fee package if applicable...
+  my $conf = new FS::Conf;
+  my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
+  my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
+  
+  if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
+
+    my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
+
+    $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
+    return { 'error' => "payment processed successfully, but error ordering fee: $error" }
+      if $error;
+
+    #and generate an invoice for it now too
+    $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+    return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" }
+      if $error;
+
+  }
+
+  $cust_main->apply_payments;
+
   my $cust_pay = '';
   my $receipt_html = '';
   if ($paynum) {
index 26dbbcd..1e0d999 100644 (file)
@@ -1537,7 +1537,7 @@ and customer address. Include units.',
     'type'        => 'select',
     'per_agent'   => 1,
     'select_enum' => [ 
-      '', 'Payable upon receipt', 'Net 0', 'Net 3', 'Net 5', 'Net 9', 'Net 10', 'Net 14', 
+      '', 'Payable upon receipt', 'Net 0', 'Net 3', 'Net 5', 'Net 7', 'Net 9', 'Net 10', 'Net 14', 
       'Net 15', 'Net 18', 'Net 20', 'Net 21', 'Net 25', 'Net 30', 'Net 45', 
       'Net 60', 'Net 90'
     ], },
@@ -4178,7 +4178,7 @@ and customer address. Include units.',
   {
     'key'         => 'disable_previous_balance',
     'section'     => 'invoicing',
-    'description' => 'Disable inclusion of previous balance, payment, and credit lines on invoices.',
+    'description' => 'Show new charges only; do not list previous invoices, payments, or credits on the invoice.',
     'type'        => 'checkbox',
     'per_agent'   => 1,
   },
index 85fbbeb..486860f 100644 (file)
@@ -7038,6 +7038,7 @@ sub tables_hashref {
         'zonenum',        'serial',  '',     '',      '', '',
         'description',    'char',    'NULL', $char_d, '', '',
         'agentnum',       'int',     '',     '',      '', '',
+        'censusyear',     'char',    'NULL', 4,      '', '',
         'dbaname',        'char',    'NULL', $char_d, '', '',
         'zonetype',       'char',    '',     1,       '', '',
         'technology',     'int',     '',     '',      '', '',
@@ -7069,7 +7070,7 @@ sub tables_hashref {
         'blocknum',       'serial',  '',     '',      '', '',
         'zonenum',        'int',     '',     '',      '', '',
         'censusblock',    'char',    '',     15,      '', '',
-        'censusyear',     'char',    '',      4,      '', '',
+        'censusyear',     'char','NULL',      4,      '', '',
       ],
       'primary_key' => 'blocknum',
       'unique' => [],
index 206c03c..1a3217c 100644 (file)
@@ -684,7 +684,12 @@ sub print_generic {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
   #my $balance_due = $self->owed + $pr_total - $cr_total;
-  my $balance_due = $self->owed + $pr_total;
+  my $balance_due = $self->owed;
+  if ( $self->enable_previous ) {
+    $balance_due += $pr_total;
+  }
+  # otherwise the previous balance is not shown, so including it in the
+  # balance due is just confusing
 
   # the sum of amount owed on all invoices
   # (this is used in the summary & on the payment coupon)
index 09424ba..6546bfa 100644 (file)
@@ -2836,8 +2836,7 @@ sub _items_total {
   my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
 
   if ( $conf->exists('previous_balance-exclude_from_total') ) {
-    # can we do some caching on this stuff? it's going to change infrequently
-    # in production
+    # if enabled, specifically add a line for the previous balance total
     $previous_charges_desc = $self->mt(
       $conf->config('previous_balance-text') || 'Previous Balance'
     );
@@ -2849,6 +2848,12 @@ sub _items_total {
           total_amount  => sprintf('%.2f',$pr_total)
         };
     }
+  }
+
+  if (   $conf->exists('previous_balance-exclude_from_total')
+      or !$self->enable_previous ) {
+    # show new charges only
+
     $new_charges_desc = $self->mt(
       $conf->config('previous_balance-text-total_new_charges')
        || 'Total New Charges'
@@ -2857,9 +2862,14 @@ sub _items_total {
     $new_charges_amount = $self->charged;
 
   } else {
+    # show new charges + previous invoice total
 
     $new_charges_desc = $self->mt('Total Charges');
-    $new_charges_amount = sprintf('%.2f',$self->charged + $pr_total);
+    if ( $self->enable_previous ) {
+      $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total);
+    } else {
+      $new_charges_amount = sprintf('%.2f', $self->charged);
+    }
 
   }
 
index 2d7b690..eee0958 100644 (file)
@@ -1024,8 +1024,14 @@ sub _make_lines {
         return "$@ running calc_setup for $cust_pkg\n"
           if $@;
 
-        $unitsetup = $cust_pkg->base_setup()
-                       || $setup; #XXX uuh
+        # Only increment unitsetup here if there IS a setup fee.
+        # prorate_defer_bill may cause calc_setup on a setup-stage package
+        # to return zero, and the setup fee to be charged later. (This happens
+        # when it's first billed on the prorate cutoff day. RT#31276.)
+        if ( $setup ) {
+          $unitsetup = $cust_pkg->base_setup()
+                         || $setup; #XXX uuh
+        }
 
         if ( $setup_param{'billed_currency'} ) {
           $setup_billed_currency = delete $setup_param{'billed_currency'};
@@ -1196,7 +1202,7 @@ sub _make_lines {
       # Add an additional setup fee at the billing stage.
       # Used for prorate_defer_bill.
       $setup += $param{'setup_fee'};
-      $unitsetup += $param{'setup_fee'};
+      $unitsetup = $cust_pkg->base_setup();
       $lineitems++;
     }
 
index c6b3b31..2a920e0 100644 (file)
@@ -765,8 +765,6 @@ sub realtime_bop {
 
   if ( $transaction->can('card_token') && $transaction->card_token ) {
 
-    $self->card_token($transaction->card_token);
-
     if ( $options{'payinfo'} eq $self->payinfo ) {
       $self->payinfo($transaction->card_token);
       my $error = $self->replace;
index 38dd7dc..71129cf 100644 (file)
@@ -6,6 +6,13 @@ use FS::Record qw( qsearch qsearchs dbh );
 use Storable qw(thaw);
 use MIME::Base64;
 
+use JSON qw(encode_json decode_json) ;
+use LWP::UserAgent;
+use HTTP::Request::Common;
+
+# update this in 2020, along with the URL for the TIGERweb service
+our $CENSUS_YEAR = 2010;
+
 =head1 NAME
 
 FS::deploy_zone - Object methods for deploy_zone records
@@ -48,6 +55,12 @@ Optional text describing the zone.
 
 The agent that serves this zone.
 
+=item censusyear
+
+The census map year for which this zone was last updated. May be null for
+zones that contain no census blocks (mobile zones, or fixed zones that haven't
+had their block lists filled in yet).
+
 =item dbaname
 
 The name under which service is marketed in this zone.  If null, will 
@@ -58,6 +71,8 @@ default to the agent name.
 The way the zone geography is defined: "B" for a list of census blocks
 (used by the FCC for fixed broadband service), "P" for a polygon (for 
 mobile services).  See L<FS::deploy_zone_block> and L<FS::deploy_zone_vertex>.
+Note that block-type zones are still allowed to have a vertex list, for
+use by the map editor.
 
 =item technology
 
@@ -147,12 +162,16 @@ sub delete {
   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(@_);
+  my $error;
+  foreach (qw(deploy_zone_block deploy_zone_vertex)) {
+    $error ||= $self->process_o2m(
+      'table'   => $_,
+      'num_col' => 'zonenum',
+      'fields'  => 'zonenum',
+      'params'  => {},
+    );
+  }
+  $error ||= $self->SUPER::delete(@_);
   
   if ($error) {
     dbh->rollback if $oldAutoCommit;
@@ -185,6 +204,7 @@ sub check {
     $self->ut_numbern('zonenum')
     || $self->ut_text('description')
     || $self->ut_number('agentnum')
+    || $self->ut_numbern('censusyear')
     || $self->ut_foreign_key('agentnum', 'agent', 'agentnum')
     || $self->ut_textn('dbaname')
     || $self->ut_enum('zonetype', [ 'B', 'P' ])
@@ -219,24 +239,6 @@ sub check {
   $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
@@ -244,8 +246,7 @@ 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.
+Returns the vertex records for this zone, in order by sequence number.
 
 =cut
 
@@ -267,7 +268,19 @@ sub deploy_zone_vertex {
   });
 }
 
-=back
+=item vertices_json
+
+Returns the vertex list for this zone, as a JSON string of
+
+[ [ latitude0, longitude0 ], [ latitude1, longitude1 ] ... ]
+
+=cut
+
+sub vertices_json {
+  my $self = shift;
+  my @vertices = map { [ $_->latitude, $_->longitude ] } $self->deploy_zone_vertex;
+  encode_json(\@vertices);
+}
 
 =head2 SUBROUTINES
 
@@ -315,7 +328,125 @@ sub process_batch_import {
   FS::Record::process_batch_import( $job, $opt, $param );
 
 }
-        
+
+=item process_block_lookup JOB, ZONENUM
+
+Look up all the census blocks in the zone's footprint, and insert them.
+This will replace any existing block list.
+
+=cut
+
+sub process_block_lookup {
+  my $job = shift;
+  my $param = shift;
+  if (!ref($param)) {
+    $param = thaw(decode_base64($param));
+  }
+  my $zonenum = $param->{zonenum};
+  my $zone = FS::deploy_zone->by_key($zonenum)
+    or die "zone $zonenum not found\n";
+
+  # wipe the existing list of blocks
+  my $error = $zone->process_o2m(
+    'table'   => 'deploy_zone_block',
+    'num_col' => 'zonenum', 
+    'fields'  => 'zonenum',
+    'params'  => {},
+  );
+  die $error if $error;
+
+  $job->update_statustext('0,querying census database') if $job;
+
+  # negotiate the rugged jungle trails of the ArcGIS REST protocol:
+  # 1. unlike most places, longitude first.
+  my @zone_vertices = map { [ $_->longitude, $_->latitude ] }
+    $zone->deploy_zone_vertex;
+
+  return if scalar(@zone_vertices) < 3; # then don't bother
+
+  # 2. package this as "rings", inside a JSON geometry object
+  # 3. announce loudly and frequently that we are using spatial reference 
+  #    4326, "true GPS coordinates"
+  my $geometry = encode_json({
+      'rings' => [ \@zone_vertices ],
+      'wkid'  => 4326,
+  });
+
+  my %query = (
+    f               => 'json', # duh
+    geometry        => $geometry,
+    geometryType    => 'esriGeometryPolygon', # as opposed to a bounding box
+    inSR            => 4326,
+    outSR           => 4326,
+    spatialRel      => 'esriSpatialRelIntersects', # the test to perform
+    outFields       => 'OID,GEOID',
+    returnGeometry  => 'false',
+    orderByFields   => 'OID',
+  );
+  my $url = 'http://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Tracts_Blocks/MapServer/12/query';
+  my $ua = LWP::UserAgent->new;
+
+  # first find out how many of these we're dealing with
+  my $response = $ua->request(
+    POST $url, Content => [
+      %query,
+      returnCountOnly => 1,
+    ]
+  );
+  die $response->status_line unless $response->is_success;
+  my $data = decode_json($response->content);
+  # their error messages are mostly useless, but don't just blindly continue
+  die $data->{error}{message} if $data->{error};
+
+  my $count = $data->{count};
+  my $inserted = 0;
+
+  #warn "Census block lookup: $count\n";
+
+  # we have to do our own pagination on this, because the census bureau
+  # doesn't support resultOffset (maybe they don't have ArcGIS 10.3 yet).
+  # that's why we're ordering by OID, it's globally unique
+  my $last_oid = 0;
+  my $done = 0;
+  while (!$done) {
+    $response = $ua->request(
+      POST $url, Content => [
+        %query,
+        where => "OID>$last_oid",
+      ]
+    );
+    die $response->status_line unless $response->is_success;
+    $data = decode_json($response->content);
+    die $data->{error}{message} if $data->{error};
+
+    foreach my $feature (@{ $data->{features} }) {
+      my $geoid = $feature->{attributes}{GEOID}; # the prize
+      my $block = FS::deploy_zone_block->new({
+          zonenum     => $zonenum,
+          censusblock => $geoid
+      });
+      $error = $block->insert;
+      die "$error (inserting census block $geoid)" if $error;
+
+      $inserted++;
+      if ($job and $inserted % 100 == 0) {
+        my $percent = sprintf('%.0f', $inserted / $count * 100);
+        $job->update_statustext("$percent,creating block records");
+      }
+    }
+
+    #warn "Inserted $inserted records\n";
+    $last_oid = $data->{features}[-1]{attributes}{OID};
+    $done = 1 unless $data->{exceededTransferLimit};
+  }
+
+  $zone->set('censusyear', $CENSUS_YEAR);  
+  $error = $zone->replace;
+  warn "$error (updating zone census year)" if $error; # whatever, continue
+
+  return;
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
index 757af7e..2ac18e2 100644 (file)
@@ -43,10 +43,6 @@ L<FS::deploy_zone> foreign key for the zone.
 
 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
@@ -107,7 +103,6 @@ sub check {
     $self->ut_numbern('blocknum')
     || $self->ut_number('zonenum')
     || $self->ut_number('censusblock')
-    || $self->ut_number('censusyear')
   ;
   return $error if $error;
 
index 4f6d2e7..430f00b 100644 (file)
@@ -35,11 +35,19 @@ Available options:
 
 table (required) - Table into which the records are inserted.
 
-num_col (optional) - Column in table which links to the primary key of the base table.  If not specified, it is assumed this has the same name.
-
-params (required) - Hashref of keys and values, often passed as C<scalar($cgi->Vars)> from a form.
-
-fields (required) - Arrayref of field names for each record in table.  Pulled from params as "pkeyNN_field" where pkey is table's primary key and NN is the entry's numeric identifier.
+fields (required) - Arrayref of the field names in the "many" table.
+
+params (required) - Hashref of keys and values, often passed as
+C<scalar($cgi->Vars)> from a form. This will be scanned for keys of the form
+"pkeyNN" (where pkey is the primary key column name, and NN is an integer).
+Each of these designates one record in the "many" table. The contents of
+that record will be taken from other parameters with the names
+"pkeyNN_myfield" (where myfield is one of the fields in the 'fields'
+array).
+
+num_col (optional) - Name of the foreign key column in the "many" table, which
+links to the primary key of the base table. If not specified, it is assumed
+this has the same name as in the base table.
 
 =cut
 
index 5c78e5f..3d821f5 100644 (file)
@@ -148,7 +148,7 @@ tie our %spectrum_labels, 'Tie::IxHash', (
   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',
+  98 => 'Unlicensed (including broadcast television "white spaces") Bands',
   99 => '600 MHz',
   100 => 'H Block',
   101 => 'Advanced Wireless Services (AWS) 3 Band',
index 54af9dd..6b68917 100644 (file)
@@ -1,7 +1,7 @@
 #!/usr/bin/perl -w
 
 use strict;
-use vars qw($opt_s $opt_u $opt_p);
+use vars qw($opt_s $opt_u $opt_p $opt_e);
 use Getopt::Std;
 use FS::UID qw(adminsuidsetup);
 use FS::Record qw(qsearch qsearchs);
@@ -22,7 +22,7 @@ if ( $export_x =~ /^(\d+)$/ ) {
     or die "no exports of type $export_x found\n";
 }
 
-getopts('s:u:p:');
+getopts('s:u:p:e:');
 
 my @svc_x = ();
 if ( $opt_s ) {
@@ -38,16 +38,20 @@ if ( $opt_s ) {
   die "no services with svcpart $opt_p found\n" unless @svc_x;
 }
 
+$opt_e ||= 'insert';
+die &usage unless grep { $_ eq $opt_e } qw( insert replace delete suspend unsuspend );
+my $method = 'export_' . $opt_e;
+
 foreach my $part_export ( @part_export ) {
   foreach my $svc_x ( @svc_x ) {
-    my $error = $part_export->export_insert($svc_x);
+    my $error = $part_export->$method($svc_x,$svc_x);
     die $error if $error;
   }
 }
 
 
 sub usage {
-  die "Usage:\n\n  freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]\n";
+  return "Usage:\n\n  freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] [ -e insert|replace|delete|suspend|unsuspend ]\n";
 }
 
 =head1 NAME
@@ -56,12 +60,13 @@ freeside-reexport - Command line tool to re-trigger export jobs for existing ser
 
 =head1 SYNOPSIS
 
-  freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ]
+  freeside-reexport user exportnum|exporttype [ -s svcnum | -u username | -p svcpart ] [ -e insert|replace|delete|suspend|unsuspend ]
 
 =head1 DESCRIPTION
 
   Re-queues the export job for the specified exportnum or exporttype(s) and
-  specified service (selected by svcnum or username).
+  specified service (selected by svcnum, username or svcpart).  Optionally 
+  specify the phase of export using the -e flag (default is insert.)
 
 =head1 SEE ALSO
 
index 3bd9d07..02ebb8b 100644 (file)
@@ -17,6 +17,7 @@
                         'Market',
                         'Advertised Mbps',
                         'Contractual Mbps',
+                        'Vertices',
                         'Census blocks',
                      ],
   fields          => [  'zonenum',
@@ -42,6 +43,9 @@
                               )
                             },
                         sub { my $self = shift;
+                              FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum)
+                            },
+                        sub { my $self = shift;
                               FS::deploy_zone_block->count('zonenum = '.$self->zonenum)
                             },
                      ],
@@ -53,7 +57,7 @@
                        '(cir_speed_down, cir_speed_up)',
                      ],
   links           => [  $link_fixed, $link_fixed, ],
-  align           => 'clllllr',
+  align           => 'cllllrr',
   nohtmlheader    => 1,
   disable_maxselect => 1,
   disable_total     => 1,
index 90d1b66..b8d9f8b 100644 (file)
           value => 'Contractually guaranteed speed (Mbps)' },
         'cir_speed_down',
         'cir_speed_up',
-
-        { type => 'tablebreak-tr-title', value => 'Census blocks'},
-        { field => 'file',
-          type  => 'file-upload',
-        },
-        { field => 'format',
-          type  => 'hidden',
-          value => 'plain',
-        },
-        { field => 'censusyear',
-          type  => 'select',
-          options => [ '', qw( 2013 2012 2011 ) ],
-        },
-
-        { type => 'tablebreak-tr-title', value => '', },
-        { field => 'blocknum',
-          type              => 'deploy_zone_block',
-          o2m_table         => 'deploy_zone_block',
-          m2_label          => ' ',
-          m2_error_callback => $m2_error_callback,
-        },
+        { type => 'tablebreak-tr-title', value => 'Footprint'},
+        { field               => 'vertices',
+          type                => 'polygon',
+          curr_value_callback => sub {
+            my ($cgi, $object) = @_;
+            $cgi->param('vertices') || $object->vertices_json;
+          },
+        }
+#
+#        { type => 'tablebreak-tr-title', value => 'Census blocks'},
+#        { field => 'file',
+#          type  => 'file-upload',
+#        },
+#        { field => 'format',
+#          type  => 'hidden',
+#          value => 'plain',
+#        },
+#        { field => 'censusyear',
+#          type  => 'hidden',
+#          options => [ '', qw( 2013 2012 2011 ) ],
+#        },
+#
+#        { type => 'tablebreak-tr-title', value => '', },
+#        { 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;
@@ -90,22 +97,22 @@ 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;
-};
+#my $m2_error_callback = sub {
+#  my ($cgi, $deploy_zone) = @_;
+#  my @blocknums = grep {
+#    /^blocknum\d+/ and length($cgi->param($_.'_censusblock'))
+#  } $cgi->param;
+#
+#  sort { $a->censusblock <=> $b->censusblock }
+#  map {
+#    my $k = $_;
+#    FS::deploy_zone_block->new({
+#      blocknum    => scalar($cgi->param($k)),
+#      zonenum     => $deploy_zone->zonenum,
+#      censusblock => scalar($cgi->param($k.'_censusblock')),
+#      censusyear  => scalar($cgi->param($k.'_censusyear')),
+#    })
+#  } @blocknums;
+#};
 
 </%init>
index d049cb0..8cec298 100644 (file)
         '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,
-        },
-    ],
+        { field               => 'vertices',
+          type                => 'polygon',
+          curr_value_callback => sub {
+            my ($cgi, $object) = @_;
+            $cgi->param('vertices') || $object->vertices_json;
+          },
+        }
 
+#        { 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;
index eae3a74..0033bbe 100644 (file)
@@ -3,12 +3,31 @@
     error_redirect => popurl(2).'deploy_zone-fixed.html',
     table       => 'deploy_zone',
     viewall_dir => 'browse',
-    process_o2m => {
-      'table'  => 'deploy_zone_block',
-      'fields' => [qw( censusblock censusyear )]
-    },
-    process_upload => {
-      'process' => 'misc/process/deploy_zone-import.html',
-      'fields' => [qw( censusyear format )],
-    },
+    precheck_callback => $precheck_callback,
+    process_o2m =>
+      { 'table'  => 'deploy_zone_vertex',
+                     'fields' => [qw( latitude longitude )]
+      },
+    progress_init => [
+      'PostForm',
+      [ 'zonenum' ],
+      $fsurl.'misc/process/deploy_zone-block_lookup.cgi',
+      $fsurl.'browse/deploy_zone.html',
+    ],
 &>
+<%init>
+my $precheck_callback = sub {
+  # convert the vertex list into a process_o2m-style parameter list
+  if ( $cgi->param('vertices') ) {
+    my $vertices = decode_json($cgi->param('vertices'));
+    my $i = 0;
+    foreach (@$vertices) {
+      $cgi->param("vertexnum${i}", '');
+      $cgi->param("vertexnum${i}_latitude", $_->[0]);
+      $cgi->param("vertexnum${i}_longitude", $_->[1]);
+      $i++;
+    }
+  }
+  '';
+};
+</%init>
index 7b8f911..d36d5d4 100644 (file)
@@ -2,8 +2,25 @@
     error_redirect => popurl(2).'deploy_zone-mobile.html',
     table       => 'deploy_zone',
     viewall_dir => 'browse',
-    process_o2m => 
+    precheck_callback => $precheck_callback,
+    process_o2m =>
       { 'table'  => 'deploy_zone_vertex',
                      'fields' => [qw( latitude longitude )]
       },
 &>
+<%init>
+my $precheck_callback = sub {
+  # convert the vertex list into a process_o2m-style parameter list
+  if ( $cgi->param('vertices') ) {
+    my $vertices = decode_json($cgi->param('vertices'));
+    my $i = 0;
+    foreach (@$vertices) {
+      $cgi->param("vertexnum${i}", '');
+      $cgi->param("vertexnum${i}_latitude", $_->[0]);
+      $cgi->param("vertexnum${i}_longitude", $_->[1]);
+      $i++;
+    }
+  }
+  '';
+};
+</%init>
index 69bd605..a76f4be 100644 (file)
@@ -160,7 +160,26 @@ process();
 </script>
 <& /elements/footer.html &>
 
-%} elsif ( $opt{'popup_reload'} ) {
+% } elsif ( $opt{'progress_init'} ) {
+%   # some false laziness with the above
+%   my ($form_name, $job_fields) = @{ $opt{'progress_init'} };
+<form name="<% $form_name %>">
+%   foreach my $field (@$job_fields) {
+  <input type="hidden" name="<% $field %>" value="<% $cgi->param($field) |h %>">
+%   }
+<& /elements/progress-init.html,
+  @{ $opt{'progress_init'} }
+&>
+<input type="submit" style="display:none">
+</form>
+<script>
+<&| /elements/onload.js &>
+process();
+</&>
+</script>
+<& /elements/footer.html &>
+
+% } elsif ( $opt{'popup_reload'} ) {
 
   <% include('/elements/header-popup.html', $opt{'popup_reload'} ) %>
 
diff --git a/httemplate/elements/polygon.html b/httemplate/elements/polygon.html
new file mode 100644 (file)
index 0000000..c26e985
--- /dev/null
@@ -0,0 +1,127 @@
+<%init>
+my %opt = @_;
+my $field = $opt{'field'};
+my $id = $opt{'id'} || $opt{'field'};
+my $div_id = "div_$id";
+
+my $vertices_json = $opt{'curr_value'} || '[]';
+</%init>
+<& hidden.html, %opt &>
+<div id="<% $div_id %>" style="height: 600px; width: 600px"></div>
+
+<script src="https://maps.googleapis.com/maps/api/js?libraries=drawing"></script>
+<script>
+var map;
+var drawingManager;
+
+function updateFormInput(event) {
+  var path = window.polygon.getPath();
+  var vertices = []; // array of arrays, geoJSON style
+  for (var i =0; i < path.getLength(); i++) {
+    var xy = path.getAt(i);
+    vertices[i] = [ xy.lat(), xy.lng() ];
+  }
+  console.log(vertices); //XXX
+  $('#<% $field %>').prop('value', JSON.stringify(vertices));
+}
+
+$(function() {
+  mapOptions = {
+    zoom: 4,
+    center: {lat: 39.40114, lng: -96.57127}, // continental U.S.
+    mapTypeId: google.maps.MapTypeId.ROADMAP,
+    panControl: true,
+    scaleControl: true,
+    streetViewControl: false,
+  };
+  map = new google.maps.Map($('#<% $div_id %>')[0], mapOptions);
+
+  var polygonComplete = function(p) {
+    window.polygon = p;
+    if (drawingManager) {
+      drawingManager.setDrawingMode(null);
+      drawingManager.setOptions({ drawingControl: false });
+    }
+    // double click to delete a vertex (so long as it remains a polygon)
+    p.addListener('dblclick', function (mev) {
+      if (mev.vertex != null && window.polygon.getPath().length > 3) {
+        p.getPath().removeAt(mev.vertex);
+      }
+    });
+    // any time the polygon is modified, update the vertex list
+    p.getPath().addListener('set_at', updateFormInput);
+    p.getPath().addListener('insert_at', updateFormInput);
+    p.getPath().addListener('remove_at', updateFormInput);
+
+    // and also now
+    updateFormInput();
+  };
+
+  var polygonOptions = {
+    fillColor: '#0000a0',
+    fillOpacity: 0.2,
+    strokeColor: '#0000a0',
+    strokeWeight: 2,
+    clickable: false,
+    editable: true,
+    zIndex: 1,
+    map: map,
+  };
+
+  var vertex_array = <% $vertices_json %>;
+  if ( vertex_array.length > 2 ) {
+    // then we already have a polygon. make it acceptable to google maps,
+    // and also create a bounding box for it and fit the map to that.
+
+    var path = [];
+    var bounds = new google.maps.LatLngBounds();
+    for (var i = 0; i < vertex_array.length; i++) {
+      var xy = new google.maps.LatLng(vertex_array[i][0], vertex_array[i][1]);
+      path.push(xy);
+      bounds.extend(xy);
+    }
+
+    polygonOptions.paths = [ path ];
+    polygonComplete(new google.maps.Polygon(polygonOptions));
+    map.fitBounds(bounds);
+
+  } else {
+    // there are no vertices, or not enough to make a polygon, so 
+    // enable drawing mode to create a new one
+
+    drawingManager = new google.maps.drawing.DrawingManager({
+      drawingMode: google.maps.drawing.OverlayType.POLYGON,
+      drawingControl: true,
+      drawingControlOptions: {
+        position: google.maps.ControlPosition.TOP_CENTER,
+        drawingModes: [
+          google.maps.drawing.OverlayType.POLYGON,
+        ]
+      },
+      polygonOptions: polygonOptions,
+    });
+
+    // after a single polygon is drawn: remember it, add a listener to let
+    // nodes be deleted, and exit drawing mode
+    drawingManager.addListener('polygoncomplete', polygonComplete);
+    drawingManager.setMap(map);
+
+    // center the map on the user (for lack of a better choice)
+    if (navigator.geolocation) {
+      navigator.geolocation.getCurrentPosition(function(position) {
+        var pos = {
+          lat: position.coords.latitude,
+          lng: position.coords.longitude
+        };
+
+        map.setCenter(pos);
+        map.setZoom(12);
+      });
+    } // on error, or if geolocation isn't available, do nothing
+  }
+
+});
+
+    </script>
+  </body>
+</html>
index 716832f..a330df1 100644 (file)
@@ -36,7 +36,7 @@ my $empty_value = $opt{'empty_value'} || '';
 
 my @terms = ( emt('Payable upon receipt'),
               ( map "Net $_",
-                0, 3, 5, 9, 10, 14, 15, 18, 20, 21, 25, 30, 45, 60, 90 ),
+                0, 3, 5, 7, 9, 10, 14, 15, 18, 20, 21, 25, 30, 45, 60, 90 ),
             );
 
 my @pre_options = $opt{pre_options} ? @{ $opt{pre_options} } : ();
diff --git a/httemplate/elements/tr-polygon.html b/httemplate/elements/tr-polygon.html
new file mode 100644 (file)
index 0000000..6990d3d
--- /dev/null
@@ -0,0 +1,5 @@
+<tr>
+<td colspan=2>
+<& polygon.html, @_ &>
+</td>
+</tr>
diff --git a/httemplate/misc/process/deploy_zone-block_lookup.cgi b/httemplate/misc/process/deploy_zone-block_lookup.cgi
new file mode 100644 (file)
index 0000000..8f4eac7
--- /dev/null
@@ -0,0 +1,13 @@
+<% $server->process %>
+<%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 $server = FS::UI::Web::JSRPC->new(
+  'FS::deploy_zone::process_block_lookup', $cgi
+);
+</%init>
index 27b8186..d9299e5 100644 (file)
@@ -135,6 +135,45 @@ $cgi->param('discount_term') =~ /^(\d*)$/
   or errorpage("illegal discount_term");
 my $discount_term = $1;
 
+# save first, for proper tokenization later
+if ( $cgi->param('save') ) {
+  my $new = new FS::cust_main { $cust_main->hash };
+  if ( $payby eq 'CARD' ) { 
+    $new->set( 'payby' => ( $cgi->param('auto') ? 'CARD' : 'DCRD' ) );
+  } elsif ( $payby eq 'CHEK' ) {
+    $new->set( 'payby' => ( $cgi->param('auto') ? 'CHEK' : 'DCHK' ) );
+  } else {
+    die "unknown payby $payby";
+  }
+  $new->payinfo($payinfo); #to properly set paymask
+  $new->set( 'paydate' => "$year-$month-01" );
+  $new->set( 'payname' => $payname );
+
+  #false laziness w/FS:;cust_main::realtime_bop - check both to make sure
+  # working correctly
+  if ( $payby eq 'CARD' &&
+       grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) {
+    $new->set( 'paycvv' => $paycvv );
+  } else {
+    $new->set( 'paycvv' => '');
+  }
+
+  if ( $payby eq 'CARD' ) {
+    my $bill_location = FS::cust_location->new;
+    $bill_location->set( $_ => $cgi->param($_) )
+      foreach @{$payby2fields{$payby}};
+    $new->set('bill_location' => $bill_location);
+    # will do nothing if the fields are all unchanged
+  } else {
+    $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
+  }
+
+  my $error = $new->replace($cust_main);
+  errorpage("error saving info, payment not processed: $error")
+    if $error;
+  $cust_main = $new;
+}
+
 my $error = '';
 my $paynum = '';
 if ( $cgi->param('batch') ) {
@@ -190,44 +229,6 @@ if ( $cgi->param('batch') ) {
 
 }
 
-if ( $cgi->param('save') ) {
-  my $new = new FS::cust_main { $cust_main->hash };
-  if ( $payby eq 'CARD' ) { 
-    $new->set( 'payby' => ( $cgi->param('auto') ? 'CARD' : 'DCRD' ) );
-  } elsif ( $payby eq 'CHEK' ) {
-    $new->set( 'payby' => ( $cgi->param('auto') ? 'CHEK' : 'DCHK' ) );
-  } else {
-    die "unknown payby $payby";
-  }
-  $new->set( 'payinfo' => $cust_main->card_token || $payinfo );
-  $new->set( 'paydate' => "$year-$month-01" );
-  $new->set( 'payname' => $payname );
-
-  #false laziness w/FS:;cust_main::realtime_bop - check both to make sure
-  # working correctly
-  if ( $payby eq 'CARD' &&
-       grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) {
-    $new->set( 'paycvv' => $paycvv );
-  } else {
-    $new->set( 'paycvv' => '');
-  }
-
-  if ( $payby eq 'CARD' ) {
-    my $bill_location = FS::cust_location->new;
-    $bill_location->set( $_ => $cgi->param($_) )
-      foreach @{$payby2fields{$payby}};
-    $new->set('bill_location' => $bill_location);
-    # will do nothing if the fields are all unchanged
-  } else {
-    $new->set( $_ => $cgi->param($_) ) foreach @{$payby2fields{$payby}};
-  }
-
-  my $error = $new->replace($cust_main);
-  errorpage("payment processed successfully, but error saving info: $error")
-    if $error;
-  $cust_main = $new;
-}
-
 #success!
 
 </%init>
index 6d0e95d..0ad143f 100644 (file)
@@ -77,7 +77,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
 %   # cust_bill_pkg.cgi wants a list of specific taxnums (and package class)
 %   # cust_credit_bill_pkg.html wants a geographic scope (and package class)
 %   my $rowlink = ';taxnum=' . $row->{taxnums};
-%   my $rowregion = '';
+%   my $rowregion = ';country=' . $cgi->param('country');
 %   foreach my $loc (qw(state county city district)) {
 %     if ( $row->{$loc} ) {
 %       $rowregion .= ";$loc=" . uri_escape($row->{$loc});
@@ -88,6 +88,7 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
 %     $rowlink .= ';classnum=' . ($row->{pkgclass} || 0);
 %     $rowregion .= ';classnum=' . ($row->{pkgclass} || 0);
 %   }
+%warn $rowregion;
 %
 %   if ( $row->{total} ) {
   </TBODY><TBODY CLASS="total">
@@ -125,14 +126,14 @@ TD.rowhead { font-weight: bold; text-align: left; padding: 0px 3px }
         <% $money_sprintf->( $row->{sales_credited} ) %>
       </A>
     </TD>
-    <TD CLASS="bigmath"> &times; </TD>
-    <TD><% $row->{rate} %></TD>
 %   # taxable sales
     <TD>
       <A HREF="<% $saleslink . $rowlink . ";taxable=1" %>">
         <% $money_sprintf->( $row->{taxable} ) %>
       </A>
     </TD>
+    <TD CLASS="bigmath"> &times; </TD>
+    <TD><% $row->{rate} %></TD>
 %   # estimated tax
     <TD CLASS="bigmath"> = </TD>
     <TD>