add latitude/longitude to prospects, customers and package locations, RT#15539
[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 disabled
80
81 Disabled flag; set to 'Y' to disable the location.
82
83 =back
84
85 =head1 METHODS
86
87 =over 4
88
89 =item new HASHREF
90
91 Creates a new location.  To add the location to the database, see L<"insert">.
92
93 Note that this stores the hash reference, not a distinct copy of the hash it
94 points to.  You can ask the object for a copy with the I<hash> method.
95
96 =cut
97
98 sub table { 'cust_location'; }
99
100 =item insert
101
102 Adds this record to the database.  If there is an error, returns the error,
103 otherwise returns false.
104
105 =item delete
106
107 Delete this record from the database.
108
109 =item replace OLD_RECORD
110
111 Replaces the OLD_RECORD with this one in the database.  If there is an error,
112 returns the error, otherwise returns false.
113
114 =item check
115
116 Checks all fields to make sure this is a valid location.  If there is
117 an error, returns the error, otherwise returns false.  Called by the insert
118 and replace methods.
119
120 =cut
121
122 #some false laziness w/cust_main, but since it should eventually lose these
123 #fields anyway...
124 sub check {
125   my $self = shift;
126
127   my $error = 
128     $self->ut_numbern('locationnum')
129     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
130     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
131     || $self->ut_text('address1')
132     || $self->ut_textn('address2')
133     || $self->ut_text('city')
134     || $self->ut_textn('county')
135     || $self->ut_textn('state')
136     || $self->ut_country('country')
137     || $self->ut_zip('zip', $self->country)
138     || $self->ut_coordn('latitude')
139     || $self->ut_coordn('longitude')
140     || $self->ut_enum('coord_auto', [ '', 'Y' ])
141     || $self->ut_alphan('location_type')
142     || $self->ut_textn('location_number')
143     || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
144     || $self->ut_alphan('geocode')
145   ;
146   return $error if $error;
147
148   $self->set_coord
149     unless $self->latitude && $self->longitude;
150
151   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
152   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
153
154   my $conf = new FS::Conf;
155   return 'Location kind is required'
156     if $self->prospectnum
157     && $conf->exists('prospect_main-alt_address_format')
158     && ! $self->location_kind;
159
160   unless ( qsearch('cust_main_county', {
161     'country' => $self->country,
162     'state'   => '',
163    } ) ) {
164     return "Unknown state/county/country: ".
165       $self->state. "/". $self->county. "/". $self->country
166       unless qsearch('cust_main_county',{
167         'state'   => $self->state,
168         'county'  => $self->county,
169         'country' => $self->country,
170       } );
171   }
172
173   $self->SUPER::check;
174 }
175
176 =item country_full
177
178 Returns this locations's full country name
179
180 =cut
181
182 sub country_full {
183   my $self = shift;
184   code2country($self->country);
185 }
186
187 =item line
188
189 Synonym for location_label
190
191 =cut
192
193 sub line {
194   my $self = shift;
195   $self->location_label;
196 }
197
198 =item has_ship_address
199
200 Returns false since cust_location objects do not have a separate shipping
201 address.
202
203 =cut
204
205 sub has_ship_address {
206   '';
207 }
208
209 =item location_hash
210
211 Returns a list of key/value pairs, with the following keys: address1, address2,
212 city, county, state, zip, country, geocode, location_type, location_number,
213 location_kind.
214
215 =cut
216
217 =item move_to HASHREF
218
219 Takes a hashref with one or more cust_location fields.  Creates a duplicate 
220 of the existing location with all fields set to the values in the hashref.  
221 Moves all packages that use the existing location to the new one, then sets 
222 the "disabled" flag on the old location.  Returns nothing on success, an 
223 error message on error.
224
225 =cut
226
227 sub move_to {
228   my $old = shift;
229   my $hashref = shift;
230
231   local $SIG{HUP} = 'IGNORE';
232   local $SIG{INT} = 'IGNORE';
233   local $SIG{QUIT} = 'IGNORE';
234   local $SIG{TERM} = 'IGNORE';
235   local $SIG{TSTP} = 'IGNORE';
236   local $SIG{PIPE} = 'IGNORE';
237
238   my $oldAutoCommit = $FS::UID::AutoCommit;
239   local $FS::UID::AutoCommit = 0;
240   my $dbh = dbh;
241   my $error = '';
242
243   my $new = FS::cust_location->new({
244       $old->location_hash,
245       'custnum'     => $old->custnum,
246       'prospectnum' => $old->prospectnum,
247       %$hashref
248     });
249   $error = $new->insert;
250   if ( $error ) {
251     $dbh->rollback if $oldAutoCommit;
252     return "Error creating location: $error";
253   }
254
255   my @pkgs = qsearch('cust_pkg', { 
256       'locationnum' => $old->locationnum,
257       'cancel' => '' 
258     });
259   foreach my $cust_pkg (@pkgs) {
260     $error = $cust_pkg->change(
261       'locationnum' => $new->locationnum,
262       'keep_dates'  => 1
263     );
264     if ( $error and not ref($error) ) {
265       $dbh->rollback if $oldAutoCommit;
266       return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
267     }
268   }
269
270   $old->disabled('Y');
271   $error = $old->replace;
272   if ( $error ) {
273     $dbh->rollback if $oldAutoCommit;
274     return "Error disabling old location: $error";
275   }
276
277   $dbh->commit if $oldAutoCommit;
278   return;
279 }
280
281 =item alternize
282
283 Attempts to parse data for location_type and location_number from address1
284 and address2.
285
286 =cut
287
288 sub alternize {
289   my $self = shift;
290
291   return '' if $self->get('location_type')
292             || $self->get('location_number');
293
294   my %parse;
295   if ( 1 ) { #ikano, switch on via config
296     { no warnings 'void';
297       eval { 'use FS::part_export::ikano;' };
298       die $@ if $@;
299     }
300     %parse = FS::part_export::ikano->location_types_parse;
301   } else {
302     %parse = (); #?
303   }
304
305   foreach my $from ('address1', 'address2') {
306     foreach my $parse ( keys %parse ) {
307       my $value = $self->get($from);
308       if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
309         $self->set('location_type', $parse{$parse});
310         $self->set('location_number', $2);
311         $self->set($from, $value);
312         return '';
313       }
314     }
315   }
316
317   #nothing matched, no changes
318   $self->get('address2')
319     ? "Can't parse unit type and number from address2"
320     : '';
321 }
322
323 =item dealternize
324
325 Moves data from location_type and location_number to the end of address1.
326
327 =cut
328
329 sub dealternize {
330   my $self = shift;
331
332   #false laziness w/geocode_Mixin.pm::line
333   my $lt = $self->get('location_type');
334   if ( $lt ) {
335
336     my %location_type;
337     if ( 1 ) { #ikano, switch on via config
338       { no warnings 'void';
339         eval { 'use FS::part_export::ikano;' };
340         die $@ if $@;
341       }
342       %location_type = FS::part_export::ikano->location_types;
343     } else {
344       %location_type = (); #?
345     }
346
347     $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
348     $self->location_type('');
349   }
350
351   if ( length($self->location_number) ) {
352     $self->address1( $self->address1. ' '. $self->location_number );
353     $self->location_number('');
354   }
355  
356   '';
357 }
358
359 =back
360
361 =head1 BUGS
362
363 Not yet used for cust_main billing and shipping addresses.
364
365 =head1 SEE ALSO
366
367 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
368 schema.html from the base documentation.
369
370 =cut
371
372 1;
373