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