CCH taxes with new customer locations, #21485
[freeside.git] / FS / FS / geocode_Mixin.pm
1 package FS::geocode_Mixin;
2
3 use strict;
4 use vars qw( $DEBUG $me );
5 use Carp;
6 use Locale::Country;
7 use Geo::Coder::Googlev3; #compile time for now, until others are supported
8 use FS::Record qw( qsearchs qsearch );
9 use FS::Conf;
10 use FS::cust_pkg;
11 use FS::cust_location;
12 use FS::cust_tax_location;
13 use FS::part_pkg;
14
15 $DEBUG = 0;
16 $me = '[FS::geocode_Mixin]';
17
18 =head1 NAME
19
20 FS::geocode_Mixin - Mixin class for records that contain address and other
21 location fields.
22
23 =head1 SYNOPSIS
24
25   package FS::some_table;
26   use base ( FS::geocode_Mixin FS::Record );
27
28 =head1 DESCRIPTION
29
30 FS::geocode_Mixin - This is a mixin class for records that contain address
31 and other location fields.
32
33 =head1 METHODS
34
35 =over 4
36
37 =cut
38
39 =item location_hash
40
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.
44
45 =cut
46
47 #geocode dependent on tax-ship_address config
48
49 sub location_hash {
50   my $self = shift;
51   my $prefix = $self->has_ship_address ? 'ship_' : '';
52
53   map { my $method = ($_ eq 'geocode') ? $_ : $prefix.$_;
54         $_ => $self->get($method);
55       }
56       qw( address1 address2 city county state zip country geocode 
57         location_type location_number location_kind );
58 }
59
60 =item location_label [ OPTION => VALUE ... ]
61
62 Returns the label of the service location (see analog in L<FS::cust_location>) for this customer.
63
64 Options are
65
66 =over 4
67
68 =item join_string
69
70 used to separate the address elements (defaults to ', ')
71
72 =item escape_function
73
74 a callback used for escaping the text of the address elements
75
76 =back
77
78 =cut
79
80 sub location_label {
81   my $self = shift;
82   my %opt = @_;
83
84   my $separator = $opt{join_string} || ', ';
85   my $escape = $opt{escape_function} || sub{ shift };
86   my $ds = $opt{double_space} || '  ';
87   my $line = '';
88   my $cydefault = 
89     $opt{'countrydefault'} || FS::Conf->new->config('countrydefault') || 'US';
90   my $prefix = $self->has_ship_address ? 'ship_' : '';
91
92   my $notfirst = 0;
93   foreach (qw ( address1 address2 ) ) {
94     my $method = "$prefix$_";
95     $line .= ($notfirst ? $separator : ''). &$escape($self->$method)
96       if $self->$method;
97     $notfirst++;
98   }
99
100   my $lt = $self->get($prefix.'location_type');
101   if ( $lt ) {
102     my %location_type;
103     if ( 1 ) { #ikano, switch on via config
104       { no warnings 'void';
105         eval { 'use FS::part_export::ikano;' };
106         die $@ if $@;
107       }
108       %location_type = FS::part_export::ikano->location_types;
109     } else {
110       %location_type = (); #?
111     }
112
113     $line .= ' '.&$escape( $location_type{$lt} || $lt );
114   }
115
116   $line .= ' '. &$escape($self->get($prefix.'location_number'))
117     if $self->get($prefix.'location_number');
118
119   $notfirst = 0;
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';
126       $notfirst++;
127     }
128   }
129   $line .= $separator. &$escape(code2country($self->country))
130     if $self->country ne $cydefault;
131
132   $line;
133 }
134
135 =item set_coord [ PREFIX ]
136
137 Look up the coordinates of the location using (currently) the Google Maps
138 API and set the 'latitude' and 'longitude' fields accordingly.
139
140 PREFIX, if specified, will be prepended to all location field names,
141 including latitude and longitude.
142
143 =cut
144
145 sub set_coord {
146   my $self = shift;
147   my $pre = scalar(@_) ? shift : '';
148
149   #my $module = FS::Conf->new->config('geocode_module') || 'Geo::Coder::Googlev3';
150
151   my $geocoder = Geo::Coder::Googlev3->new;
152
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'))
160     );
161   };
162   if ( $@ ) {
163     warn "geocoding error: $@\n";
164     return;
165   }
166
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');
172   }
173
174 }
175
176 =item geocode DATA_VENDOR
177
178 Returns a value for the customer location as encoded by DATA_VENDOR.
179 Currently this only makes sense for "CCH" as DATA_VENDOR.
180
181 =cut
182
183 sub geocode {
184   my ($self, $data_vendor) = (shift, shift);  #always cch for now
185
186   my $geocode = $self->get('geocode');  #XXX only one data_vendor for geocode
187   return $geocode if $geocode;
188
189   if ( $self->isa('FS::cust_main') ) {
190     warn "WARNING: FS::cust_main->geocode deprecated";
191
192     # do the best we can
193     my $m = FS::Conf->new->exists('tax-ship_address') ? 'ship_location'
194                                                       : 'bill_location';
195     my $location = $self->$m or return '';
196     return $location->geocode($data_vendor);
197   }
198
199   my($zip,$plus4) = split /-/, $self->get('zip')
200     if $self->country eq 'US';
201
202   $zip ||= '';
203   $plus4 ||= '';
204   #CCH specific location stuff
205   my $extra_sql = $plus4 ? "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"
206                          : '';
207
208   my @cust_tax_location =
209     qsearch( {
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
214              }
215            );
216   $geocode = $cust_tax_location[0]->geocode
217     if scalar(@cust_tax_location);
218
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;
223
224   $geocode;
225 }
226
227 =item process_district_update CLASS ID
228
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.
232
233 =cut
234
235 sub process_district_update {
236   my $class = shift;
237   my $id = shift;
238
239   eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
240   die $@ if $@;
241   die "$class has no location data" if !$class->can('location_hash');
242
243   my $conf = FS::Conf->new;
244   my $method = $conf->config('tax_district_method')
245     or return; #nothing to do if null
246   my $self = $class->by_key($id) or die "object $id not found";
247
248   # dies on error, fine
249   my $tax_info = get_district({ $self->location_hash }, $method);
250   
251   if ( $tax_info ) {
252     $self->set('district', $tax_info->{'district'} );
253     my $error = $self->replace;
254     die $error if $error;
255
256     my %hash = map { $_ => $tax_info->{$_} } 
257       qw( district city county state country );
258     my $old = qsearchs('cust_main_county', \%hash);
259     if ( $old ) {
260       my $new = new FS::cust_main_county { $old->hash, %$tax_info };
261       warn "updating tax rate for district ".$tax_info->{'district'} if $DEBUG;
262       $error = $new->replace($old);
263     }
264     else {
265       my $new = new FS::cust_main_county $tax_info;
266       warn "creating tax rate for district ".$tax_info->{'district'} if $DEBUG;
267       $error = $new->insert;
268     }
269     die $error if $error;
270
271   }
272   return;
273 }
274
275 =back
276
277 =head1 BUGS
278
279 =head1 SEE ALSO
280
281 L<FS::Record>, schema.html from the base documentation.
282
283 =cut
284
285 1;
286