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_alphan('location_type')
192 || $self->ut_textn('location_number')
193 || $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
194 || $self->ut_alphan('geocode')
195 || $self->ut_alphan('district')
196 || $self->ut_numbern('censusyear')
198 return $error if $error;
199 if ( $self->censustract ne '' ) {
200 $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
201 or return "Illegal census tract: ". $self->censustract;
203 $self->censustract("$1.$2");
206 if ( $conf->exists('cust_main-require_address2') and
207 !$self->ship_address2 =~ /\S/ ) {
208 return "Unit # is required";
212 unless $import || ($self->latitude && $self->longitude);
214 # tricky...we have to allow for the customer to not be inserted yet
215 return "No prospect or customer!" unless $self->prospectnum
217 || $self->get('custnum_pending');
218 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
220 return 'Location kind is required'
221 if $self->prospectnum
222 && $conf->exists('prospect_main-alt_address_format')
223 && ! $self->location_kind;
225 unless ( $import or qsearch('cust_main_county', {
226 'country' => $self->country,
229 return "Unknown state/county/country: ".
230 $self->state. "/". $self->county. "/". $self->country
231 unless qsearch('cust_main_county',{
232 'state' => $self->state,
233 'county' => $self->county,
234 'country' => $self->country,
243 Returns this locations's full country name
249 code2country($self->country);
254 Synonym for location_label
260 $self->location_label;
263 =item has_ship_address
265 Returns false since cust_location objects do not have a separate shipping
270 sub has_ship_address {
276 Returns a list of key/value pairs, with the following keys: address1, address2,
277 city, county, state, zip, country, geocode, location_type, location_number,
282 =item disable_if_unused
284 Sets the "disabled" flag on the location if it is no longer in use as a
285 prospect location, package location, or a customer's billing or default
290 sub disable_if_unused {
293 my $locationnum = $self->locationnum;
294 return '' if FS::cust_main->count('bill_locationnum = '.$locationnum)
295 or FS::cust_main->count('ship_locationnum = '.$locationnum)
296 or FS::contact->count( 'locationnum = '.$locationnum)
297 or FS::cust_pkg->count('cancel IS NULL AND
298 locationnum = '.$locationnum)
300 $self->disabled('Y');
307 Takes a new L<FS::cust_location> object. Moves all packages that use the
308 existing location to the new one, then sets the "disabled" flag on the old
309 location. Returns nothing on success, an error message on error.
317 local $SIG{HUP} = 'IGNORE';
318 local $SIG{INT} = 'IGNORE';
319 local $SIG{QUIT} = 'IGNORE';
320 local $SIG{TERM} = 'IGNORE';
321 local $SIG{TSTP} = 'IGNORE';
322 local $SIG{PIPE} = 'IGNORE';
324 my $oldAutoCommit = $FS::UID::AutoCommit;
325 local $FS::UID::AutoCommit = 0;
329 if ( !$new->locationnum ) {
330 $error = $new->insert;
332 $dbh->rollback if $oldAutoCommit;
333 return "Error creating location: $error";
337 my @pkgs = qsearch('cust_pkg', {
338 'locationnum' => $old->locationnum,
341 foreach my $cust_pkg (@pkgs) {
342 $error = $cust_pkg->change(
343 'locationnum' => $new->locationnum,
346 if ( $error and not ref($error) ) {
347 $dbh->rollback if $oldAutoCommit;
348 return "Error moving pkgnum ".$cust_pkg->pkgnum.": $error";
352 $error = $old->disable_if_unused;
354 $dbh->rollback if $oldAutoCommit;
355 return "Error disabling old location: $error";
358 $dbh->commit if $oldAutoCommit;
364 Attempts to parse data for location_type and location_number from address1
372 return '' if $self->get('location_type')
373 || $self->get('location_number');
376 if ( 1 ) { #ikano, switch on via config
377 { no warnings 'void';
378 eval { 'use FS::part_export::ikano;' };
381 %parse = FS::part_export::ikano->location_types_parse;
386 foreach my $from ('address1', 'address2') {
387 foreach my $parse ( keys %parse ) {
388 my $value = $self->get($from);
389 if ( $value =~ s/(^|\W+)$parse\W+(\w+)\W*$//i ) {
390 $self->set('location_type', $parse{$parse});
391 $self->set('location_number', $2);
392 $self->set($from, $value);
398 #nothing matched, no changes
399 $self->get('address2')
400 ? "Can't parse unit type and number from address2"
406 Moves data from location_type and location_number to the end of address1.
413 #false laziness w/geocode_Mixin.pm::line
414 my $lt = $self->get('location_type');
418 if ( 1 ) { #ikano, switch on via config
419 { no warnings 'void';
420 eval { 'use FS::part_export::ikano;' };
423 %location_type = FS::part_export::ikano->location_types;
425 %location_type = (); #?
428 $self->address1( $self->address1. ' '. $location_type{$lt} || $lt );
429 $self->location_type('');
432 if ( length($self->location_number) ) {
433 $self->address1( $self->address1. ' '. $self->location_number );
434 $self->location_number('');
442 Returns the label of the location object, with an optional site ID
443 string (based on the cust_location-label_prefix config option).
450 my $conf = new FS::Conf;
452 my $format = $conf->config('cust_location-label_prefix') || '';
453 my $cust_or_prospect;
454 if ( $self->custnum ) {
455 $cust_or_prospect = FS::cust_main->by_key($self->custnum);
457 elsif ( $self->prospectnum ) {
458 $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum);
461 if ( $format eq 'CoStAg' ) {
462 my $agent = $conf->config('cust_main-custnum-display_prefix',
463 $cust_or_prospect->agentnum)
464 || $cust_or_prospect->agent->agent;
465 # else this location is invalid
466 $prefix = uc( join('',
468 ($self->state =~ /^(..)/),
470 sprintf('%05d', $self->locationnum)
473 elsif ( $self->custnum and
474 $self->locationnum == $cust_or_prospect->ship_locationnum ) {
475 $prefix = 'Default service location';
477 $prefix .= ($opt{join_string} || ': ') if $prefix;
478 $prefix . $self->SUPER::location_label(%opt);
485 =item in_county_sql OPTIONS
487 Returns an SQL expression to test membership in a cust_main_county
488 geographic area. By default, this requires district, city, county,
489 state, and country to match exactly. Pass "ornull => 1" to allow
490 partial matches where some fields are NULL in the cust_main_county
491 record but not in the location.
493 Pass "param => 1" to receive a parameterized expression (rather than
494 one that requires a join to cust_main_county) and a list of parameter
500 # replaces FS::cust_pkg::location_sql
501 my ($class, %opt) = @_;
502 my $ornull = $opt{ornull} ? ' OR ? IS NULL' : '';
503 my $x = $ornull ? 3 : 2;
504 my @fields = (('district') x 3,
510 my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
513 "cust_location.district = ? OR ? = '' OR CAST(? AS $text) IS NULL",
514 "cust_location.city = ? OR ? = '' OR CAST(? AS $text) IS NULL",
515 "cust_location.county = ? OR (? = '' AND cust_location.county IS NULL) $ornull",
516 "cust_location.state = ? OR (? = '' AND cust_location.state IS NULL ) $ornull",
517 "cust_location.country = ?"
519 my $sql = join(' AND ', map "($_)\n", @where);
521 return $sql, @fields;
524 # do the substitution here
526 $sql =~ s/\?/cust_main_county.$_/;
527 $sql =~ s/cust_main_county.$_ = ''/cust_main_county.$_ IS NULL/;
537 L<FS::cust_main_county>, L<FS::cust_pkg>, L<FS::Record>,
538 schema.html from the base documentation.