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