Adding line 246 "edit global pockage definitions costs" back in
[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::cust_location;
8
9 use Carp qw(carp);
10
11 $DEBUG = 0;
12 $me = '[FS::cust_main::Location]';
13
14 my $init = 0;
15 BEGIN {
16   # set up accessors for location fields
17   if (!$init) {
18     no strict 'refs';
19     @location_fields = 
20       qw( address1 address2 city county state zip country district
21         latitude longitude coord_auto censustract censusyear geocode
22         addr_clean );
23
24     foreach my $f (@location_fields) {
25       *{"FS::cust_main::Location::$f"} = sub {
26         carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
27         my $l = shift->bill_location;
28         $l ? $l->$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         my $l = shift->ship_location;
33         $l ? $l->$f : '';
34       };
35     }
36     $init++;
37   }
38 }
39
40 #debugging shim--probably a performance hit, so remove this at some point
41 sub get {
42   my $self = shift;
43   my $field = shift;
44   if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
45     carp "WARNING: tried to get() location field $field";
46     $self->$field;
47   }
48   $self->FS::Record::get($field);
49 }
50
51 =head1 NAME
52
53 FS::cust_main::Location - Location-related methods for cust_main
54
55 =head1 DESCRIPTION
56
57 These methods are available on FS::cust_main objects;
58
59 =head1 METHODS
60
61 =over 4
62
63 =item bill_location
64
65 Returns an L<FS::cust_location> object for the customer's billing address.
66
67 =cut
68
69 sub bill_location {
70   my $self = shift;
71   $self->hashref->{bill_location} 
72     ||= FS::cust_location->by_key($self->bill_locationnum);
73 }
74
75 =item ship_location
76
77 Returns an L<FS::cust_location> object for the customer's service address.
78
79 =cut
80
81 sub ship_location {
82   my $self = shift;
83   $self->hashref->{ship_location}
84     ||= FS::cust_location->by_key($self->ship_locationnum);
85 }
86
87 =item location TYPE
88
89 An alternative way of saying "bill_location or ship_location, depending on 
90 if TYPE is 'bill' or 'ship'".
91
92 =cut
93
94 sub location {
95   my $self = shift;
96   return $self->bill_location if $_[0] eq 'bill';
97   return $self->ship_location if $_[0] eq 'ship';
98   die "bad location type '$_[0]'";
99 }
100
101 =back
102
103 =head1 CLASS METHODS
104
105 =over 4
106
107 =item location_fields
108
109 Returns a list of fields found in the location objects.  All of these fields
110 can be read (but not written) by calling them as methods on the 
111 L<FS::cust_main> object (prefixed with 'ship_' for the service address 
112 fields).
113
114 =cut
115
116 sub location_fields { @location_fields }
117
118 sub _upgrade_data {
119   my $class = shift;
120   eval "use FS::contact;
121         use FS::contact_class;
122         use FS::contact_phone;
123         use FS::phone_type";
124
125   local $FS::cust_location::import = 1;
126   local $DEBUG = 0;
127   my $error;
128
129   my $tax_prefix = 'bill_';
130   if ( FS::Conf->new->exists('tax-ship_address') ) {
131     $tax_prefix = 'ship_';
132   }
133
134   # Step 0: set up contact classes and phone types
135   my $service_contact_class = 
136     qsearchs('contact_class', { classname => 'Service'})
137     || new FS::contact_class { classname => 'Service'};
138
139   if ( !$service_contact_class->classnum ) {
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   foreach my $cust_main (qsearch('cust_main', {
162                            bill_locationnum => '',
163                            address1         => { op=>'!=', value=>'' },
164                         }))
165   {
166     # Step 1: extract billing and service addresses into cust_location
167     my $custnum = $cust_main->custnum;
168     my $bill_location = FS::cust_location->new(
169       {
170         custnum => $custnum,
171         map { $_ => $cust_main->get($_) } location_fields(),
172       }
173     );
174     $bill_location->set('censustract', '');
175     $bill_location->set('censusyear', '');
176      # properly goes with ship_location; if they're the same, will be set
177      # on ship_location before inserting either one
178     my $ship_location = $bill_location; # until proven otherwise
179
180     if ( $cust_main->get('ship_address1') ) {
181       # detect duplicates
182       my $same = 1;
183       foreach (location_fields()) {
184         if ( length($cust_main->get("ship_$_")) and
185              $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
186           $same = 0;
187         }
188       }
189
190       if ( !$same ) {
191         $ship_location = FS::cust_location->new(
192           {
193             custnum => $custnum,
194             map { $_ => $cust_main->get("ship_$_") } location_fields()
195           }
196         );
197       } # else it stays equal to $bill_location
198
199       # Step 2: Extract shipping address contact fields into contact
200       my %unlike = map { $_ => 1 }
201         grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
202         qw( last first company daytime night fax mobile );
203
204       if ( %unlike ) {
205         # then there IS a service contact
206         my $contact = FS::contact->new({
207           'custnum'     => $custnum,
208           'classnum'    => $service_contact_class->classnum,
209           'locationnum' => $ship_location->locationnum,
210           'last'        => $cust_main->get('ship_last'),
211           'first'       => $cust_main->get('ship_first'),
212         });
213         if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
214         {
215           warn "customer $custnum has no service contact name; substituting ".
216                "customer name\n";
217           $contact->set('last' => $cust_main->get('last'));
218           $contact->set('first' => $cust_main->get('first'));
219         }
220
221         if ( $unlike{'company'} ) {
222           # there's no contact.company field, but keep a record of it
223           $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
224         }
225         $error = $contact->insert;
226         die "error migrating service contact for customer $custnum: $error"
227           if $error;
228
229         foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
230           my $phone = $cust_main->get("ship_$_");
231           next if !$phone;
232           my $contact_phone = FS::contact_phone->new({
233             'contactnum'    => $contact->contactnum,
234             'phonetypenum'  => $phone_type{$_}->phonetypenum,
235             FS::contact::_parse_phonestring( $phone )
236           });
237           $error = $contact_phone->insert;
238           # die "whose responsible this"
239           die "error migrating service contact phone for customer $custnum: $error"
240             if $error;
241           $cust_main->set("ship_$_" => '');
242         }
243
244         $cust_main->set("ship_$_" => '') foreach qw(last first company);
245       } #if %unlike
246     } #if ship_address1
247
248     # special case: should go with whichever location is used to calculate
249     # taxes, because that's the one it originally came from
250     if ( my $geocode = $cust_main->get('geocode') ) {
251       $bill_location->set('geocode' => '');
252       $ship_location->set('geocode' => '');
253
254       if ( $tax_prefix eq 'bill_' ) {
255         $bill_location->set('geocode', $geocode);
256       } elsif ( $tax_prefix eq 'ship_' ) {
257         $ship_location->set('geocode', $geocode);
258       }
259     }
260
261     # this always goes with the ship_location (whether it's the same as
262     # bill_location or not)
263     $ship_location->set('censustract', $cust_main->get('censustract'));
264     $ship_location->set('censusyear',  $cust_main->get('censusyear'));
265
266     $error = $bill_location->insert;
267     die "error migrating billing address for customer $custnum: $error"
268       if $error;
269
270     $cust_main->set(bill_locationnum => $bill_location->locationnum);
271
272     if (!$ship_location->locationnum) {
273       $error = $ship_location->insert;
274       die "error migrating service address for customer $custnum: $error"
275         if $error;
276     }
277
278     $cust_main->set(ship_locationnum => $ship_location->locationnum);
279
280     # Step 3: Wipe the migrated fields and update the cust_main
281
282     $cust_main->set("ship_$_" => '') foreach location_fields();
283     $cust_main->set($_ => '') foreach location_fields();
284
285     $error = $cust_main->replace;
286     die "error migrating addresses for customer $custnum: $error"
287       if $error;
288
289     # Step 4: set packages at the "default service location" to ship_location
290     foreach my $cust_pkg (
291       qsearch('cust_pkg', { custnum => $custnum, locationnum => '' })  
292     ) {
293       # not a location change
294       $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
295       $error = $cust_pkg->replace;
296       die "error migrating package ".$cust_pkg->pkgnum.": $error"
297         if $error;
298     }
299
300   } #foreach $cust_main
301
302   # repair an error in earlier upgrades
303   if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
304        and FS::Conf->new->exists('cust_main-require_censustract') ) {
305
306     foreach my $cust_location (
307       qsearch('cust_location', { 'censustract' => '' })
308     ) {
309       my $custnum = $cust_location->custnum;
310       next if !$custnum; # avoid doing this for prospect locations
311       my $address1 = $cust_location->address1;
312       # find the last history record that had that address
313       my $last_h = qsearchs({
314           table     => 'h_cust_main',
315           extra_sql => " WHERE custnum = $custnum AND address1 = ".
316                         dbh->quote($address1) .
317                         " AND censustract IS NOT NULL",
318           order_by  => " ORDER BY history_date DESC LIMIT 1",
319       });
320       if (!$last_h) {
321         # this is normal; just means it never had a census tract before
322         next;
323       }
324       $cust_location->set('censustract' => $last_h->get('censustract'));
325       $cust_location->set('censusyear'  => $last_h->get('censusyear'));
326       my $error = $cust_location->replace;
327       warn "Error setting census tract for customer #$custnum:\n  $error\n"
328         if $error;
329     } # foreach $cust_location
330     FS::upgrade_journal->set_done('cust_location_censustract_repair');
331   }
332
333 }
334
335 =back
336
337 =cut
338
339 1;