calculate in_transit_payments correctly for partially complete batches, #37193
[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   local $DEBUG = 1;
240
241   eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
242   die $@ if $@;
243   die "$class has no location data" if !$class->can('location_hash');
244
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";
249
250   # dies on error, fine
251   my $tax_info = get_district({ $self->location_hash }, $method);
252   
253   if ( $tax_info ) {
254     $self->set('district', $tax_info->{'district'} );
255     my $error = $self->replace;
256     die $error if $error;
257
258     my %hash = map { $_ => $tax_info->{$_} } 
259       qw( district city county state country );
260     $hash{'taxname'} = '';
261
262     my $old = qsearchs('cust_main_county', \%hash);
263     if ( $old ) {
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);
267     }
268     else {
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;
272     }
273     die $error if $error;
274
275   }
276   return;
277 }
278
279 =back
280
281 =head1 BUGS
282
283 =head1 SEE ALSO
284
285 L<FS::Record>, schema.html from the base documentation.
286
287 =cut
288
289 1;
290