1 package FS::geocode_Mixin;
4 use vars qw( $DEBUG $me );
7 use Geo::Coder::Googlev3; #compile time for now, until others are supported
8 use FS::Record qw( qsearchs qsearch );
11 use FS::cust_location;
12 use FS::cust_tax_location;
16 $me = '[FS::geocode_Mixin]';
20 FS::geocode_Mixin - Mixin class for records that contain address and other
25 package FS::some_table;
26 use base ( FS::geocode_Mixin FS::Record );
30 FS::geocode_Mixin - This is a mixin class for records that contain address
31 and other location fields.
41 Returns a list of key/value pairs, with the following keys: address1, address2,
42 city, county, state, zip, country, geocode, location_type, location_number,
43 location_kind. The shipping address is used if present.
47 #geocode dependent on tax-ship_address config
51 my $prefix = $self->has_ship_address ? 'ship_' : '';
53 map { my $method = ($_ eq 'geocode') ? $_ : $prefix.$_;
54 $_ => $self->get($method);
56 qw( address1 address2 city county state zip country geocode
57 location_type location_number location_kind );
60 =item location_label [ OPTION => VALUE ... ]
62 Returns the label of the service location (see analog in L<FS::cust_location>) for this customer.
70 used to separate the address elements (defaults to ', ')
74 a callback used for escaping the text of the address elements
84 my $separator = $opt{join_string} || ', ';
85 my $escape = $opt{escape_function} || sub{ shift };
86 my $ds = $opt{double_space} || ' ';
89 $opt{'countrydefault'} || FS::Conf->new->config('countrydefault') || 'US';
90 my $prefix = $self->has_ship_address ? 'ship_' : '';
93 foreach (qw ( address1 address2 ) ) {
94 my $method = "$prefix$_";
95 $line .= ($notfirst ? $separator : ''). &$escape($self->$method)
100 my $lt = $self->get($prefix.'location_type');
103 if ( 1 ) { #ikano, switch on via config
104 { no warnings 'void';
105 eval { 'use FS::part_export::ikano;' };
108 %location_type = FS::part_export::ikano->location_types;
110 %location_type = (); #?
113 $line .= ' '.&$escape( $location_type{$lt} || $lt );
116 $line .= ' '. &$escape($self->get($prefix.'location_number'))
117 if $self->get($prefix.'location_number');
120 foreach (qw ( city county state zip ) ) {
121 my $method = "$prefix$_";
122 if ( $self->$method ) {
123 $line .= ' (' if $method eq 'county';
124 $line .= ($notfirst ? ' ' : $separator). &$escape($self->$method);
125 $line .= ' )' if $method eq 'county';
129 $line .= $separator. &$escape(code2country($self->country))
130 if $self->country ne $cydefault;
135 =item set_coord [ PREFIX ]
137 Look up the coordinates of the location using (currently) the Google Maps
138 API and set the 'latitude' and 'longitude' fields accordingly.
140 PREFIX, if specified, will be prepended to all location field names,
141 including latitude and longitude.
147 my $pre = scalar(@_) ? shift : '';
149 #my $module = FS::Conf->new->config('geocode_module') || 'Geo::Coder::Googlev3';
151 my $geocoder = Geo::Coder::Googlev3->new;
153 my $location = eval {
154 $geocoder->geocode( location =>
155 $self->get($pre.'address1'). ','.
156 ( $self->get($pre.'address2') ? $self->get($pre.'address2').',' : '' ).
157 $self->get($pre.'city'). ','.
158 $self->get($pre.'state'). ','.
159 code2country($self->get($pre.'country'))
163 warn "geocoding error: $@\n";
167 my $geo_loc = $location->{'geometry'}{'location'} or return;
168 if ( $geo_loc->{'lat'} && $geo_loc->{'lng'} ) {
169 $self->set($pre.'latitude', $geo_loc->{'lat'} );
170 $self->set($pre.'longitude', $geo_loc->{'lng'} );
171 $self->set($pre.'coord_auto', 'Y');
176 =item geocode DATA_VENDOR
178 Returns a value for the customer location as encoded by DATA_VENDOR.
179 Currently this only makes sense for "CCH" as DATA_VENDOR.
184 my ($self, $data_vendor) = (shift, shift); #always cch for now
186 my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
187 return $geocode if $geocode;
189 if ( $self->isa('FS::cust_main') ) {
190 warn "WARNING: FS::cust_main->geocode deprecated";
193 my $m = FS::Conf->new->exists('tax-ship_address') ? 'ship_location'
195 my $location = $self->$m or return '';
196 return $location->geocode($data_vendor);
199 my($zip,$plus4) = split /-/, $self->get('zip')
200 if $self->country eq 'US';
204 #CCH specific location stuff
205 my $extra_sql = $plus4 ? "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"
208 my @cust_tax_location =
210 'table' => 'cust_tax_location',
211 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor },
212 'extra_sql' => $extra_sql,
213 'order_by' => 'ORDER BY plus4hi',#overlapping with distinct ends
216 $geocode = $cust_tax_location[0]->geocode
217 if scalar(@cust_tax_location);
219 warn "WARNING: customer ". $self->custnum.
220 ": multiple locations for zip ". $self->get("zip").
221 "; using arbitrary geocode $geocode\n"
222 if scalar(@cust_tax_location) > 1;
227 =item process_district_update CLASS ID
229 Queueable function to update the tax district code using the selected method
230 (config 'tax_district_method'). CLASS is either 'FS::cust_main' or
231 'FS::cust_location'; ID is the key in one of those tables.
235 sub process_district_update {
241 eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
243 die "$class has no location data" if !$class->can('location_hash');
245 my $conf = FS::Conf->new;
246 my $method = $conf->config('tax_district_method')
247 or return; #nothing to do if null
248 my $self = $class->by_key($id) or die "object $id not found";
250 # dies on error, fine
251 my $tax_info = get_district({ $self->location_hash }, $method);
254 $self->set('district', $tax_info->{'district'} );
255 my $error = $self->replace;
256 die $error if $error;
258 my %hash = map { $_ => $tax_info->{$_} }
259 qw( district city county state country );
260 $hash{'taxname'} = '';
262 my $old = qsearchs('cust_main_county', \%hash);
264 my $new = new FS::cust_main_county { $old->hash, %$tax_info };
265 warn "updating tax rate for district ".$tax_info->{'district'} if $DEBUG;
266 $error = $new->replace($old);
269 my $new = new FS::cust_main_county $tax_info;
270 warn "creating tax rate for district ".$tax_info->{'district'} if $DEBUG;
271 $error = $new->insert;
273 die $error if $error;
285 L<FS::Record>, schema.html from the base documentation.