sales tax districts, #15089
[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   my $prefix =
190    ( FS::Conf->new->exists('tax-ship_address') && $self->has_ship_address )
191    ? 'ship_'
192    : '';
193
194   my($zip,$plus4) = split /-/, $self->get("${prefix}zip")
195     if $self->country eq 'US';
196
197   $zip ||= '';
198   $plus4 ||= '';
199   #CCH specific location stuff
200   my $extra_sql = $plus4 ? "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"
201                          : '';
202
203   my @cust_tax_location =
204     qsearch( {
205                'table'     => 'cust_tax_location', 
206                'hashref'   => { 'zip' => $zip, 'data_vendor' => $data_vendor },
207                'extra_sql' => $extra_sql,
208                'order_by'  => 'ORDER BY plus4hi',#overlapping with distinct ends
209              }
210            );
211   $geocode = $cust_tax_location[0]->geocode
212     if scalar(@cust_tax_location);
213
214   warn "WARNING: customer ". $self->custnum.
215        ": multiple locations for zip ". $self->get("${prefix}zip").
216        "; using arbitrary geocode $geocode\n"
217     if scalar(@cust_tax_location) > 1;
218
219   $geocode;
220 }
221
222 =item process_district_update CLASS ID
223
224 Queueable function to update the tax district code using the selected method 
225 (config 'tax_district_method').  CLASS is either 'FS::cust_main' or 
226 'FS::cust_location'; ID is the key in one of those tables.
227
228 =cut
229
230 sub process_district_update {
231   my $class = shift;
232   my $id = shift;
233
234   eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
235   die $@ if $@;
236   die "$class has no location data" if !$class->can('location_hash');
237
238   my $conf = FS::Conf->new;
239   my $method = $conf->config('tax_district_method')
240     or return; #nothing to do if null
241   my $self = $class->by_key($id) or die "object $id not found";
242
243   # dies on error, fine
244   my $tax_info = get_district({ $self->location_hash }, $method);
245   
246   if ( $tax_info ) {
247     $self->set('district', $tax_info->{'district'} );
248     my $error = $self->replace;
249     die $error if $error;
250
251     my %hash = map { $_ => $tax_info->{$_} } 
252       qw( district city county state country );
253     my $old = qsearchs('cust_main_county', \%hash);
254     if ( $old ) {
255       my $new = new FS::cust_main_county { $old->hash, %$tax_info };
256       warn "updating tax rate for district ".$tax_info->{'district'} if $DEBUG;
257       $error = $new->replace($old);
258     }
259     else {
260       my $new = new FS::cust_main_county $tax_info;
261       warn "creating tax rate for district ".$tax_info->{'district'} if $DEBUG;
262       $error = $new->insert;
263     }
264     die $error if $error;
265
266   }
267   return;
268 }
269
270 =back
271
272 =head1 BUGS
273
274 =head1 SEE ALSO
275
276 L<FS::Record>, schema.html from the base documentation.
277
278 =cut
279
280 1;
281