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