improved address standardization, #13763
authorMark Wells <mark@freeside.biz>
Tue, 6 Nov 2012 20:48:41 +0000 (12:48 -0800)
committerMark Wells <mark@freeside.biz>
Tue, 6 Nov 2012 20:49:11 +0000 (12:49 -0800)
26 files changed:
FS/FS/Conf.pm
FS/FS/GeocodeCache.pm [new file with mode: 0644]
FS/FS/Mason.pm
FS/FS/Misc/Geo.pm
FS/FS/Schema.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/MANIFEST
FS/t/GeocodeCache.t [new file with mode: 0644]
bin/generate-table-module
bin/usps-webtools-test-script [new file with mode: 0755]
httemplate/edit/cust_main.cgi
httemplate/edit/cust_main/bottomfixup.html
httemplate/edit/cust_main/bottomfixup.js
httemplate/edit/cust_main/contact.html
httemplate/elements/location.html
httemplate/elements/order_pkg.js
httemplate/elements/standardize_locations.html
httemplate/elements/standardize_locations.js
httemplate/elements/tr-select-cust_location.html
httemplate/misc/change_pkg.cgi
httemplate/misc/confirm-address_standardize.html [new file with mode: 0644]
httemplate/misc/confirm-censustract.html [new file with mode: 0644]
httemplate/misc/order_pkg.html
httemplate/misc/xmlhttp-address_standardize.html [new file with mode: 0644]
httemplate/misc/xmlhttp-cust_main-address_standardize.html [deleted file]

