alternate address standardization method (TeleAtlas), #13763
[freeside.git] / FS / FS / cust_location.pm
1 package FS::cust_location;
2
3 use strict;
4 use base qw( FS::geocode_Mixin FS::Record );
5 use Locale::Country;
6 use FS::UID qw( dbh );
7 use FS::Record qw( qsearch ); #qsearchs );
8 use FS::Conf;
9 use FS::prospect_main;
10 use FS::cust_main;
11 use FS::cust_main_county;
12
13 =head1 NAME
14
15 FS::cust_location - Object methods for cust_location records
16
17 =head1 SYNOPSIS
18
19   use FS::cust_location;
20
21   $record = new FS::cust_location \%hash;
22   $record = new FS::cust_location { 'column' => 'value' };
23
24   $error = $record->insert;
25
26   $error = $new_record->replace($old_record);
27
28   $error = $record->delete;
29
30   $error = $record->check;
31
32 =head1 DESCRIPTION
33
34 An FS::cust_location object represents a customer location.  FS::cust_location
35 inherits from FS::Record.  The following fields are currently supported:
36
37 =over 4
38
39 =item locationnum
40
41 primary key
42
43 =item custnum
44
45 custnum
46
47 =item address1
48
49 Address line one (required)
50
51 =item address2
52
53 Address line two (optional)
54
55 =item city
56
57 City
58
59 =item county
60
61 County (optional, see L<FS::cust_main_county>)
62
63 =item state
64
65 State (see L<FS::cust_main_county>)
66
67 =item zip
68
69 Zip
70
71 =item country
72
73 Country (see L<FS::cust_main_county>)
74
75 =item geocode
76
77 Geocode
78
79 =item district
80
81 Tax district code (optional)
82
83 =item disabled
84
85 Disabled flag; set to 'Y' to disable the location.
86
87 =back
88
89 =head1 METHODS
90
91 =over 4
92
93 =item new HASHREF
94
95 Creates a new location.  To add the location to the database, see L<"insert">.
96
97 Note that this stores the hash reference, not a distinct copy of the hash it
98 points to.  You can ask the object for a copy with the I<hash> method.
99
100 =cut
101
102 sub table { 'cust_location'; }
103
104 =item insert
105
106 Adds this record to the database.  If there is an error, returns the error,
107 otherwise returns false.
108
109 =cut
110
111 sub insert {
112   my $self = shift;
113   my $error = $self->SUPER::insert(@_);
114
115   #false laziness with cust_main, will go away eventually
116   my $conf = new FS::Conf;
117   if ( !$error and $conf->config('tax_district_method') ) {
118
119     my $queue = new FS::queue {
120       'job' => 'FS::geocode_Mixin::process_district_update'
121     };
122     $error = $queue->insert( ref($self), $self->locationnum );
123
124   }
125
126   $error || '';
127 }
128
129 =item delete
130
131 Delete this record from the database.
132
133 =item replace OLD_RECORD
134
135 Replaces the OLD_RECORD with this one in the database.  If there is an error,
136 returns the error, otherwise returns false.
137
138 =cut
139
140 sub replace {
141   my $self = shift;
142   my $old = shift;
143   $old ||= $self->replace_old;
144   my $error = $self->SUPER::replace($old);
145
146   #false laziness with cust_main, will go away eventually
147   my $conf = new FS::Conf;
148   if ( !$error and $conf->config('tax_district_method') 
149     and $self->get('address1') ne $old->get('address1') ) {
150
151     my $queue = new FS::queue {
152       'job' => 'FS::geocode_Mixin::process_district_update'
153     };
154     $error = $queue->insert( ref($self), $self->locationnum );
155
156   }
157
158   $error || '';
159 }
160
161
162 =item check
163
164 Checks all fields to make sure this is a valid location.  If there is
165 an error, returns the error, otherwise returns false.  Called by the insert
166 and replace methods.
167
168 =cut
169
170 #some false laziness w/cust_main, but since it should eventually lose these
171 #fields anyway...
172 sub check {
173   my $self = shift;
174
175   my $error = 
176     $self->ut_numbern('locationnum')
177     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
178     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
179     || $self->ut_text('address1')
180     || $self->ut_textn('address2')
181     || $self->ut_text('city')
182     || $self->ut_textn('county')
183     || $self->ut_textn('state')
184     || $self->ut_country('country')
185     || $self->ut_zip('zip', $self->country)
186     || $self->ut_coordn('latitude')
187     || $self->ut_coordn('longitude')
188     || $self->ut_enum('coord_auto', [ '', 'Y' ])
189     || $self->ut_enum('addr_clean', [ '', 'Y' ])
190     || $self->ut_alphan('location_type')
191     || $self->ut_textn('location_number')
192     || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
193     || $self->ut_alphan('geocode')
194     || $self->ut_alphan('district')
195   ;
196   return $error if $error;
197
198   $self->set_coord
199     unless $self->latitude && $self->longitude;
200
201   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
202   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
203
204   my $conf = new FS::Conf;
205   return 'Location kind is required'
206     if $self->prospectnum
207     && $conf->exists('prospect_main-alt_address_format')
208     && ! $self->location_kind;
209
210   unless ( qsearch('cust_main_county', {
211     'country' => $self->country,
212     'state'   => '',
213    } ) ) {
214     return "Unknown state/county/country: ".
215       $self->state. "/". $self->county. "/". $self->country
216       unless qsearch('cust_main_county',{
217         'state'   => $self->state,
218         'county'  => $self->county,
219         'country' => $self->country,
220       } );
221   }
222
223   $self->SUPER::check;
224 }
225
226 =item country_full
227
228 Returns this locations's full country name
229
230 =cut
231
232 sub country_full {
233   my $self = shift;
234   code2country($self->country);
235 }
236
237 =item line
238
239 Synonym for location_label
240
241 =cut
242
243 sub line {
244   my $self = shift;
245   $self->location_label;
246 }
247
248 =item has_ship_address
249
250 Returns false since cust_location objects do not have a separate shipping
251 address.
252
253 =cut
254
255 sub has_ship_address {
256   '';
257 }
258
259 =item location_hash
260
261 Returns a list of key/value pairs, with the following keys: address1, address2,
262 city, county, state, zip, country, geocode, location_type, location_number,
263 location_kind.
264
265 =cut
266
267 =item move_to HASHREF
268
269 Takes a hashref with one or more cust_location fields.  Creates a duplicate 
270 of the existing location with all fields set to the values in the hashref.  
271 Moves all packages that use the existing location to the new one, then sets 
272 the "disabled" flag on the old location.  Returns nothing on success, an 
273 error message on error.
274
275 =cut
276
277 sub move_to {
278   my $old = shift;
279   my $hashref = shift;
280
281   local $SIG{HUP} = 'IGNORE';
282   local $SIG{INT} = 'IGNORE';
283   local $SIG{QUIT} = 'IGNORE';
284   local $SIG{TERM} = 'IGNORE';
285   local $SIG{TSTP} = 'IGNORE';
286   local $SIG{PIPE} = 'IGNORE';
287
288   my $oldAutoCommit = $FS::UID::AutoCommit;
289   local $FS::UID::AutoCommit = 0;
290   my $dbh = dbh;
291   my $error = '';
292
293   my $new = FS::cust_location->new({
294       $old->location_hash,
295       'custnum'     => $old->custnum,
296       'prospectnum' => $old->prospectnum,
297       %$hashref
298     });
299   $error = $new->insert;
300   if ( $error ) {
301     $dbh->rollback if $oldAutoCommit;
302     return "Error creating location: $error";
303   }
304
305   my @pkgs = qsearch('cust_pkg', { 
306       'locationnum' => $old->locationnum,
307       'cancel' => '' 
308     });
309   foreach my $cust_pkg (@pkgs) {
310     $error = $cust_pkg->change(
311       'locationnum' => $new->locationnum,
312       'keep_dates'  => 1
313     );
314     if ( $error and not ref($error) ) {
315       $dbh->rollback if $oldAutoCommit;
316       return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
317     }
318   }
319
320   $old->disabled('Y');
321   $error = $old->replace;
322   if ( $error ) {
323     $dbh->rollback if $oldAutoCommit;
324     return "Error disabling old location: $error";
325   }
326
327   $dbh->commit if $oldAutoCommit;
328   return;
329 }
330
331 =item alternize
332
333 Attempts to parse data for location_type and location_number from address1
334 and address2.
335
336 =cut
337
338 sub alternize {
339   my $self = shift;
340
341   return '' if $self->get('location_type')
342             || $self->get('location_number');
343
344   my %parse;
345   if ( 1 ) { #ikano, switch on via config
346     { no warnings 'void';
347       eval { 'use FS::part_export::ikano;' };
348       die $@ if $@;
349     }
350     %parse = FS::part_export::ikano->location_types_parse;
351   } else {
352     %parse = (); #?
353   }
354
355   foreach my $from ('address1', 'address2') {
356     foreach my $parse ( keys %parse ) {
357       my $value = $self->get($from);
358       if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
359         $self->set('location_type', $parse{$parse});
360         $self->set('location_number', $2);
361         $self->set($from, $value);
362         return '';
363       }
364     }
365   }
366
367   #nothing matched, no changes
368   $self->get('address2')
369     ? "Can't parse unit type and number from address2"
370     : '';
371 }
372
373 =item dealternize
374
375 Moves data from location_type and location_number to the end of address1.
376
377 =cut
378
379 sub dealternize {
380   my $self = shift;
381
382   #false laziness w/geocode_Mixin.pm::line
383   my $lt = $self->get('location_type');
384   if ( $lt ) {
385
386     my %location_type;
387     if ( 1 ) { #ikano, switch on via config
388       { no warnings 'void';
389         eval { 'use FS::part_export::ikano;' };
390         die $@ if $@;
391       }
392       %location_type = FS::part_export::ikano->location_types;
393     } else {
394       %location_type = (); #?
395     }
396
397     $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
398     $self->location_type('');
399   }
400
401   if ( length($self->location_number) ) {
402     $self->address1( $self->address1. ' '. $self->location_number );
403     $self->location_number('');
404   }
405  
406   '';
407 }
408
409 =back
410
411 =head1 BUGS
412
413 Not yet used for cust_main billing and shipping addresses.
414
415 =head1 SEE ALSO
416
417 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
418 schema.html from the base documentation.
419
420 =cut
421
422 1;
423