1 package FS::cust_main::Location;
4 use vars qw( $DEBUG $me @location_fields );
5 use FS::Record qw(qsearch qsearchs);
13 $me = '[FS::cust_main::Location]';
17 # set up accessors for location fields
20 @location_fields = qw(
22 address1 address2 city county state zip country
23 district latitude longitude coord_auto censustract censusyear geocode
27 foreach my $f (@location_fields) {
28 *{"FS::cust_main::Location::$f"} = sub {
29 carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
30 my $l = shift->bill_location;
33 *{"FS::cust_main::Location::ship_$f"} = sub {
34 carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1);
35 my $l = shift->ship_location;
43 #debugging shim--probably a performance hit, so remove this at some point
47 if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
48 carp "WARNING: tried to get() location field $field";
51 $self->FS::Record::get($field);
56 FS::cust_main::Location - Location-related methods for cust_main
60 These methods are available on FS::cust_main objects;
68 Returns an L<FS::cust_location> object for the customer's billing address.
74 $self->hashref->{bill_location}
75 ||= FS::cust_location->by_key($self->bill_locationnum)
76 # degraded mode--let the system keep running during upgrades
77 || ( $self->get('address1')
78 && FS::cust_location->new({
79 map { $_ => $self->get($_) } @location_fields
86 Returns an L<FS::cust_location> object for the customer's service address.
92 $self->hashref->{ship_location}
93 ||= FS::cust_location->by_key($self->ship_locationnum)
94 # degraded mode--let the system keep running during upgrades
95 || ( $self->get('ship_address1')
96 ? FS::cust_location->new({
97 map { $_ => $self->get('ship_'.$_) } @location_fields
99 : $self->get('address1')
100 ? FS::cust_location->new({
101 map { $_ => $self->get($_) } @location_fields
110 An alternative way of saying "bill_location or ship_location, depending on
111 if TYPE is 'bill' or 'ship'".
117 return $self->bill_location if $_[0] eq 'bill';
118 return $self->ship_location if $_[0] eq 'ship';
119 die "bad location type '$_[0]'";
128 =item location_fields
130 Returns a list of fields found in the location objects. All of these fields
131 can be read (but not written) by calling them as methods on the
132 L<FS::cust_main> object (prefixed with 'ship_' for the service address
137 sub location_fields { @location_fields }
143 eval "use FS::contact;
144 use FS::contact_class;
145 use FS::contact_phone;
148 local $FS::cust_location::import = 1;
152 # Step 0: set up contact classes and phone types
153 my $service_contact_class =
154 qsearchs('contact_class', { classname => 'Service'})
155 || new FS::contact_class { classname => 'Service'};
157 if ( !$service_contact_class->classnum ) {
158 warn "Creating service contact class.\n";
159 $error = $service_contact_class->insert;
160 die "error creating contact class for Service: $error" if $error;
162 my %phone_type = ( # fudge slightly
169 foreach (keys %phone_type) {
170 $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
171 || new FS::phone_type { typename => $phone_type{$_},
173 # just in case someone still doesn't have these
174 if ( !$phone_type{$_}->phonetypenum ) {
175 $error = $phone_type{$_}->insert;
176 die "error creating phone type '$_': $error" if $error;
180 my $num_to_upgrade = FS::cust_main->count('bill_locationnum is null or ship_locationnum is null');
181 my $num_jobs = FS::queue->count('job = \'FS::cust_main::Location::process_upgrade_location\' and status != \'failed\'');
182 if ( $num_to_upgrade > 0 ) {
183 warn "Need to migrate $num_to_upgrade customer locations.\n";
186 if ( $num_jobs > 0 ) {
187 warn "Upgrade already queued.\n";
189 warn "Scheduling upgrade.\n";
190 my $job = FS::queue->new({ job => 'FS::cust_main::Location::process_upgrade_location' });
194 process_upgrade_location();
199 # repair an error in earlier upgrades
200 if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
201 and FS::Conf->new->exists('cust_main-require_censustract') ) {
203 foreach my $cust_location (
204 qsearch('cust_location', { 'censustract' => '' })
206 my $custnum = $cust_location->custnum;
207 next if !$custnum; # avoid doing this for prospect locations
208 my $address1 = $cust_location->address1;
209 # find the last history record that had that address
210 my $last_h = qsearchs({
211 table => 'h_cust_main',
212 extra_sql => " WHERE custnum = $custnum AND address1 = ".
213 dbh->quote($address1) .
214 " AND censustract IS NOT NULL",
215 order_by => " ORDER BY history_date DESC LIMIT 1",
218 # this is normal; just means it never had a census tract before
221 $cust_location->set('censustract' => $last_h->get('censustract'));
222 $cust_location->set('censusyear' => $last_h->get('censusyear'));
223 my $error = $cust_location->replace;
224 warn "Error setting census tract for customer #$custnum:\n $error\n"
226 } # foreach $cust_location
227 FS::upgrade_journal->set_done('cust_location_censustract_repair');
231 sub process_upgrade_location {
235 local $FS::cust_main::import = 1;
236 local $FS::cust_location::import = 1;
237 local $FS::contact::skip_fuzzyfiles = 1;
238 local $FS::UID::AutoCommit = 0;
240 my $tax_prefix = 'bill_';
241 if ( FS::Conf->new->exists('tax-ship_address') ) {
242 $tax_prefix = 'ship_';
245 # load some records that were created during the initial upgrade
246 my $service_contact_class =
247 qsearchs('contact_class', { classname => 'Service'});
255 foreach (keys %phone_type) {
256 $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}});
260 tax_prefix => $tax_prefix,
261 service_contact_class => $service_contact_class,
262 phone_type => \%phone_type,
265 my $search = FS::Cursor->new('cust_main',
266 { bill_locationnum => '',
267 address1 => { op=>'!=', value=>'' }
269 while (my $cust_main = $search->fetch) {
270 my $error = $cust_main->upgrade_location(%opt);
272 warn "cust#".$cust_main->custnum.": $error\n";
281 sub upgrade_location { # instance method
282 my $cust_main = shift;
286 # Step 1: extract billing and service addresses into cust_location
287 my $custnum = $cust_main->custnum;
288 my $bill_location = FS::cust_location->new(
291 map { $_ => $cust_main->get($_) } location_fields(),
294 $bill_location->set('censustract', '');
295 $bill_location->set('censusyear', '');
296 # properly goes with ship_location; if they're the same, will be set
297 # on ship_location before inserting either one
298 my $ship_location = $bill_location; # until proven otherwise
300 if ( $cust_main->get('ship_address1') ) {
303 foreach (location_fields()) {
304 if ( length($cust_main->get("ship_$_")) and
305 $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
311 $ship_location = FS::cust_location->new(
314 map { $_ => $cust_main->get("ship_$_") } location_fields()
317 } # else it stays equal to $bill_location
319 # Step 2: Extract shipping address contact fields into contact
320 my %unlike = map { $_ => 1 }
321 grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
322 qw( last first company daytime night fax mobile );
325 # then there IS a service contact
326 my $contact = FS::contact->new({
327 'custnum' => $custnum,
328 'classnum' => $opt{service_contact_class}->classnum,
329 'locationnum' => $ship_location->locationnum,
330 'last' => $cust_main->get('ship_last'),
331 'first' => $cust_main->get('ship_first'),
333 if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
335 warn "customer $custnum has no service contact name; substituting ".
337 $contact->set('last' => $cust_main->get('last'));
338 $contact->set('first' => $cust_main->get('first'));
341 if ( $unlike{'company'} ) {
342 # there's no contact.company field, but keep a record of it
343 $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
345 $error = $contact->insert;
346 return "error migrating service contact for customer $custnum: $error"
349 foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
350 my $phone = $cust_main->get("ship_$_");
352 my $contact_phone = FS::contact_phone->new({
353 'contactnum' => $contact->contactnum,
354 'phonetypenum' => $opt{phone_type}->{$_}->phonetypenum,
355 FS::contact::_parse_phonestring( $phone )
357 $error = $contact_phone->insert;
358 return "error migrating service contact phone for customer $custnum: $error"
360 $cust_main->set("ship_$_" => '');
363 $cust_main->set("ship_$_" => '') foreach qw(last first company);
367 # special case: should go with whichever location is used to calculate
368 # taxes, because that's the one it originally came from
369 if ( my $geocode = $cust_main->get('geocode') ) {
370 $bill_location->set('geocode' => '');
371 $ship_location->set('geocode' => '');
373 if ( $opt{tax_prefix} eq 'bill_' ) {
374 $bill_location->set('geocode', $geocode);
375 } elsif ( $opt{tax_prefix} eq 'ship_' ) {
376 $ship_location->set('geocode', $geocode);
380 # this always goes with the ship_location (whether it's the same as
381 # bill_location or not)
382 $ship_location->set('censustract', $cust_main->get('censustract'));
383 $ship_location->set('censusyear', $cust_main->get('censusyear'));
385 $error = $bill_location->insert;
386 return "error migrating billing address for customer $custnum: $error"
389 $cust_main->set(bill_locationnum => $bill_location->locationnum);
391 if (!$ship_location->locationnum) {
392 $error = $ship_location->insert;
393 return "error migrating service address for customer $custnum: $error"
397 $cust_main->set(ship_locationnum => $ship_location->locationnum);
399 # Step 3: Wipe the migrated fields and update the cust_main
401 $cust_main->set("ship_$_" => '') foreach location_fields();
402 $cust_main->set($_ => '') foreach location_fields();
404 $error = $cust_main->replace;
405 return "error migrating addresses for customer $custnum: $error"
408 # Step 4: set packages at the "default service location" to ship_location
410 FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' });
411 while (my $cust_pkg = $pkg_search->fetch) {
412 # not a location change
413 $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
414 $error = $cust_pkg->replace;
415 return "error migrating package ".$cust_pkg->pkgnum.": $error"