1 package FS::cust_main::Location;
4 use vars qw( $DEBUG $me @location_fields );
5 use FS::Record qw(qsearch qsearchs);
12 $me = '[FS::cust_main::Location]';
16 # set up accessors for location fields
20 qw( address1 address2 city county state zip country district
21 latitude longitude coord_auto censustract censusyear geocode
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;
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;
40 #debugging shim--probably a performance hit, so remove this at some point
44 if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
45 carp "WARNING: tried to get() location field $field";
48 $self->FS::Record::get($field);
53 FS::cust_main::Location - Location-related methods for cust_main
57 These methods are available on FS::cust_main objects;
65 Returns an L<FS::cust_location> object for the customer's billing address.
71 $self->hashref->{bill_location}
72 ||= FS::cust_location->by_key($self->bill_locationnum);
77 Returns an L<FS::cust_location> object for the customer's service address.
83 $self->hashref->{ship_location}
84 ||= FS::cust_location->by_key($self->ship_locationnum);
89 An alternative way of saying "bill_location or ship_location, depending on
90 if TYPE is 'bill' or 'ship'".
96 return $self->bill_location if $_[0] eq 'bill';
97 return $self->ship_location if $_[0] eq 'ship';
98 die "bad location type '$_[0]'";
107 =item location_fields
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
116 sub location_fields { @location_fields }
120 eval "use FS::contact;
121 use FS::contact_class;
122 use FS::contact_phone;
125 local $FS::cust_location::import = 1;
129 my $tax_prefix = 'bill_';
130 if ( FS::Conf->new->exists('tax-ship_address') ) {
131 $tax_prefix = 'ship_';
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'};
139 if ( !$service_contact_class->classnum ) {
140 $error = $service_contact_class->insert;
141 die "error creating contact class for Service: $error" if $error;
143 my %phone_type = ( # fudge slightly
150 foreach (keys %phone_type) {
151 $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
152 || new FS::phone_type { typename => $phone_type{$_},
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;
161 foreach my $cust_main (qsearch('cust_main', {
162 bill_locationnum => '',
163 address1 => { op=>'!=', value=>'' },
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(
171 map { $_ => $cust_main->get($_) } location_fields(),
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
180 if ( $cust_main->get('ship_address1') ) {
183 foreach (location_fields()) {
184 if ( length($cust_main->get("ship_$_")) and
185 $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
191 $ship_location = FS::cust_location->new(
194 map { $_ => $cust_main->get("ship_$_") } location_fields()
197 } # else it stays equal to $bill_location
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 );
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'),
213 if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
215 warn "customer $custnum has no service contact name; substituting ".
217 $contact->set('last' => $cust_main->get('last'));
218 $contact->set('first' => $cust_main->get('first'));
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'));
225 $error = $contact->insert;
226 die "error migrating service contact for customer $custnum: $error"
229 foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
230 my $phone = $cust_main->get("ship_$_");
232 my $contact_phone = FS::contact_phone->new({
233 'contactnum' => $contact->contactnum,
234 'phonetypenum' => $phone_type{$_}->phonetypenum,
235 FS::contact::_parse_phonestring( $phone )
237 $error = $contact_phone->insert;
238 # die "whose responsible this"
239 die "error migrating service contact phone for customer $custnum: $error"
241 $cust_main->set("ship_$_" => '');
244 $cust_main->set("ship_$_" => '') foreach qw(last first company);
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' => '');
254 if ( $tax_prefix eq 'bill_' ) {
255 $bill_location->set('geocode', $geocode);
256 } elsif ( $tax_prefix eq 'ship_' ) {
257 $ship_location->set('geocode', $geocode);
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'));
266 $error = $bill_location->insert;
267 die "error migrating billing address for customer $custnum: $error"
270 $cust_main->set(bill_locationnum => $bill_location->locationnum);
272 if (!$ship_location->locationnum) {
273 $error = $ship_location->insert;
274 die "error migrating service address for customer $custnum: $error"
278 $cust_main->set(ship_locationnum => $ship_location->locationnum);
280 # Step 3: Wipe the migrated fields and update the cust_main
282 $cust_main->set("ship_$_" => '') foreach location_fields();
283 $cust_main->set($_ => '') foreach location_fields();
285 $error = $cust_main->replace;
286 die "error migrating addresses for customer $custnum: $error"
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 => '' })
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"
300 } #foreach $cust_main
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') ) {
306 foreach my $cust_location (
307 qsearch('cust_location', { 'censustract' => '' })
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",
321 # this is normal; just means it never had a census tract before
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"
329 } # foreach $cust_location
330 FS::upgrade_journal->set_done('cust_location_censustract_repair');