avoid excess memory usage in cust_main location upgrade, #28841
[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 }
73
74 =item ship_location
75
76 Returns an L<FS::cust_location> object for the customer's service address.
77
78 =cut
79
80 sub ship_location {
81   my $self = shift;
82   $self->hashref->{ship_location}
83     ||= FS::cust_location->by_key($self->ship_locationnum);
84 }
85
86 =item location TYPE
87
88 An alternative way of saying "bill_location or ship_location, depending on 
89 if TYPE is 'bill' or 'ship'".
90
91 =cut
92
93 sub location {
94   my $self = shift;
95   return $self->bill_location if $_[0] eq 'bill';
96   return $self->ship_location if $_[0] eq 'ship';
97   die "bad location type '$_[0]'";
98 }
99
100 =back
101
102 =head1 CLASS METHODS
103
104 =over 4
105
106 =item location_fields
107
108 Returns a list of fields found in the location objects.  All of these fields
109 can be read (but not written) by calling them as methods on the 
110 L<FS::cust_main> object (prefixed with 'ship_' for the service address 
111 fields).
112
113 =cut
114
115 sub location_fields { @location_fields }
116
117 sub _upgrade_data {
118   my $class = shift;
119   eval "use FS::contact;
120         use FS::contact_class;
121         use FS::contact_phone;
122         use FS::phone_type";
123
124   local $FS::cust_location::import = 1;
125   local $DEBUG = 0;
126   my $error;
127
128   my $tax_prefix = 'bill_';
129   if ( FS::Conf->new->exists('tax-ship_address') ) {
130     $tax_prefix = 'ship_';
131   }
132
133   # Step 0: set up contact classes and phone types
134   my $service_contact_class = 
135     qsearchs('contact_class', { classname => 'Service'})
136     || new FS::contact_class { classname => 'Service'};
137
138   if ( !$service_contact_class->classnum ) {
139     warn "Creating service contact class.\n";
140     $error = $service_contact_class->insert;
141     die "error creating contact class for Service: $error" if $error;
142   }
143   my %phone_type = ( # fudge slightly
144     daytime => 'Work',
145     night   => 'Home',
146     mobile  => 'Mobile',
147     fax     => 'Fax'
148   );
149   my $w = 10;
150   foreach (keys %phone_type) {
151     $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
152                       || new FS::phone_type  { typename => $phone_type{$_},
153                                                weight   => $w };
154     # just in case someone still doesn't have these
155     if ( !$phone_type{$_}->phonetypenum ) {
156       $error = $phone_type{$_}->insert;
157       die "error creating phone type '$_': $error" if $error;
158     }
159   }
160
161   warn "Migrating customer locations.\n";
162   my $search = FS::Cursor->new('cust_main', { bill_locationnum => '' });
163   while (my $cust_main = $search->fetch) {
164     # Step 1: extract billing and service addresses into cust_location
165     my $custnum = $cust_main->custnum;
166     my $bill_location = FS::cust_location->new(
167       {
168         custnum => $custnum,
169         map { $_ => $cust_main->get($_) } location_fields(),
170       }
171     );
172     $bill_location->set('censustract', '');
173     $bill_location->set('censusyear', '');
174      # properly goes with ship_location; if they're the same, will be set
175      # on ship_location before inserting either one
176     my $ship_location = $bill_location; # until proven otherwise
177
178     if ( $cust_main->get('ship_address1') ) {
179       # detect duplicates
180       my $same = 1;
181       foreach (location_fields()) {
182         if ( length($cust_main->get("ship_$_")) and
183              $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
184           $same = 0;
185         }
186       }
187
188       if ( !$same ) {
189         $ship_location = FS::cust_location->new(
190           {
191             custnum => $custnum,
192             map { $_ => $cust_main->get("ship_$_") } location_fields()
193           }
194         );
195       } # else it stays equal to $bill_location
196
197       # Step 2: Extract shipping address contact fields into contact
198       my %unlike = map { $_ => 1 }
199         grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
200         qw( last first company daytime night fax mobile );
201
202       if ( %unlike ) {
203         # then there IS a service contact
204         my $contact = FS::contact->new({
205           'custnum'     => $custnum,
206           'classnum'    => $service_contact_class->classnum,
207           'locationnum' => $ship_location->locationnum,
208           'last'        => $cust_main->get('ship_last'),
209           'first'       => $cust_main->get('ship_first'),
210         });
211         if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
212         {
213           warn "customer $custnum has no service contact name; substituting ".
214                "customer name\n";
215           $contact->set('last' => $cust_main->get('last'));
216           $contact->set('first' => $cust_main->get('first'));
217         }
218
219         if ( $unlike{'company'} ) {
220           # there's no contact.company field, but keep a record of it
221           $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
222         }
223         $error = $contact->insert;
224         die "error migrating service contact for customer $custnum: $error"
225           if $error;
226
227         foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
228           my $phone = $cust_main->get("ship_$_");
229           next if !$phone;
230           my $contact_phone = FS::contact_phone->new({
231             'contactnum'    => $contact->contactnum,
232             'phonetypenum'  => $phone_type{$_}->phonetypenum,
233             FS::contact::_parse_phonestring( $phone )
234           });
235           $error = $contact_phone->insert;
236           # die "whose responsible this"
237           die "error migrating service contact phone for customer $custnum: $error"
238             if $error;
239           $cust_main->set("ship_$_" => '');
240         }
241
242         $cust_main->set("ship_$_" => '') foreach qw(last first company);
243       } #if %unlike
244     } #if ship_address1
245
246     # special case: should go with whichever location is used to calculate
247     # taxes, because that's the one it originally came from
248     if ( my $geocode = $cust_main->get('geocode') ) {
249       $bill_location->set('geocode' => '');
250       $ship_location->set('geocode' => '');
251
252       if ( $tax_prefix eq 'bill_' ) {
253         $bill_location->set('geocode', $geocode);
254       } elsif ( $tax_prefix eq 'ship_' ) {
255         $ship_location->set('geocode', $geocode);
256       }
257     }
258
259     # this always goes with the ship_location (whether it's the same as
260     # bill_location or not)
261     $ship_location->set('censustract', $cust_main->get('censustract'));
262     $ship_location->set('censusyear',  $cust_main->get('censusyear'));
263
264     $error = $bill_location->insert;
265     die "error migrating billing address for customer $custnum: $error"
266       if $error;
267
268     $cust_main->set(bill_locationnum => $bill_location->locationnum);
269
270     if (!$ship_location->locationnum) {
271       $error = $ship_location->insert;
272       die "error migrating service address for customer $custnum: $error"
273         if $error;
274     }
275
276     $cust_main->set(ship_locationnum => $ship_location->locationnum);
277
278     # Step 3: Wipe the migrated fields and update the cust_main
279
280     $cust_main->set("ship_$_" => '') foreach location_fields();
281     $cust_main->set($_ => '') foreach location_fields();
282
283     $error = $cust_main->replace;
284     die "error migrating addresses for customer $custnum: $error"
285       if $error;
286
287     # Step 4: set packages at the "default service location" to ship_location
288     my $pkg_search =
289       FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' });
290     while (my $cust_pkg = $pkg_search->fetch) {
291       # not a location change
292       $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
293       $error = $cust_pkg->replace;
294       die "error migrating package ".$cust_pkg->pkgnum.": $error"
295         if $error;
296     }
297
298   } #while (my $cust_main...)
299
300   # repair an error in earlier upgrades
301   if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
302        and FS::Conf->new->exists('cust_main-require_censustract') ) {
303
304     foreach my $cust_location (
305       qsearch('cust_location', { 'censustract' => '' })
306     ) {
307       my $custnum = $cust_location->custnum;
308       next if !$custnum; # avoid doing this for prospect locations
309       my $address1 = $cust_location->address1;
310       # find the last history record that had that address
311       my $last_h = qsearchs({
312           table     => 'h_cust_main',
313           extra_sql => " WHERE custnum = $custnum AND address1 = ".
314                         dbh->quote($address1) .
315                         " AND censustract IS NOT NULL",
316           order_by  => " ORDER BY history_date DESC LIMIT 1",
317       });
318       if (!$last_h) {
319         # this is normal; just means it never had a census tract before
320         next;
321       }
322       $cust_location->set('censustract' => $last_h->get('censustract'));
323       $cust_location->set('censusyear'  => $last_h->get('censusyear'));
324       my $error = $cust_location->replace;
325       warn "Error setting census tract for customer #$custnum:\n  $error\n"
326         if $error;
327     } # foreach $cust_location
328     FS::upgrade_journal->set_done('cust_location_censustract_repair');
329   }
330
331 }
332
333 =back
334
335 =cut
336
337 1;