7ffa5ed411263e353c19b993d92498212b55e68c
[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_alphan('location_type')
139     || $self->ut_textn('location_number')
140     || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
141     || $self->ut_alphan('geocode')
142   ;
143   return $error if $error;
144
145   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
146   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
147
148   my $conf = new FS::Conf;
149   return 'Location kind is required'
150     if $self->prospectnum
151     && $conf->exists('prospect_main-alt_address_format')
152     && ! $self->location_kind;
153
154   unless ( qsearch('cust_main_county', {
155     'country' => $self->country,
156     'state'   => '',
157    } ) ) {
158     return "Unknown state/county/country: ".
159       $self->state. "/". $self->county. "/". $self->country
160       unless qsearch('cust_main_county',{
161         'state'   => $self->state,
162         'county'  => $self->county,
163         'country' => $self->country,
164       } );
165   }
166
167   $self->SUPER::check;
168 }
169
170 =item country_full
171
172 Returns this locations's full country name
173
174 =cut
175
176 sub country_full {
177   my $self = shift;
178   code2country($self->country);
179 }
180
181 =item line
182
183 Synonym for location_label
184
185 =cut
186
187 sub line {
188   my $self = shift;
189   $self->location_label;
190 }
191
192 =item has_ship_address
193
194 Returns false since cust_location objects do not have a separate shipping
195 address.
196
197 =cut
198
199 sub has_ship_address {
200   '';
201 }
202
203 =item location_hash
204
205 Returns a list of key/value pairs, with the following keys: address1, address2,
206 city, county, state, zip, country, geocode, location_type, location_number,
207 location_kind.
208
209 =cut
210
211 =item move_to HASHREF
212
213 Takes a hashref with one or more cust_location fields.  Creates a duplicate 
214 of the existing location with all fields set to the values in the hashref.  
215 Moves all packages that use the existing location to the new one, then sets 
216 the "disabled" flag on the old location.  Returns nothing on success, an 
217 error message on error.
218
219 =cut
220
221 sub move_to {
222   my $old = shift;
223   my $hashref = shift;
224
225   local $SIG{HUP} = 'IGNORE';
226   local $SIG{INT} = 'IGNORE';
227   local $SIG{QUIT} = 'IGNORE';
228   local $SIG{TERM} = 'IGNORE';
229   local $SIG{TSTP} = 'IGNORE';
230   local $SIG{PIPE} = 'IGNORE';
231
232   my $oldAutoCommit = $FS::UID::AutoCommit;
233   local $FS::UID::AutoCommit = 0;
234   my $dbh = dbh;
235   my $error = '';
236
237   my $new = FS::cust_location->new({
238       $old->location_hash,
239       'custnum'     => $old->custnum,
240       'prospectnum' => $old->prospectnum,
241       %$hashref
242     });
243   $error = $new->insert;
244   if ( $error ) {
245     $dbh->rollback if $oldAutoCommit;
246     return "Error creating location: $error";
247   }
248
249   my @pkgs = qsearch('cust_pkg', { 
250       'locationnum' => $old->locationnum,
251       'cancel' => '' 
252     });
253   foreach my $cust_pkg (@pkgs) {
254     $error = $cust_pkg->change(
255       'locationnum' => $new->locationnum,
256       'keep_dates'  => 1
257     );
258     if ( $error and not ref($error) ) {
259       $dbh->rollback if $oldAutoCommit;
260       return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
261     }
262   }
263
264   $old->disabled('Y');
265   $error = $old->replace;
266   if ( $error ) {
267     $dbh->rollback if $oldAutoCommit;
268     return "Error disabling old location: $error";
269   }
270
271   $dbh->commit if $oldAutoCommit;
272   return;
273 }
274
275 =item alternize
276
277 Attempts to parse data for location_type and location_number from address1
278 and address2.
279
280 =cut
281
282 sub alternize {
283   my $self = shift;
284
285   return '' if $self->get('location_type')
286             || $self->get('location_number');
287
288   my %parse;
289   if ( 1 ) { #ikano, switch on via config
290     { no warnings 'void';
291       eval { 'use FS::part_export::ikano;' };
292       die $@ if $@;
293     }
294     %parse = FS::part_export::ikano->location_types_parse;
295   } else {
296     %parse = (); #?
297   }
298
299   foreach my $from ('address1', 'address2') {
300     foreach my $parse ( keys %parse ) {
301       my $value = $self->get($from);
302       if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
303         $self->set('location_type', $parse{$parse});
304         $self->set('location_number', $2);
305         $self->set($from, $value);
306         return '';
307       }
308     }
309   }
310
311   #nothing matched, no changes
312   $self->get('address2')
313     ? "Can't parse unit type and number from address2"
314     : '';
315 }
316
317 =item dealternize
318
319 Moves data from location_type and location_number to the end of address1.
320
321 =cut
322
323 sub dealternize {
324   my $self = shift;
325
326   #false laziness w/geocode_Mixin.pm::line
327   my $lt = $self->get('location_type');
328   if ( $lt ) {
329
330     my %location_type;
331     if ( 1 ) { #ikano, switch on via config
332       { no warnings 'void';
333         eval { 'use FS::part_export::ikano;' };
334         die $@ if $@;
335       }
336       %location_type = FS::part_export::ikano->location_types;
337     } else {
338       %location_type = (); #?
339     }
340
341     $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
342     $self->location_type('');
343   }
344
345   if ( length($self->location_number) ) {
346     $self->address1( $self->address1. ' '. $self->location_number );
347     $self->location_number('');
348   }
349  
350   '';
351 }
352
353 =back
354
355 =head1 BUGS
356
357 Not yet used for cust_main billing and shipping addresses.
358
359 =head1 SEE ALSO
360
361 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
362 schema.html from the base documentation.
363
364 =cut
365
366 1;
367