index e74c19f..02869b1 100644 (file)
@@ -4065,6 +4065,17 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'address_standardize_method',
+    'section'     => 'UI', #???
+    'description' => 'Method for standardizing customer addresses.',
+    'type'        => 'select',
+    'select_hash' => [ '' => '', 
+                       'usps' => 'U.S. Postal Service',
+                       'ezlocate' => 'EZLocate',
+                     ],
+  },
+
+  {
     'key'         => 'usps_webtools-userid',
     'section'     => 'UI',
     'description' => 'Production UserID for USPS web tools.   Enables USPS address standardization.  See the <a href="http://www.usps.com/webtools/">USPS website</a>, register and agree not to use the tools for batch purposes.',
@@ -4079,6 +4090,20 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'ezlocate-userid',
+    'section'     => 'UI',
+    'description' => 'User ID for EZ-Locate service.  See <a href="http://www.geocode.com/">the TomTom website</a> for access and pricing information.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'ezlocate-password',
+    'section'     => 'UI',
+    'description' => 'Password for EZ-Locate service.',
+    'type'        => 'text'
+  },
+
+  {
     'key'         => 'cust_main-auto_standardize_address',
     'section'     => 'UI',
     'description' => 'When using USPS web tools, automatically standardize the address without asking.',
diff --git a/FS/FS/GeocodeCache.pm b/FS/FS/GeocodeCache.pm
new file mode 100644 (file)
index 0000000..7829c4d
--- /dev/null
@@ -0,0 +1,209 @@
+package FS::GeocodeCache;
+
+use strict;
+use vars qw($conf $DEBUG);
+use base qw( FS::geocode_Mixin );
+use FS::Record qw( qsearch qsearchs );
+use FS::Conf;
+use FS::Misc::Geo;
+
+use Data::Dumper;
+
+FS::UID->install_callback( sub { $conf = new FS::Conf; } );
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::GeocodeCache - An address undergoing the geocode process.
+
+=head1 SYNOPSIS
+
+  use FS::GeocodeCache;
+
+  $record = FS::GeocodeCache->standardize(%location_hash);
+
+=head1 DESCRIPTION
+
+An FS::GeocodeCache object represents a street address in the process of 
+being geocoded.  FS::GeocodeCache inherits from FS::geocode_Mixin.
+
+Most methods on this object throw an exception on error.
+
+FS::GeocodeCache has the following fields, with the same meaning as in 
+L<FS::cust_location>:
+
+=over 4
+
+=item address1
+
+=item address2
+
+=item city
+
+=item county
+
+=item state
+
+=item zip
+
+=item latitude
+
+=item longitude
+
+=item addr_clean
+
+=item country
+
+=item censustract
+
+=item geocode
+
+=item district
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new cache object.  For internal use.  See C<standardize>.
+
+=cut
+
+# minimalist constructor
+sub new {
+  my $class = shift;
+  my $self = {
+    company     => '',
+    address1    => '',
+    address2    => '',
+    city        => '',
+    state       => '',
+    zip         => '',
+    country     => '',
+    latitude    => '',
+    longitude   => '',
+    addr_clean  => '',
+    censustract => '',
+    @_
+  };
+  bless $self, $class;
+}
+
+# minimalist accessor, for compatibility with geocode_Mixin
+sub get {
+  $_[0]->{$_[1]}
+}
+
+sub set {
+  $_[0]->{$_[1]} = $_[2];
+}
+
+sub location_hash { %{$_[0]} };
+
+=item set_censustract
+
+Look up the censustract, if it's not already filled in, and return it.
+On error, sets 'error' and returns nothing.
+
+This uses the "get_censustract_*" methods in L<FS::Misc::Geo>; currently
+the only one is 'ffiec'.
+
+=cut
+
+sub set_censustract {
+  my $self = shift;
+
+  if ( $self->get('censustract') =~ /^\d{9}\.\d{2}$/ ) {
+    return $self->get('censustract');
+  }
+  my $censusyear = $conf->config('census_year');
+  return if !$censusyear;
+
+  my $method = 'ffiec';
+  # configurable censustract-only lookup goes here if it's ever needed.
+  $method = "get_censustract_$method";
+  my $censustract = eval { FS::Misc::Geo->$method($self, $censusyear) };
+  $self->set("censustract_error", $@);
+  $self->set("censustract", $censustract);
+}
+
+=item set_coord
+
+Set the latitude and longitude fields if they're not already set.  Returns
+those values, in order.
+
+=cut
+
+sub set_coord { # the one in geocode_Mixin will suffice
+  my $self = shift;
+  if ( !$self->get('latitude') || !$self->get('longitude') ) {
+    $self->SUPER::set_coord;
+    $self->set('coord_error', $@);
+  }
+  return $self->get('latitude'), $self->get('longitude');
+}
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item standardize LOCATION
+
+Given a location hash or L<FS::geocode_Mixin> object, standardize the 
+address using the configured method and return an L<FS::GeocodeCache> 
+object.
+
+The methods are the "standardize_*" functions in L<FS::Geo::Misc>.
+
+=cut
+
+sub standardize {
+  my $class = shift;
+  my $location = shift;
+  $location = { $location->location_hash }
+    if UNIVERSAL::can($location, 'location_hash');
+
+  local $Data::Dumper::Terse = 1;
+  warn "standardizing location:\n".Dumper($location) if $DEBUG;
+
+  my $method = $conf->config('address_standardize_method');
+
+  if ( $method ) {
+    $method = "standardize_$method";
+    my $new_location = eval { FS::Misc::Geo->$method( $location ) };
+    if ( $new_location ) {
+      $location = {
+        addr_clean => 'Y',
+        %$new_location
+        # standardize_* can return an address with addr_clean => '' if
+        # the address is somehow questionable
+      }
+    }
+    else {
+      # XXX need an option to decide what to do on error
+      $location->{'addr_clean'} = '';
+      $location->{'error'} = $@;
+    }
+    warn "result:\n".Dumper($location) if $DEBUG;
+  }
+  # else $location = $location
+  my $cache = $class->new(%$location);
+  return $cache;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index f7d98a1..944a483 100644 (file)
@@ -64,7 +64,7 @@ if ( -e $addl_handler_use_file ) {
   use DateTime;
   use DateTime::Format::Strptime;
   use FS::Misc::DateTime qw( parse_datetime );
-  use FS::Misc::Geo qw( get_censustract get_district );
+  use FS::Misc::Geo qw( get_district );
   use Lingua::EN::Inflect qw(PL);
   Lingua::EN::Inflect::classical names=>0; #Categorys
   use Tie::IxHash;
@@ -326,6 +326,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::cust_bill_pkg_discount_void;
   use FS::agent_pkg_class;
   use FS::svc_export_machine;
+  use FS::GeocodeCache;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 5d6f33c..6bc71fc 100644 (file)
@@ -2,7 +2,7 @@ package FS::Misc::Geo;
 
 use strict;
 use base qw( Exporter );
-use vars qw( $DEBUG @EXPORT_OK );
+use vars qw( $DEBUG @EXPORT_OK $conf );
 use LWP::UserAgent;
 use HTTP::Request;
 use HTTP::Request::Common qw( GET POST );
@@ -10,15 +10,19 @@ use HTML::TokeParser;
 use URI::Escape 3.31;
 use Data::Dumper;
 
+FS::UID->install_callback( sub {
+  $conf = new FS::Conf;
+} );
+
 $DEBUG = 0;
 
-@EXPORT_OK = qw( get_censustract get_district );
+@EXPORT_OK = qw( get_district );
 
 =head1 NAME
 
 FS::Misc::Geo - routines to fetch geographic information
 
-=head1 FUNCTIONS
+=head1 CLASS METHODS
 
 =over 4
 
@@ -30,7 +34,8 @@ codes) or an error message.
 
 =cut
 
-sub get_censustract {
+sub get_censustract_ffiec {
+  my $class = shift;
   my $location = shift;
   my $year  = shift;
 
@@ -45,7 +50,7 @@ sub get_censustract {
   my $res = $ua->request( GET( $url ) );
 
   warn $res->as_string
-    if $DEBUG > 1;
+    if $DEBUG > 2;
 
   unless ($res->code  eq '200') {
 
@@ -87,12 +92,12 @@ sub get_censustract {
         btnSearch   => 'Search',
       );
       warn join("\n", @ffiec_args )
-        if $DEBUG;
+        if $DEBUG > 1;
 
       push @{ $ua->requests_redirectable }, 'POST';
       $res = $ua->request( POST( $url, \@ffiec_args ) );
       warn $res->as_string
-        if $DEBUG > 1;
+        if $DEBUG > 2;
 
       unless ($res->code  eq '200') {
 
@@ -102,7 +107,7 @@ sub get_censustract {
 
         my @id = qw( MSACode StateCode CountyCode TractCode );
         $content = $res->content;
-        warn $res->content if $DEBUG > 1;
+        warn $res->content if $DEBUG > 2;
         $p = new HTML::TokeParser \$content;
         my $prefix = 'UcGeoResult11_lb';
         my $compare =
@@ -127,7 +132,7 @@ sub get_censustract {
 
   } #unless ($res->code  eq '200')
 
-  return "FFIEC Geocoding error: $error" if $error;
+  die "FFIEC Geocoding error: $error\n" if $error;
 
   $return->{'statecode'} .  $return->{'countycode'} .  $return->{'tractcode'};
 }
@@ -201,12 +206,12 @@ sub wa_sales {
   
   my $query_string = join($delim, @args );
   $url .= "?$query_string";
-  warn "\nrequest:  $url\n\n" if $DEBUG;
+  warn "\nrequest:  $url\n\n" if $DEBUG > 1;
 
   my $res = $ua->request( GET( "$url?$query_string" ) );
 
   warn $res->as_string
-  if $DEBUG > 1;
+  if $DEBUG > 2;
 
   if ($res->code ne '200') {
     $error = $res->message;
@@ -253,7 +258,7 @@ sub wa_sales {
     # just to make sure
     if ( $return->{'district'} =~ /^\d+$/ and $return->{'tax'} =~ /^.\d+$/ ) {
       $return->{'tax'} *= 100; #percentage
-      warn Dumper($return) if $DEBUG;
+      warn Dumper($return) if $DEBUG > 1;
       return $return;
     }
     else {
@@ -267,6 +272,131 @@ sub wa_sales {
   die "WA tax district lookup error: $error";
 }
 
+sub standardize_usps {
+  my $class = shift;
+
+  eval "use Business::US::USPS::WebTools::AddressStandardization";
+  die $@ if $@;
+
+  my $location = shift;
+  if ( $location->{country} ne 'US' ) {
+    # soft failure
+    warn "standardize_usps not for use in country ".$location->{country}."\n";
+    $location->{addr_clean} = '';
+    return $location;
+  }
+  my $userid   = $conf->config('usps_webtools-userid');
+  my $password = $conf->config('usps_webtools-password');
+  my $verifier = Business::US::USPS::WebTools::AddressStandardization->new( {
+      UserID => $userid,
+      Password => $password,
+      Testing => 0,
+  } ) or die "error starting USPS WebTools\n";
+
+  my($zip5, $zip4) = split('-',$location->{'zip'});
+
+  my %usps_args = (
+    FirmName => $location->{company},
+    Address2 => $location->{address1},
+    Address1 => $location->{address2},
+    City     => $location->{city},
+    State    => $location->{state},
+    Zip5     => $zip5,
+    Zip4     => $zip4,
+  );
+  warn join('', map "$_: $usps_args{$_}\n", keys %usps_args )
+    if $DEBUG > 1;
+
+  my $hash = $verifier->verify_address( %usps_args );
+
+  warn $verifier->response
+    if $DEBUG > 1;
+
+  die "USPS WebTools error: ".$verifier->{error}{description} ."\n"
+    if $verifier->is_error;
+
+  my $zip = $hash->{Zip5};
+  $zip .= '-' . $hash->{Zip4} if $hash->{Zip4} =~ /\d/;
+
+  { company   => $hash->{FirmName},
+    address1  => $hash->{Address2},
+    address2  => $hash->{Address1},
+    city      => $hash->{City},
+    state     => $hash->{State},
+    zip       => $zip,
+    country   => 'US',
+    addr_clean=> 'Y' }
+}
+
+my %ezlocate_error = ( # USA_Geo_002 documentation
+  10  => 'State not found',
+  11  => 'City not found',
+  12  => 'Invalid street address',
+  14  => 'Street name not found',
+  15  => 'Address range does not exist',
+  16  => 'Ambiguous address',
+  17  => 'Intersection not found', #unused?
+);
+
+sub standardize_ezlocate {
+  my $self = shift;
+  my $location = shift;
+  my $class;
+  #if ( $location->{country} eq 'US' ) {
+  #  $class = 'USA_Geo_004Tool';
+  #}
+  #elsif ( $location->{country} eq 'CA' ) {
+  #  $class = 'CAN_Geo_001Tool';
+  #}
+  #else { # shouldn't be a fatal error, just pass through unverified address
+  #  warn "standardize_teleatlas: address lookup in '".$location->{country}.
+  #       "' not available\n";
+  #  return $location;
+  #}
+  #my $path = $conf->config('teleatlas-path') || '';
+  #local @INC = (@INC, $path);
+  #eval "use $class;";
+  #if ( $@ ) {
+  #  die "Loading $class failed:\n$@".
+  #      "\nMake sure the TeleAtlas Perl SDK is installed correctly.\n";
+  #}
+
+  $class = 'Geo::EZLocate'; # use our own library
+  eval "use $class";
+  die $@ if $@;
+
+  my $userid = $conf->config('ezlocate-userid')
+    or die "no ezlocate-userid configured\n";
+  my $password = $conf->config('ezlocate-password')
+    or die "no ezlocate-password configured\n";
+  
+  my $tool = $class->new($userid, $password);
+  my $match = $tool->findAddress(
+    $location->{address1},
+    $location->{city},
+    $location->{state},
+    $location->{zip}, #12345-6789 format is allowed
+  );
+  warn "ezlocate returned match:\n".Dumper($match) if $DEBUG > 1;
+  # error handling - B codes indicate success
+  die $ezlocate_error{$match->{MAT_STAT}}."\n"
+    unless $match->{MAT_STAT} =~ /^B\d$/;
+
+  {
+    address1    => $match->{STD_ADDR},
+    address2    => $location->{address2},
+    city        => $match->{STD_CITY},
+    state       => $match->{STD_ST},
+    country     => $location->{country},
+    zip         => $match->{STD_ZIP}.'-'.$match->{STD_P4},
+    latitude    => $match->{MAT_LAT},
+    longitude   => $match->{MAT_LON},
+    censustract => $match->{FIPS_ST}.$match->{FIPS_CTY}.
+                   sprintf('%04.2f',$match->{CEN_TRCT}),
+    addr_clean  => 'Y',
+  };
+}
+
 =back
 
 =cut
index 912f3e2..9eb59a0 100644 (file)
@@ -1010,6 +1010,7 @@ sub tables_hashref {
         'latitude', 'decimal', 'NULL', '10,7', '', '', 
         'longitude','decimal', 'NULL', '10,7', '', '', 
         'coord_auto',  'char', 'NULL',  1, '', '',
+        'addr_clean',  'char', 'NULL',  1, '', '',
         'daytime',  'varchar', 'NULL', 20, '', '', 
         'night',    'varchar', 'NULL', 20, '', '', 
         'fax',      'varchar', 'NULL', 12, '', '', 
@@ -1028,6 +1029,7 @@ sub tables_hashref {
         'ship_latitude', 'decimal', 'NULL', '10,7', '', '', 
         'ship_longitude','decimal', 'NULL', '10,7', '', '', 
         'ship_coord_auto',  'char', 'NULL',  1, '', '',
+        'ship_addr_clean',  'char', 'NULL',  1, '', '',
         'ship_daytime',  'varchar', 'NULL', 20, '', '', 
         'ship_night',    'varchar', 'NULL', 20, '', '', 
         'ship_fax',      'varchar', 'NULL', 12, '', '', 
@@ -1252,6 +1254,7 @@ sub tables_hashref {
         'latitude',        'decimal', 'NULL',  '10,7', '', '', 
         'longitude',       'decimal', 'NULL',  '10,7', '', '', 
         'coord_auto',         'char', 'NULL',       1, '', '',
+        'addr_clean',         'char', 'NULL',       1, '', '',
         'country',            'char',     '',       2, '', '', 
         'geocode',         'varchar', 'NULL',      20, '', '',
         'district',        'varchar', 'NULL',      20, '', '',
index 2810dc9..1521960 100644 (file)
@@ -188,6 +188,7 @@ sub check {
     || $self->ut_coordn('latitude')
     || $self->ut_coordn('longitude')
     || $self->ut_enum('coord_auto', [ '', 'Y' ])
+    || $self->ut_enum('addr_clean', [ '', 'Y' ])
     || $self->ut_alphan('location_type')
     || $self->ut_textn('location_number')
     || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
@@ -208,9 +209,6 @@ sub check {
     return "Unit # is required";
   }
 
-  $self->set_coord
-    unless $import || ($self->latitude && $self->longitude);
-
   # tricky...we have to allow for the customer to not be inserted yet
   return "No prospect or customer!" unless $self->prospectnum 
                                         || $self->custnum
index 9e39b30..4ea4a6b 100644 (file)
@@ -1488,20 +1488,6 @@ sub replace {
     return "You are not permitted to create complimentary accounts.";
   }
 
-  # should be unnecessary--geocode will default to null on new locations
-  #if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode')
-  #     && $conf->exists('enable_taxproducts')
-  #   )
-  #{
-  #  my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip)
-  #              ? 'ship_' : '';
-  #  $self->set('geocode', '')
-  #    if $old->get($pre.'zip') ne $self->get($pre.'zip')
-  #    && length($self->get($pre.'zip')) >= 10;
-  #}
-
-  # set_coord/coord_auto stuff is now handled by cust_location
-
   local($ignore_expired_card) = 1
     if $old->payby  =~ /^(CARD|DCRD)$/
     && $self->payby =~ /^(CARD|DCRD)$/
@@ -1862,8 +1848,6 @@ sub check {
   
   }
 
-  #ship_ fields are gone
-
   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
   #  or return "Illegal payby: ". $self->payby;
   #$self->payby($1);
index f530610..9c444be 100644 (file)
@@ -673,3 +673,5 @@ FS/part_export_machine.pm
 t/part_export_machine.t
 FS/svc_export_machine.pm
 t/svc_export_machine.t
+FS/GeocodeCache.pm
+t/GeocodeCache.t
diff --git a/FS/t/GeocodeCache.t b/FS/t/GeocodeCache.t
new file mode 100644 (file)
index 0000000..eae6f0d
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::GeocodeCache;
+$loaded=1;
+print "ok 1\n";
index e7fc992..b536360 100755 (executable)
@@ -95,7 +95,7 @@ close TEST;
 # add them to MANIFEST
 ###
 
-system('cvs edit FS/MANIFEST');
+#system('cvs edit FS/MANIFEST');
 
 open(MANIFEST,">>FS/MANIFEST") or die $!;
 print MANIFEST "FS/$table.pm\n",
diff --git a/bin/usps-webtools-test-script b/bin/usps-webtools-test-script
new file mode 100755 (executable)
index 0000000..414ae4c
--- /dev/null
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+
+use FS::Misc::Geo 'standardize';
+use Data::Dumper; $Data::Dumper::Terse = 1;
+my @tests = (
+  {
+    address1  => '6406 Ivy Lane',
+    address2  => '',
+    city      => 'Greenbelt',
+    state     => 'MD',
+    zip       => '',
+  },
+  {
+    address1  => '8 Wildwood Drive',
+    address2  => '',
+    city      => 'Old Lyme',
+    state     => 'CT',
+    zip       => '06371',
+  },
+);
+
+my ($userid, $password) = @ARGV;
+
+my %opt = (
+  userid  => $userid,
+  password=> $password,
+  test    => 1,
+);
+my $i = 1;
+foreach (@tests) {
+  print "Test $i\n";
+  my $result = eval { standardize($_, %opt) };
+  print "ERROR: $@\n\n" if $@;
+  print Dumper($result);
+  $i++;
+}
+
+1;
index 2628b4e..0aded59 100755 (executable)
@@ -316,6 +316,8 @@ if ( $cgi->param('error') ) {
   $payinfo = '';
 
   $cgi->param('tagnum', FS::part_tag->default_tags);
+  $cust_main->coord_auto('Y');
+  $cust_main->ship_coord_auto('Y');
 
   if ( $cgi->param('qualnum') =~ /^(\d+)$/ ) {
     my $qualnum = $1;
index 60edcc1..b5d10c4 100644 (file)
@@ -1,15 +1,9 @@
 <& /elements/init_overlib.html &>
 
 <& /elements/xmlhttp.html,
-  url  => $p.'misc/xmlhttp-cust_main-address_standardize.html',
+  url  => $p.'misc/xmlhttp-address_standardize.html',
   subs => [ 'address_standardize' ],
-  #'method' => 'POST', #could get too long?
-&>
-
-<& /elements/xmlhttp.html,
-  url  => $p.'misc/xmlhttp-cust_main-censustract.html',
-  subs => [ 'censustract' ],
-  #'method' => 'POST', #could get too long?
+  method => 'POST', #could get too long?
 &>
 
 <INPUT TYPE="hidden" NAME="duplicate_of_custnum" VALUE="">
index 77d4294..4f3b7da 100644 (file)
@@ -7,8 +7,7 @@ my $company_longitude = $conf->config('company_longitude');
 
 my @fixups = ('copy_payby_fields', 'standardize_locations');
 
-push @fixups, 'fetch_censustract'
-    if $conf->exists('cust_main-require_censustract');
+push @fixups, 'confirm_censustract';
 
 push @fixups, 'check_unique'
     if $conf->exists('cust_main-check_unique') and !$opt{'custnum'};
@@ -18,15 +17,19 @@ push @fixups, 'do_submit'; # always last
 
 var fixups = <% encode_json(\@fixups) %>;
 var fixup_position;
+var running = false;
 
 %# state machine to deal with all the asynchronous stuff we're doing
 %# call this after each fixup on success:
 function submit_continue() {
-  window[ fixups[fixup_position++] ].call();
+  if ( running ) {
+    window[ fixups[fixup_position++] ].call();
+  }
 }
 
 %# or on failure:
 function submit_abort() {
+  running = false;
   fixup_position = 0;
   document.CustomerForm.submitButton.disabled = false;
   cClick();
@@ -35,6 +38,7 @@ function submit_abort() {
 function bottomfixup(what) {
   fixup_position = 0;
   document.CustomerForm.submitButton.disabled = true;
+  running = true;
   submit_continue();
 }
 
@@ -63,8 +67,6 @@ function copy_payby_fields() {
   submit_continue();
 }
 
-%# call submit_continue() on completion...
-%# otherwise not touching standardize_locations for now
 <% include( '/elements/standardize_locations.js',
             'callback' => 'submit_continue();',
             'main_prefix' => 'bill_',
@@ -72,104 +74,6 @@ function copy_payby_fields() {
           )
 %>
 
-var prefix;
-function fetch_censustract() {
-
-  //alert('fetch census tract data');
-  prefix = document.getElementById('same').checked ? 'bill_' : 'ship_';
-  var cf = document.CustomerForm;
-  var state_el = cf.elements[prefix + 'state'];
-  var census_data = new Array(
-    'year',     <% $conf->config('census_year') || '2012' %>,
-    'address1', cf.elements[prefix + 'address1'].value,
-    'city',     cf.elements[prefix + 'city'].value,
-    'state',    state_el.options[ state_el.selectedIndex ].value,
-    'zip',      cf.elements[prefix + 'zip'].value
-  );
-
-  censustract( census_data, update_censustract );
-
-}
-
-var set_censustract;
-
-function update_censustract(arg) {
-
-  var argsHash = eval('(' + arg + ')');
-
-  var cf = document.CustomerForm;
-
-/*  var msacode    = argsHash['msacode'];
-  var statecode  = argsHash['statecode'];
-  var countycode = argsHash['countycode'];
-  var tractcode  = argsHash['tractcode'];
-  
-  var newcensus = 
-    new String(statecode)  +
-    new String(countycode) +
-    new String(tractcode).replace(/\s$/, '');  // JSON 1 workaround */
-  var error      = argsHash['error'];
-  var newcensus  = argsHash['censustract'];
-
-  set_censustract = function () {
-
-    cf.elements[prefix + 'censustract'].value = newcensus;
-    submit_continue();
-
-  }
-
-  if (error || cf.elements[prefix + 'censustract'].value != newcensus) {
-    // popup an entry dialog
-
-    if (error) { newcensus = error; }
-    newcensus.replace(/.*ndefined.*/, 'Not found');
-
-    var latitude = cf.elements[prefix + 'latitude'].value 
-                   || '<% $company_latitude %>';
-    var longitude= cf.elements[prefix + 'longitude'].value 
-                   || '<% $company_longitude %>';
-
-    var choose_censustract =
-      '<CENTER><BR><B>Confirm censustract</B><BR>' +
-      '<A href="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?' +
-      'census_year=<% $conf->config('census_year') || '2012' %>' +
-      '&latitude=' + latitude +
-      '&longitude=' + longitude +
-      '" target="_blank">Map service module location</A><BR>' +
-      '<A href="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?' +
-      'census_year=<% $conf->config('census_year') || '2012' %>' +
-      '&zip_code=' + cf.elements[prefix + 'zip'].value +
-      '" target="_blank">Map zip code center</A><BR><BR>' +
-      '<TABLE>';
-    
-    choose_censustract = choose_censustract + 
-      '<TR><TH style="width:50%">Entered census tract</TH>' +
-        '<TH style="width:50%">Calculated census tract</TH></TR>' +
-      '<TR><TD>' + cf.elements[prefix + 'censustract'].value +
-        '</TD><TD>' + newcensus + '</TD></TR>' +
-        '<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
-
-    choose_censustract = choose_censustract +
-      '<TR><TD ALIGN="center">' +
-        '<BUTTON TYPE="button" onClick="submit_continue();"><IMG SRC="<%$p%>images/error.png" ALT=""> Use entered census tract </BUTTON>' + 
-      '</TD><TD ALIGN="center">' +
-        '<BUTTON TYPE="button" onClick="set_censustract();"><IMG SRC="<%$p%>images/tick.png" ALT=""> Use calculated census tract </BUTTON>' + 
-      '</TD></TR>' +
-      '<TR><TD COLSPAN=2 ALIGN="center">' +
-        '<BUTTON TYPE="button" onClick="submit_abort();"><IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission</BUTTON></TD></TR>' +
-        
-      '</TABLE></CENTER>';
-
-    overlib( choose_censustract, CAPTION, 'Confirm censustract', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
-
-  } else {
-
-    submit_continue();
-
-  }
-
-}
-
 function copyelement(from, to) {
   if ( from == undefined ) {
     to.value = '';
@@ -192,6 +96,35 @@ function copyelement(from, to) {
   //alert(from + " (" + from.type + "): " + to.name + " => " + to.value);
 }
 
+% # the value in 'censustract' is the confirmed censustract; if it's set,
+% # do nothing here
+function confirm_censustract() {
+  var cf = document.CustomerForm;
+  if ( cf.elements['censustract'].value == '' ) {
+    var address_info = form_address_info();
+    address_info['ship_latitude']  = cf.elements['ship_latitude'].value;
+    address_info['ship_longitude'] = cf.elements['ship_longitude'].value;
+    OLpostAJAX(
+        '<%$p%>/misc/confirm-censustract.html',
+        'q=' + encodeURIComponent(JSON.stringify(address_info)),
+        function() {
+          overlib( OLresponseAJAX, CAPTION, 'Confirm censustract', STICKY,
+            AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH,
+            576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399',
+            TEXTSIZE, 3 );
+        },
+        0);
+  } else submit_continue();
+}
+
+%# called from confirm-censustract.html
+function set_censustract(tract, year) {
+  var cf = document.CustomerForm;
+  cf.elements['censustract'].value = tract;
+  cf.elements['censusyear'].value = year;
+  submit_continue();
+}
+
 function check_unique() {
   var search_hash = new Object;
 % foreach ($conf->config('cust_main-check_unique')) {
index 57490b9..4140ec1 100644 (file)
@@ -174,9 +174,7 @@ $cust_main->set('stateid_state', $cust_main->state )
 
 $opt{geocode} ||= $cust_main->get('geocode');
 
-if ( $conf->exists('cust_main-require_censustract') ) {
-  $opt{censustract} ||= $cust_main->censustract;
-}
+$opt{censustract} ||= $cust_main->censustract;
 
 $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
                    ? 'Day'
index 5c7c888..de844e4 100644 (file)
@@ -33,7 +33,7 @@ Example:
                  'options'    => \@location_kind_options,
                  'labels'     => $location_kind_labels,
                  'curr_value' => scalar($cgi->param('location_kind'))
-                                   || $object->get($pre.'location_kind'),
+                                   || $object->get('location_kind'),
               )
     %>
     </TD>
@@ -164,7 +164,7 @@ Example:
            NAME     = "<%$pre%>zip"
            ID       = "<%$pre%>zip"
            VALUE    = "<% $object->get('zip') |h %>"
-           SIZE     = 10
+           SIZE     = 11
            onChange = "<% $onchange %>"
            <% $disabled %>
            <% $style %>
@@ -206,23 +206,22 @@ Example:
 <INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
 
 <INPUT TYPE="hidden" NAME="<%$pre%>geocode" VALUE="<% $object->geocode %>">
+<INPUT TYPE="hidden" NAME="<%$pre%>censustract" VALUE="<% $object->censustract %>">
 <INPUT TYPE="hidden" NAME="<%$pre%>censusyear" VALUE="<% $object->censusyear %>">
-<TR>
 % if ( $opt{enable_censustract} ) {
+<TR>
   <TD ALIGN="right">Census&nbsp;tract</TD>
   <TD COLSPAN=8>
     <INPUT TYPE="text" SIZE=15
-           NAME="<%$pre%>censustract" 
+           NAME="enter_censustract" 
            VALUE="<% $object->censustract %>">
     <% '(automatic)' %>
   </TD>
-% } else {
-  <INPUT TYPE="hidden" NAME="<%$pre%>censustract" VALUE="<% $object->censustract %>">
-% } 
 </TR>
+% }
 % if ( $conf->config('tax_district_method') ) {
-  <TR>
 %   if ( $opt{enable_district} ) {
+  <TR>
     <TD ALIGN="right">Tax&nbsp;district</TD>
     <TD COLSPAN=8>
       <INPUT TYPE="text" SIZE=15
@@ -230,12 +229,22 @@ Example:
              VALUE="<% $object->district %>">
     <% '(automatic)' %>
     </TD>
+  </TR>
 %   } else {
     <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>">
 %   }
-  </TR>
 % }
 
+%# For address standardization:
+%# keep a clean copy of the address so we know if we need
+%# to re-standardize
+% foreach (qw(address1 city state country zip latitude
+%             longitude censustract addr_clean) ) {
+<INPUT TYPE="hidden" NAME="old_<%$pre.$_%>" ID="old_<%$pre.$_%>" VALUE="<% $object->get($_) |h%>">
+% }
+%# Placeholders
+<INPUT TYPE="hidden" NAME="<%$pre%>cachenum" VALUE="">
+<INPUT TYPE="hidden" NAME="<%$pre%>addr_clean" VALUE="">
 <%init>
 
 my %opt = @_;
index 4807359..8c1efd9 100644 (file)
@@ -19,13 +19,10 @@ function pkg_changed () {
       form.start_date_text.disabled = false;
       form.start_date.style.backgroundColor = '#ffffff';
       form.start_date_button.style.display = '';
-      form.start_date_button_disabled.style.display = 'none';
-      form.invoice_terms.disabled = true;
     } else {
       form.start_date_text.disabled = true;
       form.start_date.style.backgroundColor = '#dddddd';
       form.start_date_button.style.display = 'none';
-      form.start_date_button_disabled.style.display = '';
     }
 
   } else {
@@ -44,3 +41,7 @@ function standardize_new_location() {
     form.submit();
   }
 }
+
+function submit_abort() {
+  document.OrderPkgForm.submitButton.disabled = false;
+}
index 9f8b71c..5a4ee0f 100644 (file)
@@ -1,7 +1,7 @@
 <% include('/elements/init_overlib.html') %>
 
 <% include( '/elements/xmlhttp.html',
-              'url'  => $p.'misc/xmlhttp-cust_main-address_standardize.html',
+              'url'  => $p.'misc/xmlhttp-address_standardize.html',
               'subs' => [ 'address_standardize' ],
               #'method' => 'POST', #could get too long?
           )
index 86f8d2b..f6564a5 100644 (file)
-function standardize_locations() {
-
+function form_address_info() {
   var cf = document.<% $formname %>;
-
   var state_el      = cf.elements['<% $main_prefix %>state'];
   var ship_state_el = cf.elements['<% $ship_prefix %>state'];
-
-  var address_info = new Array(
+  return {
 % if ( $onlyship ) {
-    'onlyship', 1,
+    'onlyship': 1,
 % } else {
 %   if ( $withfirm ) {
     'company',  cf.elements['company'].value,
 %   }
-    'address1', cf.elements['<% $main_prefix %>address1'].value,
-    'address2', cf.elements['<% $main_prefix %>address2'].value,
-    'city',     cf.elements['<% $main_prefix %>city'].value,
-    'state',    state_el.options[ state_el.selectedIndex ].value,
-    'zip',      cf.elements['<% $main_prefix %>zip'].value,
+    'address1': cf.elements['<% $main_prefix %>address1'].value,
+    'address2': cf.elements['<% $main_prefix %>address2'].value,
+    'city':     cf.elements['<% $main_prefix %>city'].value,
+    'state':    state_el.options[ state_el.selectedIndex ].value,
+    'zip':      cf.elements['<% $main_prefix %>zip'].value,
+    'country':  cf.elements['<% $main_prefix %>country'].value,
 % }
-    'ship_address1', cf.elements['<% $ship_prefix %>address1'].value,
-    'ship_address2', cf.elements['<% $ship_prefix %>address2'].value,
-    'ship_city',     cf.elements['<% $ship_prefix %>city'].value,
-    'ship_state',    ship_state_el.options[ ship_state_el.selectedIndex ].value,
-    'ship_zip',      cf.elements['<% $ship_prefix %>zip'].value
-  );
-
-  address_standardize( address_info, update_address );
-
+% if ( $withcensus ) {
+    'ship_censustract': cf.elements['enter_censustract'].value,
+% }
+    'ship_address1': cf.elements['<% $ship_prefix %>address1'].value,
+    'ship_address2': cf.elements['<% $ship_prefix %>address2'].value,
+    'ship_city':     cf.elements['<% $ship_prefix %>city'].value,
+    'ship_state':    ship_state_el.options[ ship_state_el.selectedIndex ].value,
+    'ship_zip':      cf.elements['<% $ship_prefix %>zip'].value,
+    'ship_country':  cf.elements['<% $ship_prefix %>country'].value,
+  };
 }
 
-var standardize_address;
-
-function update_address(arg) {
+function standardize_locations() {
 
-  var argsHash = eval('(' + arg + ')');
+  var startup_msg = '<P STYLE="position:absolute; top:50%; margin-top:-1em; width:100%; text-align:center"><B><FONT SIZE="+1">Verifying address...</FONT></B></P>';
+  overlib(startup_msg, WIDTH, 444, HEIGHT, 168, CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSECLICK, MIDX, 0, MIDY, 0);
+  var cf = document.<% $formname %>;
+  var address_info = form_address_info();
 
-  var changed  = argsHash['address_standardized'];
-  var ship_changed = argsHash['ship_address_standardized'];
-  var error = argsHash['error'];
-  var ship_error = argsHash['ship_error'];
-  
+  var changed = false; // have any of the address fields been changed?
 
-  //yay closures
-  standardize_address = function () {
+// clear coord_auto fields if the user has changed the coordinates
+% for my $pre ($ship_prefix, $onlyship ? () : $main_prefix) {
+%   for my $field ($pre.'latitude', $pre.'longitude') {
 
-    var cf = document.<% $formname %>;
-    var state_el      = cf.elements['<% $main_prefix %>state'];
-    var ship_state_el = cf.elements['<% $ship_prefix %>state'];
+  if ( cf.elements['<% $field %>'].value != cf.elements['old_<% $field %>'].value ) {
+    cf.elements['<% $pre %>coord_auto'].value = '';
+  }
 
-% if ( !$onlyship ) {
-    if ( changed ) {
-%   if ( $withfirm ) {
-      cf.elements['<% $main_prefix %>company'].value  = argsHash['new_company'];
 %   }
-      cf.elements['<% $main_prefix %>address1'].value = argsHash['new_address1'];
-      cf.elements['<% $main_prefix %>address2'].value = argsHash['new_address2'];
-      cf.elements['<% $main_prefix %>city'].value     = argsHash['new_city'];
-      setselect(cf.elements['<% $main_prefix %>state'], argsHash['new_state']);
-      cf.elements['<% $main_prefix %>zip'].value      = argsHash['new_zip'];
-    }
-% }
+  // but if the coordinates have been set to null, turn coord_auto on 
+  // and standardize
+  if ( cf.elements['<% $pre %>latitude'].value == '' &&
+       cf.elements['<% $pre %>longitude'].value == '' ) {
+    cf.elements['<% $pre %>coord_auto'].value = 'Y';
+    changed = true;
+  }
 
-    if ( ship_changed ) {
-% if ( $withfirm ) {
-      cf.elements['<% $ship_prefix %>company'].value  = argsHash['new_ship_company'];
 % }
-      cf.elements['<% $ship_prefix %>address1'].value = argsHash['new_ship_address1'];
-      cf.elements['<% $ship_prefix %>address2'].value = argsHash['new_ship_address2'];
-      cf.elements['<% $ship_prefix %>city'].value     = argsHash['new_ship_city'];
-      setselect(cf.elements['<% $ship_prefix %>state'], argsHash['new_ship_state']);
-      cf.elements['<% $ship_prefix %>zip'].value      = argsHash['new_ship_zip'];
-    }
 
-    post_standardization();
+  // standardize if the old address wasn't clean
+  if ( cf.elements['old_<% $ship_prefix %>addr_clean'].value == '' ||
+      ( <% !$onlyship || 0 %> && 
+        cf.elements['old_<% $main_prefix %>addr_clean'].value == '' ) ) {
 
+    changed = true;
+
+  }
+  // or if it was clean but has been changed
+  for (var key in address_info) {
+    var old_el = cf.elements['old_'+key];
+    if ( old_el && address_info[key] != old_el.value ) {
+      changed = true;
+      break;
+    }
   }
 
+% # If address hasn't been changed, auto-confirm the existing value of 
+% # censustract so that we don't ask the user to confirm it again.
 
+  if ( !changed ) {
+    cf.elements['<% $main_prefix %>censustract'].value =
+      address_info['ship_censustract'];
+  }
 
-  if ( changed || ship_changed ) {
+% if ( $conf->config('address_standardize_method') ) {
+  if ( changed ) {
+    address_standardize(JSON.stringify(address_info), confirm_standardize);
+  }
+  else {
+    cf.elements['ship_addr_clean'].value = 'Y';
+%   if ( !$onlyship ) {
+    cf.elements['addr_clean'].value = 'Y';
+%   }
+    post_standardization();
+  }
 
-%   if ( $conf->exists('cust_main-auto_standardize_address') ) {
+% } else {
 
-    standardize_address();
+  post_standardization();
 
-%   } else {
+% } # if address_standardize_method
+}
 
-    // popup a confirmation popup
+var returned;
 
-    var confirm_change =
-      '<CENTER><BR><B>Confirm address standardization</B><BR><BR>' +
-      '<TABLE>';
-    
-    if ( changed ) {
+function confirm_standardize(arg) {
+  // contains 'old', which was what we sent, and 'new', which is what came
+  // back, including any errors
+  returned = JSON.parse(arg);
 
-      confirm_change = confirm_change + 
-        '<TR><TH>Entered billing address</TH>' +
-          '<TH>Standardized billing address</TH></TR>';
-        // + '<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
-      
-      if ( argsHash['company'] || argsHash['new_company'] ) {
-        confirm_change = confirm_change +
-        '<TR><TD>' + argsHash['company'] +
-          '</TD><TD>' + argsHash['new_company'] + '</TD></TR>';
-      }
-      
-      confirm_change = confirm_change +
-        '<TR><TD>' + argsHash['address1'] +
-          '</TD><TD>' + argsHash['new_address1'] + '</TD></TR>' +
-        '<TR><TD>' + argsHash['address2'] +
-          '</TD><TD>' + argsHash['new_address2'] + '</TD></TR>' +
-        '<TR><TD>' + argsHash['city'] + ', ' + argsHash['state'] + '  ' + argsHash['zip'] +
-          '</TD><TD>' + argsHash['new_city'] + ', ' + argsHash['new_state'] + '  ' + argsHash['new_zip'] + '</TD></TR>' +
-          '<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
+  if ( <% $conf->exists('cust_main-auto_standardize_address') || 0 %> ) {
 
-    }
+    replace_address(); // with the contents of returned['new']
+  
+  }
+  else {
+
+    var querystring = encodeURIComponent( JSON.stringify(returned) );
+    // confirmation popup: knows to call replace_address(), 
+    // post_standardization(), or submit_abort() depending on the 
+    // user's choice.
+    OLpostAJAX(
+        '<%$p%>/misc/confirm-address_standardize.html', 
+        'q='+querystring,
+        function() {
+          overlib( OLresponseAJAX, CAPTION, 'Address standardization', STICKY, 
+            AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 
+            576, HEIGHT, 268, BGCOLOR, '#333399', CGCOLOR, '#333399', 
+            TEXTSIZE, 3 );
+        }, 0);
 
-    if ( ship_changed ) {
-
-      confirm_change = confirm_change + 
-        '<TR><TH>Entered service address</TH>' +
-          '<TH>Standardized service address</TH></TR>';
-        // + '<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
-      
-      if ( argsHash['ship_company'] || argsHash['new_ship_company'] ) {
-        confirm_change = confirm_change +
-        '<TR><TD>' + argsHash['ship_company'] +
-          '</TD><TD>' + argsHash['new_ship_company'] + '</TD></TR>';
-      }
-      
-      confirm_change = confirm_change +
-        '<TR><TD>' + argsHash['ship_address1'] +
-          '</TD><TD>' + argsHash['new_ship_address1'] + '</TD></TR>' +
-        '<TR><TD>' + argsHash['ship_address2'] +
-          '</TD><TD>' + argsHash['new_ship_address2'] + '</TD></TR>' +
-        '<TR><TD>' + argsHash['ship_city'] + ', ' + argsHash['ship_state'] + '  ' + argsHash['ship_zip'] +
-          '</TD><TD>' + argsHash['new_ship_city'] + ', ' + argsHash['new_ship_state'] + '  ' + argsHash['new_ship_zip'] + '</TD></TR>' +
-        '<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
+  }
+}
 
-    }
+function replace_address() {
 
-    var addresses = 'address';
-    var height = 268;
-    if ( changed && ship_changed ) {
-      addresses = 'addresses';
-      height = 396; // #what
-    }
+  var newaddr = returned['new'];
 
-    confirm_change = confirm_change +
-      '<TR><TD>' +
-        '<BUTTON TYPE="button" onClick="post_standardization();"><IMG SRC="<%$p%>images/error.png" ALT=""> Use entered ' + addresses + '</BUTTON>' + 
-      '</TD><TD>' +
-        '<BUTTON TYPE="button" onClick="standardize_address();"><IMG SRC="<%$p%>images/tick.png" ALT=""> Use standardized ' + addresses + '</BUTTON>' + 
-      '</TD></TR>' +
-      '<TR><TD COLSPAN=2 ALIGN="center">' +
-        '<BUTTON TYPE="button" onClick="document.<% $formname %>.submitButton.disabled=false; parent.cClick();"><IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission</BUTTON></TD></TR>' +
-        
-      '</TABLE></CENTER>';
+  var clean = newaddr['addr_clean'] == 'Y';
+  var ship_clean = newaddr['ship_addr_clean'] == 'Y';
+  var error = newaddr['error'];
+  var ship_error = newaddr['ship_error'];
 
-    overlib( confirm_change, CAPTION, 'Confirm address standardization', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', MIDX, 0, MIDY, 0, DRAGGABLE, WIDTH, 576, HEIGHT, height, BGCOLOR, '#333399', CGCOLOR, '#333399', TEXTSIZE, 3 );
+  var cf = document.<% $formname %>;
+  var state_el      = cf.elements['<% $main_prefix %>state'];
+  var ship_state_el = cf.elements['<% $ship_prefix %>state'];
 
+% if ( !$onlyship ) {
+  if ( clean ) {
+%   if ( $withfirm ) {
+        cf.elements['<% $main_prefix %>company'].value  = newaddr['company'];
 %   }
+        cf.elements['<% $main_prefix %>address1'].value = newaddr['address1'];
+        cf.elements['<% $main_prefix %>address2'].value = newaddr['address2'];
+        cf.elements['<% $main_prefix %>city'].value     = newaddr['city'];
+        setselect(cf.elements['<% $main_prefix %>state'], newaddr['state']);
+        cf.elements['<% $main_prefix %>zip'].value      = newaddr['zip'];
+        cf.elements['<% $main_prefix %>addr_clean'].value = 'Y';
+
+        if ( cf.elements['<% $main_prefix %>coord_auto'].value ) {
+          cf.elements['<% $main_prefix %>latitude'].value = newaddr['latitude'];
+          cf.elements['<% $main_prefix %>longitude'].value = newaddr['longitude'];
+        }
+  }
+% }
 
-  } else {
-
-    post_standardization();
-
+  if ( ship_clean ) {
+% if ( $withfirm ) {
+      cf.elements['<% $ship_prefix %>company'].value  = newaddr['ship_company'];
+% }
+      cf.elements['<% $ship_prefix %>address1'].value = newaddr['ship_address1'];
+      cf.elements['<% $ship_prefix %>address2'].value = newaddr['ship_address2'];
+      cf.elements['<% $ship_prefix %>city'].value     = newaddr['ship_city'];
+      setselect(cf.elements['<% $ship_prefix %>state'], newaddr['ship_state']);
+      cf.elements['<% $ship_prefix %>zip'].value      = newaddr['ship_zip'];
+      cf.elements['<% $ship_prefix %>addr_clean'].value = 'Y';
+      if ( cf.elements['<% $ship_prefix %>coord_auto'].value ) {
+        cf.elements['<% $ship_prefix %>latitude'].value = newaddr['latitude'];
+        cf.elements['<% $ship_prefix %>longitude'].value = newaddr['longitude'];
+      }
+  }
+% if ( $withcensus ) {
+% # then set the censustract if address_standardize provided one.
+  if ( ship_clean && newaddr['ship_censustract'] ) {
+      cf.elements['<% $main_prefix %>censustract'].value = newaddr['ship_censustract'];
   }
+% }
 
+  post_standardization();
 
 }
 
-function post_standardization() {
-
+function confirm_manual_address() {
+%# not much to do in this case, just confirm the censustract
+% if ( $withcensus ) {
   var cf = document.<% $formname %>;
+  cf.elements['<% $main_prefix %>censustract'].value =
+  cf.elements['<% $main_prefix %>enter_censustract'].value;
+% }
+  post_standardization();
+}
+
+function post_standardization() {
 
 % if ( $conf->exists('enable_taxproducts') ) {
 
@@ -262,6 +288,7 @@ my %opt = @_;
 my $conf = new FS::Conf;
 
 my $withfirm = 1;
+my $withcensus = 1;
 
 my $formname =  $opt{form} || 'CustomerForm';
 my $onlyship =  $opt{onlyship} || '';
@@ -271,5 +298,6 @@ my $taxpre = $main_prefix;
 $taxpre = $ship_prefix if ( $conf->exists('tax-ship_address') || $onlyship );
 my $post_geocode = $opt{callback} || 'post_geocode();';
 $withfirm = 0 if $opt{no_company};
+$withcensus = 0 if $opt{no_census};
 
 </%init>
index b804f45..7ffbd6c 100644 (file)
@@ -52,10 +52,11 @@ Example:
       if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff';
 %   } 
 %   if ( $opt{'alt_format'} ) {
-    if ( what.form.location_type.options[what.form.location_type.selectedIndex].value ) {
+      if ( what.form.location_type &&
+           what.form.location_type.options[what.form.location_type.selectedIndex].value ) {
       what.form.location_number.disabled = false;
       what.form.location_number.style.backgroundColor = '#ffffff';
-    }
+      }
 %   }
   }
 
@@ -281,6 +282,8 @@ if ( $locationnum && $locationnum > 0 ) {
   }
 }
 
+$cust_location->coord_auto('Y');
+
 my $location_sort = sub {
         $a->country   cmp $b->country
   or lc($a->city)     cmp lc($b->city)
index 2ab9329..7b08f7b 100755 (executable)
@@ -34,6 +34,7 @@
             'form'       => "OrderPkgForm",
             'onlyship'   => 1,
             'no_company' => 1,
+            'no_census'  => 1,
             'callback'   => 'document.OrderPkgForm.submit();',
 &>
 
diff --git a/httemplate/misc/confirm-address_standardize.html b/httemplate/misc/confirm-address_standardize.html
new file mode 100644 (file)
index 0000000..3603b95
--- /dev/null
@@ -0,0 +1,123 @@
+<STYLE type="text/css">
+th { line-height: 150% }
+</STYLE>
+<CENTER><BR><B>
+% if ( $new{error} or $new{ship_error} ) {
+Address standardization error
+% }
+% else {
+Confirm address standardization
+% }
+
+</B><BR><BR>
+<TABLE WIDTH="100%">
+% for my $pre ('', 'ship_') {
+%   next if !$pre and $old{onlyship};
+%   my $name = $pre eq 'ship_' ? 'service' : 'billing';
+%   if ( $new{$pre.'addr_clean'} ) {
+  <TR>
+    <TH>Entered <%$name%> address</TH>
+    <TH>Standardized <%$name%> address</TH>
+  </TR>
+  <TR>
+%     if ( $old{$pre.'company'} ) {
+  <TR>
+    <TD><% $old{$pre.'company'} %></TD>
+    <TD><% $new{$pre.'company'} %></TD>
+  </TR>
+%     }
+  <TR>
+    <TD><% $old{$pre.'address1'} %></TD>
+    <TD><% $new{$pre.'address1'} %></TD>
+  </TR>
+  <TR>
+    <TD><% $old{$pre.'address2'} %></TD>
+    <TD><% $new{$pre.'address2'} %></TD>
+  </TR>
+  <TR>
+    <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
+    <TD><% $new{$pre.'city'} %>, <% $new{$pre.'state'} %>  <% $new{$pre.'zip'} %></TD>
+  </TR>
+
+%   } # if addr_clean
+%     elsif ( $new{$pre.'error'} ) {
+  <TR>
+    <TH>Entered <%$name%> address</TH>
+  </TR>
+%     if ( $old{$pre.'company'} ) {
+  <TR>
+    <TD><% $old{$pre.'company'} %></TD>
+  </TR>
+%     }
+  <TR>
+    <TD><% $old{$pre.'address1'} %></TD>
+    <TD ROWSPAN=3><FONT COLOR="#ff0000"><B><% $new{$pre.'error'} %></B></FONT></TD>
+  </TR>
+  <TR>
+    <TD><% $old{$pre.'address2'} %></TD>
+  </TR>
+  <TR>
+    <TD><% $old{$pre.'city'} %>, <% $old{$pre.'state'} %>  <% $old{$pre.'zip'} %></TD>
+  </TR>
+%   } #if error
+% } # for $pre
+
+%# only do this part if address standardization provided a censustract
+% if ( $new{'ship_censustract'} ) {
+  <TR>
+    <TH>Entered census tract</TH>
+    <TH>Calculated census tract</TH>
+  </TR>
+  <TR>
+    <TD><% $old{'ship_censustract'} %></TD>
+    <TD>
+%     if ( $new{'ship_census_error'} ) {
+      <FONT COLOR="#ff0000"><% $new{'ship_census_error'} %></FONT>
+%     } else {
+      <% $new{'ship_censustract'} %>
+%     }
+    </TD>
+  </TR>
+% } #if censustract
+
+% if ( $new{error} or $new{ship_error} ) {
+  <TR>
+    <TD ALIGN="center">
+    <BUTTON TYPE="button" STYLE="width:205px" onclick="confirm_manual_address();">
+      <IMG SRC="<%$p%>images/error.png" ALT=""> Use entered <%$addresses%>
+    </BUTTON></TD>
+    <TD ALIGN="center">
+    <BUTTON TYPE="button" STYLE="width:205px" onclick="submit_abort();">
+      <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission
+    </BUTTON></TD>
+  </TR>
+% }
+% else {
+  <TR>
+    <TD ALIGN="center">
+    <BUTTON TYPE="button" STYLE="width:205px" onclick="confirm_manual_address()();">
+      <IMG SRC="<%$p%>images/error.png" ALT=""> Use entered <%$addresses%>
+    </BUTTON></TD>
+    <TD ALIGN="center">
+    <BUTTON TYPE="button" STYLE="width:205px" onclick="replace_address();">
+      <IMG SRC="<%$p%>images/tick.png" ALT=""> Use standardized <%$addresses%>
+    </BUTTON></TD>
+  </TR>
+  <TR ALIGN="center"><TD COLSPAN=2>
+    <BUTTON TYPE="button" STYLE="width:205px" onclick="submit_abort();">
+      <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission
+    </BUTTON>
+  </TD></TR>
+</TABLE>
+% } # !error
+<%init>
+
+# slightly weird interface...
+my $q = decode_json($cgi->param('q'));
+#warn Dumper($q);
+my %old = %{ $q->{old} };
+my %new = %{ $q->{new} };
+
+my $addresses = $old{onlyship} ? 'address' : 'addresses';
+
+</%init>
diff --git a/httemplate/misc/confirm-censustract.html b/httemplate/misc/confirm-censustract.html
new file mode 100644 (file)
index 0000000..ae0ae3a
--- /dev/null
@@ -0,0 +1,78 @@
+<CENTER><BR><B>
+% if ( $error ) {
+Census tract error
+% }
+% else {
+Confirm census tract
+% }
+</B><BR>
+% my $querystring = "census_year=$year&latitude=".$cache->get('latitude').'&longitude='.$cache->get('longitude');
+<A HREF="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?<% $querystring %>"
+   TARGET="_blank">Map service module location</A><BR>
+% $querystring = "census_year=$year&zip_code=".$cache->get('zip');
+<A HREF="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?<% $querystring %>"
+   TARGET="_blank">Map zip code center</A><BR>
+<BR>
+<TABLE>
+  <TR>
+    <TH style="width:50%">Entered census tract</TH>
+    <TH style="width:50%">Calculated census tract</TH>
+  </TR>
+  <TR>
+    <TD><% $old_tract %></TD>
+% if ( $error ) {
+    <TD><FONT COLOR="#ff0000"><% $error %></FONT></TD>
+% } else {
+    <TD><% $new_tract %></TD>
+% }
+  </TR>
+  <TR>
+    <TD ALIGN="center">
+      <BUTTON TYPE="button"
+              onclick="set_censustract('<% $old_tract %>', '<% $year %>')">
+      <IMG SRC="<%$p%>images/error.png" ALT=""> Use entered census tract
+      </BUTTON>
+    </TD>
+    <TD ALIGN="center">
+      <BUTTON TYPE="button"
+              onclick="set_censustract('<% $new_tract %>', '<% $year %>')">
+      <IMG SRC="<%$p%>images/tick.png" ALT=""> Use calculated census tract
+      </BUTTON>
+    </TD>
+  </TR>
+  <TR>
+    <TD COLSPAN=2 ALIGN="center">
+      <BUTTON TYPE="button" onclick="submit_abort()">
+      <IMG SRC="<%$p%>images/cross.png" ALT=""> Cancel submission
+      </BUTTON>
+    </TD>
+  </TR>
+</TABLE></CENTER>
+<%init>
+
+local $SIG{__DIE__}; #disable Mason error trap
+
+my $DEBUG = 0;
+
+my $conf = new FS::Conf;
+
+warn $cgi->param('q') if $DEBUG;
+
+my $q = decode_json($cgi->param('q'))
+  or die "bad argument '".$cgi->param('q')."'";
+
+my %location = (
+  map { $_ => $q->{'ship_'.$_} }
+    qw( company address1 address2 city state zip country latitude longitude )
+);
+
+my $old_tract = $q->{'ship_censustract'};
+my $cache = eval { FS::GeocodeCache->new(%location) };
+$cache->set_censustract;
+my $year = FS::Conf->new->config('census_year');
+my $new_tract = $cache->get('censustract');
+my $error = $cache->get('censustract_error');
+
+warn Dumper($cache) if $DEBUG;
+
+</%init>
index c5f4509..bfc7b69 100644 (file)
                 'form'       => "OrderPkgForm",
                 'onlyship'   => 1,
                 'no_company' => 1,
+                'no_census'  => 1,
                 'callback'   => 'document.OrderPkgForm.submit();',
   &>
 
diff --git a/httemplate/misc/xmlhttp-address_standardize.html b/httemplate/misc/xmlhttp-address_standardize.html
new file mode 100644 (file)
index 0000000..f53c35f
--- /dev/null
@@ -0,0 +1,38 @@
+<% encode_json($return) %>
+<%init>
+
+local $SIG{__DIE__}; #disable Mason error trap
+
+my $DEBUG = 0;
+
+my $conf = new FS::Conf;
+
+my $sub = $cgi->param('sub');
+
+warn $cgi->param('arg') if $DEBUG;
+
+my %old = %{ decode_json($cgi->param('arg')) }
+  or die "bad argument '".$cgi->param('arg')."'";
+
+my %new;
+
+foreach my $pre ( '', 'ship_' ) {
+  next unless ($pre || !$old{onlyship});
+
+  my $location = {
+    map { $_ => $old{$pre.$_} }
+      qw( company address1 address2 city state zip country )
+  };
+
+  my $cache = eval { FS::GeocodeCache->standardize($location) };
+  $cache->set_coord;
+  # don't do set_censustract here, though censustract may be set by now
+
+  foreach ( keys(%$cache) ) {
+    $new{$pre.$_} = $cache->get($_);
+  }
+}
+
+my $return = { old => \%old, new => \%new };
+warn "result:\n".encode_json($return) if $DEBUG;
+</%init>
diff --git a/httemplate/misc/xmlhttp-cust_main-address_standardize.html b/httemplate/misc/xmlhttp-cust_main-address_standardize.html
deleted file mode 100644 (file)
index d0627cd..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<% objToJson($return) %>
-<%init>
-
-my $DEBUG = 0;
-
-my $conf = new FS::Conf;
-
-my $sub = $cgi->param('sub');
-
-my $return = {};
-
-if ( $sub eq 'address_standardize' ) {
-
-  my %arg = $cgi->param('arg');
-  $return = \%arg;
-  warn join('', map "$_: $arg{$_}\n", keys %arg )
-    if $DEBUG;
-
-  my $userid   = $conf->config('usps_webtools-userid');
-  my $password = $conf->config('usps_webtools-password');
-
-  if ( length($userid) && length($password) ) {
-
-    my $verifier = Business::US::USPS::WebTools::AddressStandardization->new( {
-      UserID   => $userid,   #$ENV{USPS_WEBTOOLS_USERID},
-      Password => $password, #$ENV{USPS_WEBTOOLS_PASSWORD},
-      #Testing  => 1,
-    } );
-
-    foreach my $pre ( '', 'ship_' ) {
-      next unless ($pre || !$arg{onlyship});
-
-      my($zip5, $zip4) = split('-',$arg{$pre.'zip'});
-
-      my %usps_args = (
-        FirmName => $arg{$pre.'company'},
-        Address2 => $arg{$pre.'address1'},
-        Address1 => $arg{$pre.'address2'},
-        City     => $arg{$pre.'city'},  
-        State    => $arg{$pre.'state'},
-        Zip5     => $zip5,
-        Zip4     => $zip4,
-      );
-      warn join('', map "$_: $usps_args{$_}\n", keys %usps_args )
-        if $DEBUG;
-
-      my $hash = $verifier->verify_address( %usps_args );
-
-      warn $verifier->response
-        if $DEBUG;
-
-      unless ( $verifier->is_error ) {
-
-        my $zip = $hash->{Zip5};
-        $zip .= '-'. $hash->{Zip4} if $hash->{Zip4} =~ /\d/;
-
-        $return = {
-          %$return,
-          "new_$pre".'company'  => $hash->{FirmName},
-          "new_$pre".'address1' => $hash->{Address2},
-          "new_$pre".'address2' => $hash->{Address1},
-          "new_$pre".'city'     => $hash->{City},
-          "new_$pre".'state'    => $hash->{State},
-          "new_$pre".'zip'      => $zip,
-        };
-
-        my @fields = (qw( company address1 address2 city state zip )); #hmm
-
-        my $changed =
-          scalar( grep { $return->{$pre.$_} ne $return->{"new_$pre$_"} }
-                       @fields
-                )
-            ? 1 : 0;
-
-        $return->{$pre.'address_standardized'} = $changed;
-
-      } else {
-
-        $return->{$pre.'error'} = "USPS WebTools error: ".
-                                  $verifier->{error}{description};
-
-
-      }
-
-    }
-
-  }
-
-  $return;
-
-}
-
-</%init>