1 package FS::cust_location;
2 use base qw( FS::geocode_Mixin FS::Record );
5 use vars qw( $import );
7 use FS::UID qw( dbh driver_name );
8 use FS::Record qw( qsearch ); #qsearchs );
10 use FS::prospect_main;
12 use FS::cust_main_county;
18 FS::cust_location - Object methods for cust_location records
22 use FS::cust_location;
24 $record = new FS::cust_location \%hash;
25 $record = new FS::cust_location { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::cust_location object represents a customer location. FS::cust_location
38 inherits from FS::Record. The following fields are currently supported:
52 Address line one (required)
56 Address line two (optional)
64 County (optional, see L<FS::cust_main_county>)
68 State (see L<FS::cust_main_county>)
76 Country (see L<FS::cust_main_county>)
84 Tax district code (optional)
88 Disabled flag; set to 'Y' to disable the location.
98 Creates a new location. To add the location to the database, see L<"insert">.
100 Note that this stores the hash reference, not a distinct copy of the hash it
101 points to. You can ask the object for a copy with the I<hash> method.
105 sub table { 'cust_location'; }
109 Adds this record to the database. If there is an error, returns the error,
110 otherwise returns false.
116 my $conf = new FS::Conf;
118 if ( $self->censustract ) {
119 $self->set('censusyear' => $conf->config('census_year') || 2012);
122 my $error = $self->SUPER::insert(@_);
124 #false laziness with cust_main, will go away eventually
125 if ( !$import and !$error and $conf->config('tax_district_method') ) {
127 my $queue = new FS::queue {
128 'job' => 'FS::geocode_Mixin::process_district_update'
130 $error = $queue->insert( ref($self), $self->locationnum );
139 Delete this record from the database.
141 =item replace OLD_RECORD
143 Replaces the OLD_RECORD with this one in the database. If there is an error,
144 returns the error, otherwise returns false.
151 $old ||= $self->replace_old;
152 # the following fields are immutable
153 foreach (qw(address1 address2 city state zip country)) {
154 if ( $self->$_ ne $old->$_ ) {
155 return "can't change cust_location field $_";
159 $self->SUPER::replace($old);
165 Checks all fields to make sure this is a valid location. If there is
166 an error, returns the error, otherwise returns false. Called by the insert
171 #some false laziness w/cust_main, but since it should eventually lose these
175 my $conf = new FS::Conf;
178 $self->ut_numbern('locationnum')
179 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
180 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
181 || $self->ut_text('address1')
182 || $self->ut_textn('address2')
183 || $self->ut_text('city')
184 || $self->ut_textn('county')
185 || $self->ut_textn('state')
186 || $self->ut_country('country')
187 || (!$import && $self->ut_zip('zip', $self->country))
188 || $self->ut_coordn('latitude')
189 || $self->ut_coordn('longitude')
190 || $self->ut_enum('coord_auto', [ '', 'Y' ])
191 || $self->ut_enum('addr_clean', [ '', 'Y' ])
192 || $self->ut_alphan('location_type')
193 || $self->ut_textn('location_number')
194 || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
195 || $self->ut_alphan('geocode')
196 || $self->ut_alphan('district')
197 || $self->ut_numbern('censusyear')
199 return $error if $error;
200 if ( $self->censustract ne '' ) {
201 $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
202 or return "Illegal census tract: ". $self->censustract;
204 $self->censustract("$1.$2");
207 if ( $conf->exists('cust_main-require_address2') and
208 !$self->ship_address2 =~ /\S/ ) {
209 return "Unit # is required";
212 # tricky...we have to allow for the customer to not be inserted yet
213 return "No prospect or customer!" unless $self->prospectnum
215 || $self->get('custnum_pending');
216 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
218 return 'Location kind is required'
219 if $self->prospectnum
220 && $conf->exists('prospect_main-alt_address_format')
221 && ! $self->location_kind;
223 unless ( $import or qsearch('cust_main_county', {
224 'country' => $self->country,
227 return "Unknown state/county/country: ".
228 $self->state. "/". $self->county. "/". $self->country
229 unless qsearch('cust_main_county',{
230 'state' => $self->state,
231 'county' => $self->county,
232 'country' => $self->country,
241 Returns this locations's full country name
247 code2country($self->country);
252 Synonym for location_label
258 $self->location_label;
261 =item has_ship_address
263 Returns false since cust_location objects do not have a separate shipping
268 sub has_ship_address {
274 Returns a list of key/value pairs, with the following keys: address1, address2,
275 city, county, state, zip, country, geocode, location_type, location_number,
280 =item disable_if_unused
282 Sets the "disabled" flag on the location if it is no longer in use as a
283 prospect location, package location, or a customer's billing or default
288 sub disable_if_unused {
291 my $locationnum = $self->locationnum;
292 return '' if FS::cust_main->count('bill_locationnum = '.$locationnum)
293 or FS::cust_main->count('ship_locationnum = '.$locationnum)
294 or FS::contact->count( 'locationnum = '.$locationnum)
295 or FS::cust_pkg->count('cancel IS NULL AND
296 locationnum = '.$locationnum)
298 $self->disabled('Y');
305 Takes a new L<FS::cust_location> object. Moves all packages that use the
306 existing location to the new one, then sets the "disabled" flag on the old
307 location. Returns nothing on success, an error message on error.
315 local $SIG{HUP} = 'IGNORE';
316 local $SIG{INT} = 'IGNORE';
317 local $SIG{QUIT} = 'IGNORE';
318 local $SIG{TERM} = 'IGNORE';
319 local $SIG{TSTP} = 'IGNORE';
320 local $SIG{PIPE} = 'IGNORE';
322 my $oldAutoCommit = $FS::UID::AutoCommit;
323 local $FS::UID::AutoCommit = 0;
327 # prevent this from failing because of pkg_svc quantity limits
328 local( $FS::cust_svc::ignore_quantity ) = 1;
330 if ( !$new->locationnum ) {
331 $error = $new->insert;
333 $dbh->rollback if $oldAutoCommit;
334 return "Error creating location: $error";
338 my @pkgs = qsearch('cust_pkg', {
339 'locationnum' => $old->locationnum,
342 foreach my $cust_pkg (@pkgs) {
343 $error = $cust_pkg->change(
344 'locationnum' => $new->locationnum,
347 if ( $error and not ref($error) ) {
348 $dbh->rollback if $oldAutoCommit;
349 return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
353 $error = $old->disable_if_unused;
355 $dbh->rollback if $oldAutoCommit;
356 return "Error disabling old location: $error";
359 $dbh->commit if $oldAutoCommit;
365 Attempts to parse data for location_type and location_number from address1
373 return '' if $self->get('location_type')
374 || $self->get('location_number');
377 if ( 1 ) { #ikano, switch on via config
378 { no warnings 'void';
379 eval { 'use FS::part_export::ikano;' };
382 %parse = FS::part_export::ikano->location_types_parse;
387 foreach my $from ('address1', 'address2') {
388 foreach my $parse ( keys %parse ) {
389 my $value = $self->get($from);
390 if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
391 $self->set('location_type', $parse{$parse});
392 $self->set('location_number', $2);
393 $self->set($from, $value);
399 #nothing matched, no changes
400 $self->get('address2')
401 ? "Can't parse unit type and number from address2"
407 Moves data from location_type and location_number to the end of address1.
414 #false laziness w/geocode_Mixin.pm::line
415 my $lt = $self->get('location_type');
419 if ( 1 ) { #ikano, switch on via config
420 { no warnings 'void';
421 eval { 'use FS::part_export::ikano;' };
424 %location_type = FS::part_export::ikano->location_types;
426 %location_type = (); #?
429 $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
430 $self->location_type('');
433 if ( length($self->location_number) ) {
434 $self->address1( $self->address1. ' '. $self->location_number );
435 $self->location_number('');
443 Returns the label of the location object, with an optional site ID
444 string (based on the cust_location-label_prefix config option).
451 my $conf = new FS::Conf;
453 my $format = $conf->config('cust_location-label_prefix') || '';
454 my $cust_or_prospect;
455 if ( $self->custnum ) {
456 $cust_or_prospect = FS::cust_main->by_key($self->custnum);
458 elsif ( $self->prospectnum ) {
459 $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum);
462 if ( $format eq 'CoStAg' ) {
463 my $agent = $conf->config('cust_main-custnum-display_prefix',
464 $cust_or_prospect->agentnum)
465 || $cust_or_prospect->agent->agent;
466 # else this location is invalid
467 $prefix = uc( join('',
469 ($self->state =~ /^(..)/),
471 sprintf('%05d', $self->locationnum)
474 elsif ( $self->custnum and
475 $self->locationnum == $cust_or_prospect->ship_locationnum ) {
476 $prefix = 'Default service location';
478 $prefix .= ($opt{join_string} || ': ') if $prefix;
479 $prefix . $self->SUPER::location_label(%opt);
482 =item county_state_county
484 Returns a string consisting of just the county, state and country.
488 sub county_state_country {
490 my $label = $self->country;
491 $label = $self->state.", $label" if $self->state;
492 $label = $self->county." County, $label" if $self->county;
500 =item in_county_sql OPTIONS
502 Returns an SQL expression to test membership in a cust_main_county
503 geographic area. By default, this requires district, city, county,
504 state, and country to match exactly. Pass "ornull => 1" to allow
505 partial matches where some fields are NULL in the cust_main_county
506 record but not in the location.
508 Pass "param => 1" to receive a parameterized expression (rather than
509 one that requires a join to cust_main_county) and a list of parameter
515 # replaces FS::cust_pkg::location_sql
516 my ($class, %opt) = @_;
517 my $ornull = $opt{ornull} ? ' OR ? IS NULL' : '';
518 my $x = $ornull ? 3 : 2;
519 my @fields = (('district') x 3,
525 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
528 "cust_location.district = ? OR ? = '' OR CAST(? AS $text) IS NULL",
529 "cust_location.city = ? OR ? = '' OR CAST(? AS $text) IS NULL",
530 "cust_location.county = ? OR (? = '' AND cust_location.county IS NULL) $ornull",
531 "cust_location.state = ? OR (? = '' AND cust_location.state IS NULL ) $ornull",
532 "cust_location.country = ?"
534 my $sql = join(' AND ', map "($_)\n", @where);
536 return $sql, @fields;
539 # do the substitution here
541 $sql =~ s/\?/cust_main_county.$_/;
542 $sql =~ s/cust_main_county.$_ = ''/cust_main_county.$_ IS NULL/;
552 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
553 schema.html from the base documentation.