095bbe7b8f15d888b16183707059ccc134d82b8b
[freeside.git] / FS / FS / cust_main / Location.pm
1 package FS::cust_main::Location;
2
3 use strict;
4 use vars qw( $DEBUG $me @location_fields );
5 use FS::Record qw(qsearch qsearchs);
6 use FS::UID qw(dbh);
7 use FS::Cursor;
8 use FS::cust_location;
9
10 use Carp qw(carp);
11
12 $DEBUG = 0;
13 $me = '[FS::cust_main::Location]';
14
15 my $init = 0;
16 BEGIN {
17   # set up accessors for location fields
18   if (!$init) {
19     no strict 'refs';
20     @location_fields = 
21       qw( address1 address2 city county state zip country district
22         latitude longitude coord_auto censustract censusyear geocode
23         addr_clean );
24
25     foreach my $f (@location_fields) {
26       *{"FS::cust_main::Location::$f"} = sub {
27         carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
28         shift->bill_location->$f
29       };
30       *{"FS::cust_main::Location::ship_$f"} = sub {
31         carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1);
32         shift->ship_location->$f
33       };
34     }
35     $init++;
36   }
37 }
38
39 #debugging shim--probably a performance hit, so remove this at some point
40 sub get {
41   my $self = shift;
42   my $field = shift;
43   if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
44     carp "WARNING: tried to get() location field $field";
45     $self->$field;
46   }
47   $self->FS::Record::get($field);
48 }
49
50 =head1 NAME
51
52 FS::cust_main::Location - Location-related methods for cust_main
53
54 =head1 DESCRIPTION
55
56 These methods are available on FS::cust_main objects;
57
58 =head1 METHODS
59
60 =over 4
61
62 =item bill_location
63
64 Returns an L<FS::cust_location> object for the customer's billing address.
65
66 =cut
67
68 sub bill_location {
69   my $self = shift;
70   $self->hashref->{bill_location} 
71     ||= FS::cust_location->by_key($self->bill_locationnum)
72     # degraded mode--let the system keep running during upgrades
73     ||  FS::cust_location->new({
74         map { $_ => $self->get($_) } @location_fields
75       })
76 }
77
78 =item ship_location
79
80 Returns an L<FS::cust_location> object for the customer's service address.
81
82 =cut
83
84 sub ship_location {
85   my $self = shift;
86   $self->hashref->{ship_location}
87     ||= FS::cust_location->by_key($self->ship_locationnum)
88     ||  FS::cust_location->new({
89         map { $_ => $self->get('ship_'.$_) || $self->get($_) } @location_fields
90       })
91
92 }
93
94 =item location TYPE
95
96 An alternative way of saying "bill_location or ship_location, depending on 
97 if TYPE is 'bill' or 'ship'".
98
99 =cut
100
101 sub location {
102   my $self = shift;
103   return $self->bill_location if $_[0] eq 'bill';
104   return $self->ship_location if $_[0] eq 'ship';
105   die "bad location type '$_[0]'";
106 }
107
108 =back
109
110 =head1 CLASS METHODS
111
112 =over 4
113
114 =item location_fields
115
116 Returns a list of fields found in the location objects.  All of these fields
117 can be read (but not written) by calling them as methods on the 
118 L<FS::cust_main> object (prefixed with 'ship_' for the service address 
119 fields).
120
121 =cut
122
123 sub location_fields { @location_fields }
124
125 sub _upgrade_data {
126   my $class = shift;
127   my %opt = @_;
128
129   eval "use FS::contact;
130         use FS::contact_class;
131         use FS::contact_phone;
132         use FS::phone_type";
133
134   local $FS::cust_location::import = 1;
135   local $DEBUG = 0;
136   my $error;
137
138   # Step 0: set up contact classes and phone types
139   my $service_contact_class = 
140     qsearchs('contact_class', { classname => 'Service'})
141     || new FS::contact_class { classname => 'Service'};
142
143   if ( !$service_contact_class->classnum ) {
144     warn "Creating service contact class.\n";
145     $error = $service_contact_class->insert;
146     die "error creating contact class for Service: $error" if $error;
147   }
148   my %phone_type = ( # fudge slightly
149     daytime => 'Work',
150     night   => 'Home',
151     mobile  => 'Mobile',
152     fax     => 'Fax'
153   );
154   my $w = 10;
155   foreach (keys %phone_type) {
156     $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
157                       || new FS::phone_type  { typename => $phone_type{$_},
158                                                weight   => $w };
159     # just in case someone still doesn't have these
160     if ( !$phone_type{$_}->phonetypenum ) {
161       $error = $phone_type{$_}->insert;
162       die "error creating phone type '$_': $error" if $error;
163     }
164   }
165   
166   my $num_to_upgrade = FS::cust_main->count('bill_locationnum is null or ship_locationnum is null');
167   my $num_jobs = FS::queue->count('job = \'FS::cust_main::Location::process_upgrade_location\' and status != \'failed\'');
168   if ( $num_to_upgrade > 0 ) {
169     warn "Need to migrate $num_to_upgrade customer locations.\n";
170
171     if ( $opt{queue} ) {
172       if ( $num_jobs > 0 ) {
173         warn "Upgrade already queued.\n";
174       } else {
175         warn "Scheduling upgrade.\n";
176         my $job = FS::queue->new({ job => 'FS::cust_main::Location::process_upgrade_location' });
177         $job->insert;
178       }
179     } else { #do it now
180       process_upgrade_location();
181     }
182
183   }
184   # repair an error in earlier upgrades
185   if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
186        and FS::Conf->new->exists('cust_main-require_censustract') ) {
187
188     foreach my $cust_location (
189       qsearch('cust_location', { 'censustract' => '' })
190     ) {
191       my $custnum = $cust_location->custnum;
192       next if !$custnum; # avoid doing this for prospect locations
193       my $address1 = $cust_location->address1;
194       # find the last history record that had that address
195       my $last_h = qsearchs({
196           table     => 'h_cust_main',
197           extra_sql => " WHERE custnum = $custnum AND address1 = ".
198                         dbh->quote($address1) .
199                         " AND censustract IS NOT NULL",
200           order_by  => " ORDER BY history_date DESC LIMIT 1",
201       });
202       if (!$last_h) {
203         # this is normal; just means it never had a census tract before
204         next;
205       }
206       $cust_location->set('censustract' => $last_h->get('censustract'));
207       $cust_location->set('censusyear'  => $last_h->get('censusyear'));
208       my $error = $cust_location->replace;
209       warn "Error setting census tract for customer #$custnum:\n  $error\n"
210         if $error;
211     } # foreach $cust_location
212     FS::upgrade_journal->set_done('cust_location_censustract_repair');
213   }
214 }
215
216 sub process_upgrade_location {
217   my $class = shift;
218
219   my $dbh = dbh;
220   local $FS::cust_main::import = 1;
221   local $FS::cust_location::import = 1;
222   local $FS::contact::skip_fuzzyfiles = 1;
223   local $FS::UID::AutoCommit = 0;
224
225   my $tax_prefix = 'bill_';
226   if ( FS::Conf->new->exists('tax-ship_address') ) {
227     $tax_prefix = 'ship_';
228   }
229
230   # load some records that were created during the initial upgrade
231   my $service_contact_class = 
232     qsearchs('contact_class', { classname => 'Service'});
233
234   my %phone_type = (
235     daytime => 'Work',
236     night   => 'Home',
237     mobile  => 'Mobile',
238     fax     => 'Fax'
239   );
240   foreach (keys %phone_type) {
241     $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}});
242   }
243
244   my %opt = (
245     tax_prefix            => $tax_prefix,
246     service_contact_class => $service_contact_class,
247     phone_type            => \%phone_type,
248   );
249
250   my $search = FS::Cursor->new('cust_main', { bill_locationnum => '' });
251   while (my $cust_main = $search->fetch) {
252     my $error = $cust_main->upgrade_location(%opt);
253     if ( $error ) {
254       warn "cust#".$cust_main->custnum.": $error\n";
255       $dbh->rollback;
256     } else {
257       # commit as we go
258       $dbh->commit;
259     }
260   }
261 }
262
263 sub upgrade_location { # instance method
264   my $cust_main = shift;
265   my %opt = @_;
266   my $error;
267
268   # Step 1: extract billing and service addresses into cust_location
269   my $custnum = $cust_main->custnum;
270   my $bill_location = FS::cust_location->new(
271     {
272       custnum => $custnum,
273       map { $_ => $cust_main->get($_) } location_fields(),
274     }
275   );
276   $bill_location->set('censustract', '');
277   $bill_location->set('censusyear', '');
278    # properly goes with ship_location; if they're the same, will be set
279    # on ship_location before inserting either one
280   my $ship_location = $bill_location; # until proven otherwise
281
282   if ( $cust_main->get('ship_address1') ) {
283     # detect duplicates
284     my $same = 1;
285     foreach (location_fields()) {
286       if ( length($cust_main->get("ship_$_")) and
287            $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
288         $same = 0;
289       }
290     }
291
292     if ( !$same ) {
293       $ship_location = FS::cust_location->new(
294         {
295           custnum => $custnum,
296           map { $_ => $cust_main->get("ship_$_") } location_fields()
297         }
298       );
299     } # else it stays equal to $bill_location
300
301     # Step 2: Extract shipping address contact fields into contact
302     my %unlike = map { $_ => 1 }
303       grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
304       qw( last first company daytime night fax mobile );
305
306     if ( %unlike ) {
307       # then there IS a service contact
308       my $contact = FS::contact->new({
309         'custnum'     => $custnum,
310         'classnum'    => $opt{service_contact_class}->classnum,
311         'locationnum' => $ship_location->locationnum,
312         'last'        => $cust_main->get('ship_last'),
313         'first'       => $cust_main->get('ship_first'),
314       });
315       if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
316       {
317         warn "customer $custnum has no service contact name; substituting ".
318              "customer name\n";
319         $contact->set('last' => $cust_main->get('last'));
320         $contact->set('first' => $cust_main->get('first'));
321       }
322
323       if ( $unlike{'company'} ) {
324         # there's no contact.company field, but keep a record of it
325         $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
326       }
327       $error = $contact->insert;
328       return "error migrating service contact for customer $custnum: $error"
329         if $error;
330
331       foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
332         my $phone = $cust_main->get("ship_$_");
333         next if !$phone;
334         my $contact_phone = FS::contact_phone->new({
335           'contactnum'    => $contact->contactnum,
336           'phonetypenum'  => $opt{phone_type}->{$_}->phonetypenum,
337           FS::contact::_parse_phonestring( $phone )
338         });
339         $error = $contact_phone->insert;
340         return "error migrating service contact phone for customer $custnum: $error"
341           if $error;
342         $cust_main->set("ship_$_" => '');
343       }
344
345       $cust_main->set("ship_$_" => '') foreach qw(last first company);
346     } #if %unlike
347   } #if ship_address1
348
349   # special case: should go with whichever location is used to calculate
350   # taxes, because that's the one it originally came from
351   if ( my $geocode = $cust_main->get('geocode') ) {
352     $bill_location->set('geocode' => '');
353     $ship_location->set('geocode' => '');
354
355     if ( $opt{tax_prefix} eq 'bill_' ) {
356       $bill_location->set('geocode', $geocode);
357     } elsif ( $opt{tax_prefix} eq 'ship_' ) {
358       $ship_location->set('geocode', $geocode);
359     }
360   }
361
362   # this always goes with the ship_location (whether it's the same as
363   # bill_location or not)
364   $ship_location->set('censustract', $cust_main->get('censustract'));
365   $ship_location->set('censusyear',  $cust_main->get('censusyear'));
366
367   $error = $bill_location->insert;
368   return "error migrating billing address for customer $custnum: $error"
369     if $error;
370
371   $cust_main->set(bill_locationnum => $bill_location->locationnum);
372
373   if (!$ship_location->locationnum) {
374     $error = $ship_location->insert;
375     return "error migrating service address for customer $custnum: $error"
376       if $error;
377   }
378
379   $cust_main->set(ship_locationnum => $ship_location->locationnum);
380
381   # Step 3: Wipe the migrated fields and update the cust_main
382
383   $cust_main->set("ship_$_" => '') foreach location_fields();
384   $cust_main->set($_ => '') foreach location_fields();
385
386   $error = $cust_main->replace;
387   return "error migrating addresses for customer $custnum: $error"
388     if $error;
389
390   # Step 4: set packages at the "default service location" to ship_location
391   my $pkg_search =
392     FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' });
393   while (my $cust_pkg = $pkg_search->fetch) {
394     # not a location change
395     $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
396     $error = $cust_pkg->replace;
397     return "error migrating package ".$cust_pkg->pkgnum.": $error"
398       if $error;
399   }
400   '';
401
402 }
403
404
405 =back
406
407 =cut
408
409 1;