[freeside-commits] branch master updated. 468c9e660eb0edb2033f0f8dbb4458f20280082c

Mark Wells mark at 420.am
Tue Nov 6 12:49:52 PST 2012


The branch, master has been updated
       via  468c9e660eb0edb2033f0f8dbb4458f20280082c (commit)
      from  2b2aa5664742a134da11862a7cedb37d25524423 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 468c9e660eb0edb2033f0f8dbb4458f20280082c
Author: Mark Wells <mark at freeside.biz>
Date:   Tue Nov 6 12:48:41 2012 -0800

    improved address standardization, #13763

diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index e74c19f..02869b1 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -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
index 0000000..7829c4d
--- /dev/null
+++ b/FS/FS/GeocodeCache.pm
@@ -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;
+
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index f7d98a1..944a483 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -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 ) {
diff --git a/FS/FS/Misc/Geo.pm b/FS/FS/Misc/Geo.pm
index 5d6f33c..6bc71fc 100644
--- a/FS/FS/Misc/Geo.pm
+++ b/FS/FS/Misc/Geo.pm
@@ -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;
 
- at EXPORT_OK = qw( get_censustract get_district );
+ at 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
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 912f3e2..9eb59a0 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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, '', '',
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index 2810dc9..1521960 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -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
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 9e39b30..4ea4a6b 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -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);
diff --git a/FS/MANIFEST b/FS/MANIFEST
index f530610..9c444be 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -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
index 0000000..eae6f0d
--- /dev/null
+++ b/FS/t/GeocodeCache.t
@@ -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";
diff --git a/bin/generate-table-module b/bin/generate-table-module
index e7fc992..b536360 100755
--- a/bin/generate-table-module
+++ b/bin/generate-table-module
@@ -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
index 0000000..414ae4c
--- /dev/null
+++ b/bin/usps-webtools-test-script
@@ -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;
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
index 2628b4e..0aded59 100755
--- a/httemplate/edit/cust_main.cgi
+++ b/httemplate/edit/cust_main.cgi
@@ -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;
diff --git a/httemplate/edit/cust_main/bottomfixup.html b/httemplate/edit/cust_main/bottomfixup.html
index 60edcc1..b5d10c4 100644
--- a/httemplate/edit/cust_main/bottomfixup.html
+++ b/httemplate/edit/cust_main/bottomfixup.html
@@ -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="">
diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js
index 77d4294..4f3b7da 100644
--- a/httemplate/edit/cust_main/bottomfixup.js
+++ b/httemplate/edit/cust_main/bottomfixup.js
@@ -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> </TD><TD> </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')) {
diff --git a/httemplate/edit/cust_main/contact.html b/httemplate/edit/cust_main/contact.html
index 57490b9..4140ec1 100644
--- a/httemplate/edit/cust_main/contact.html
+++ b/httemplate/edit/cust_main/contact.html
@@ -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'
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html
index 5c7c888..de844e4 100644
--- a/httemplate/elements/location.html
+++ b/httemplate/elements/location.html
@@ -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 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 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 = @_;
diff --git a/httemplate/elements/order_pkg.js b/httemplate/elements/order_pkg.js
index 4807359..8c1efd9 100644
--- a/httemplate/elements/order_pkg.js
+++ b/httemplate/elements/order_pkg.js
@@ -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;
+}
diff --git a/httemplate/elements/standardize_locations.html b/httemplate/elements/standardize_locations.html
index 9f8b71c..5a4ee0f 100644
--- a/httemplate/elements/standardize_locations.html
+++ b/httemplate/elements/standardize_locations.html
@@ -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?
           )
diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js
index 86f8d2b..f6564a5 100644
--- a/httemplate/elements/standardize_locations.js
+++ b/httemplate/elements/standardize_locations.js
@@ -1,179 +1,205 @@
-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> </TD><TD> </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> </TD><TD> </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> </TD><TD> </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> </TD><TD> </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>
diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html
index b804f45..7ffbd6c 100644
--- a/httemplate/elements/tr-select-cust_location.html
+++ b/httemplate/elements/tr-select-cust_location.html
@@ -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)
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
index 2ab9329..7b08f7b 100755
--- a/httemplate/misc/change_pkg.cgi
+++ b/httemplate/misc/change_pkg.cgi
@@ -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
index 0000000..3603b95
--- /dev/null
+++ b/httemplate/misc/confirm-address_standardize.html
@@ -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
index 0000000..ae0ae3a
--- /dev/null
+++ b/httemplate/misc/confirm-censustract.html
@@ -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>
diff --git a/httemplate/misc/order_pkg.html b/httemplate/misc/order_pkg.html
index c5f4509..bfc7b69 100644
--- a/httemplate/misc/order_pkg.html
+++ b/httemplate/misc/order_pkg.html
@@ -131,6 +131,7 @@
                 '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
index 0000000..f53c35f
--- /dev/null
+++ b/httemplate/misc/xmlhttp-address_standardize.html
@@ -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
index d0627cd..0000000
--- a/httemplate/misc/xmlhttp-cust_main-address_standardize.html
+++ /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>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Conf.pm                                      |   25 ++
 FS/FS/GeocodeCache.pm                              |  209 ++++++++++++++
 FS/FS/Mason.pm                                     |    3 +-
 FS/FS/Misc/Geo.pm                                  |  154 ++++++++++-
 FS/FS/Schema.pm                                    |    3 +
 FS/FS/cust_location.pm                             |    4 +-
 FS/FS/cust_main.pm                                 |   16 -
 FS/MANIFEST                                        |    2 +
 FS/t/{AccessRight.t => GeocodeCache.t}             |    2 +-
 bin/generate-table-module                          |    2 +-
 bin/usps-webtools-test-script                      |   38 +++
 httemplate/edit/cust_main.cgi                      |    2 +
 httemplate/edit/cust_main/bottomfixup.html         |   10 +-
 httemplate/edit/cust_main/bottomfixup.js           |  139 +++-------
 httemplate/edit/cust_main/contact.html             |    4 +-
 httemplate/elements/location.html                  |   27 ++-
 httemplate/elements/order_pkg.js                   |    7 +-
 httemplate/elements/standardize_locations.html     |    2 +-
 httemplate/elements/standardize_locations.js       |  288 +++++++++++---------
 httemplate/elements/tr-select-cust_location.html   |    7 +-
 httemplate/misc/change_pkg.cgi                     |    1 +
 httemplate/misc/confirm-address_standardize.html   |  123 +++++++++
 httemplate/misc/confirm-censustract.html           |   78 ++++++
 httemplate/misc/order_pkg.html                     |    1 +
 httemplate/misc/xmlhttp-address_standardize.html   |   38 +++
 .../xmlhttp-cust_main-address_standardize.html     |   93 -------
 26 files changed, 892 insertions(+), 386 deletions(-)
 create mode 100644 FS/FS/GeocodeCache.pm
 copy FS/t/{AccessRight.t => GeocodeCache.t} (82%)
 create mode 100755 bin/usps-webtools-test-script
 create mode 100644 httemplate/misc/confirm-address_standardize.html
 create mode 100644 httemplate/misc/confirm-censustract.html
 create mode 100644 httemplate/misc/xmlhttp-address_standardize.html
 delete mode 100644 httemplate/misc/xmlhttp-cust_main-address_standardize.html




More information about the freeside-commits mailing list