4 use base qw( FS::svc_Domain_Mixin FS::location_Mixin FS::svc_Common );
5 use vars qw( $DEBUG $me @pw_set $conf );
7 use Scalar::Util qw( blessed );
9 use FS::Record qw( qsearch qsearchs dbh );
10 use FS::Msgcat qw(gettext);
15 use FS::cust_location;
17 $me = '[' . __PACKAGE__ . ']';
21 @pw_set = ( 'a'..'k', 'm','n', 'p-z', 'A'..'N', 'P'..'Z' , '2'..'9' );
23 #ask FS::UID to run this stuff for us later
24 $FS::UID::callback{'FS::svc_acct'} = sub {
30 FS::svc_phone - Object methods for svc_phone records
36 $record = new FS::svc_phone \%hash;
37 $record = new FS::svc_phone { 'column' => 'value' };
39 $error = $record->insert;
41 $error = $new_record->replace($old_record);
43 $error = $record->delete;
45 $error = $record->check;
47 $error = $record->suspend;
49 $error = $record->unsuspend;
51 $error = $record->cancel;
55 An FS::svc_phone object represents a phone number. FS::svc_phone inherits
56 from FS::Record. The following fields are currently supported:
78 Optional svcnum from svc_pbx
88 Creates a new phone number. To add the number to the database, see L<"insert">.
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to. You can ask the object for a copy with the I<hash> method.
95 # the new method can be inherited from FS::Record, if a table method is defined
99 'name' => 'Phone number',
100 'sorts' => 'phonenum',
101 'display_weight' => 60,
102 'cancel_weight' => 80,
104 'countrycode' => { label => 'Country code',
106 disable_inventory => 1,
109 'phonenum' => 'Phone number',
110 'pin' => { label => 'Personal Identification Number',
112 disable_inventory => 1,
115 'sip_password' => 'SIP password',
116 'phone_name' => 'Name',
117 'pbxsvc' => { label => 'PBX',
118 type => 'select-svc_pbx.html',
119 disable_inventory => 1,
120 disable_select => 1, #UI wonky, pry works otherwise
125 select_table => 'svc_domain',
126 select_key => 'svcnum',
127 select_label => 'domain',
128 disable_inventory => 1,
131 label => 'E911 location',
132 disable_inventory => 1,
139 sub table { 'svc_phone'; }
141 sub table_dupcheck_fields { ( 'countrycode', 'phonenum' ); }
143 =item search_sql STRING
145 Class method which returns an SQL fragment to search for the given string.
150 my( $class, $string ) = @_;
152 if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
158 my $conf = new FS::Conf;
159 my $ccode = ( $conf->exists('default_phone_countrycode')
160 && $conf->config('default_phone_countrycode')
162 ? $conf->config('default_phone_countrycode')
165 $string =~ s/^$ccode//;
167 $class->search_sql_field('phonenum', $string );
172 Returns the phone number.
178 my $phonenum = $self->phonenum; #XXX format it better
179 my $label = $phonenum;
180 $label .= '@'.$self->domain if $self->domsvc;
181 $label .= ' ('.$self->phone_name.')' if $self->phone_name;
187 Adds this phone number to the database. If there is an error, returns the
188 error, otherwise returns false.
197 warn "[$me] insert called on $self: ". Dumper($self).
198 "\nwith options: ". Dumper(%options);
201 local $SIG{HUP} = 'IGNORE';
202 local $SIG{INT} = 'IGNORE';
203 local $SIG{QUIT} = 'IGNORE';
204 local $SIG{TERM} = 'IGNORE';
205 local $SIG{TSTP} = 'IGNORE';
206 local $SIG{PIPE} = 'IGNORE';
208 my $oldAutoCommit = $FS::UID::AutoCommit;
209 local $FS::UID::AutoCommit = 0;
212 #false laziness w/cust_pkg.pm... move this to location_Mixin? that would
213 #make it more of a base class than a mixin... :)
214 if ( $options{'cust_location'}
215 && ( ! $self->locationnum || $self->locationnum == -1 ) ) {
216 my $error = $options{'cust_location'}->insert;
218 $dbh->rollback if $oldAutoCommit;
219 return "inserting cust_location (transaction rolled back): $error";
221 $self->locationnum( $options{'cust_location'}->locationnum );
223 #what about on-the-fly edits? if the ui supports it?
225 my $error = $self->SUPER::insert(%options);
227 $dbh->rollback if $oldAutoCommit;
231 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
238 Delete this record from the database.
245 local $SIG{HUP} = 'IGNORE';
246 local $SIG{INT} = 'IGNORE';
247 local $SIG{QUIT} = 'IGNORE';
248 local $SIG{TERM} = 'IGNORE';
249 local $SIG{TSTP} = 'IGNORE';
250 local $SIG{PIPE} = 'IGNORE';
252 my $oldAutoCommit = $FS::UID::AutoCommit;
253 local $FS::UID::AutoCommit = 0;
256 foreach my $phone_device ( $self->phone_device ) {
257 my $error = $phone_device->delete;
259 $dbh->rollback if $oldAutoCommit;
264 my $error = $self->SUPER::delete;
266 $dbh->rollback if $oldAutoCommit;
270 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275 # the delete method can be inherited from FS::Record
277 =item replace OLD_RECORD
279 Replaces the OLD_RECORD with this one in the database. If there is an error,
280 returns the error, otherwise returns false.
287 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
294 warn "[$me] replacing $old with $new\n".
295 "\nwith options: ". Dumper(%options);
298 local $SIG{HUP} = 'IGNORE';
299 local $SIG{INT} = 'IGNORE';
300 local $SIG{QUIT} = 'IGNORE';
301 local $SIG{TERM} = 'IGNORE';
302 local $SIG{TSTP} = 'IGNORE';
303 local $SIG{PIPE} = 'IGNORE';
305 my $oldAutoCommit = $FS::UID::AutoCommit;
306 local $FS::UID::AutoCommit = 0;
309 #false laziness w/cust_pkg.pm... move this to location_Mixin? that would
310 #make it more of a base class than a mixin... :)
311 if ( $options{'cust_location'}
312 && ( ! $new->locationnum || $new->locationnum == -1 ) ) {
313 my $error = $options{'cust_location'}->insert;
315 $dbh->rollback if $oldAutoCommit;
316 return "inserting cust_location (transaction rolled back): $error";
318 $new->locationnum( $options{'cust_location'}->locationnum );
320 #what about on-the-fly edits? if the ui supports it?
322 my $error = $new->SUPER::replace($old, %options);
324 $dbh->rollback if $oldAutoCommit;
325 return $error if $error;
328 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
334 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
338 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
342 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
346 Checks all fields to make sure this is a valid phone number. If there is
347 an error, returns the error, otherwise returns false. Called by the insert
352 # the check method should currently be supplied - FS::Record contains some
353 # data checking routines
358 my $conf = new FS::Conf;
360 my $phonenum = $self->phonenum;
361 my $phonenum_check_method;
362 if ( $conf->exists('svc_phone-allow_alpha_phonenum') ) {
363 $phonenum =~ s/\W//g;
364 $phonenum_check_method = 'ut_alpha';
366 $phonenum =~ s/\D//g;
367 $phonenum_check_method = 'ut_number';
369 $self->phonenum($phonenum);
371 $self->locationnum('') if !$self->locationnum || $self->locationnum == -1;
374 $self->ut_numbern('svcnum')
375 || $self->ut_numbern('countrycode')
376 || $self->$phonenum_check_method('phonenum')
377 || $self->ut_anything('sip_password')
378 || $self->ut_numbern('pin')
379 || $self->ut_textn('phone_name')
380 || $self->ut_foreign_keyn('pbxsvc', 'svc_pbx', 'svcnum' )
381 || $self->ut_foreign_keyn('domsvc', 'svc_domain', 'svcnum' )
382 || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
384 return $error if $error;
386 $self->countrycode(1) unless $self->countrycode;
388 unless ( length($self->sip_password) ) {
391 join('', map $pw_set[ int(rand $#pw_set) ], (0..16) )
399 =item _check duplicate
401 Internal method to check for duplicate phone numers.
405 #false laziness w/svc_acct.pm's _check_duplicate.
406 sub _check_duplicate {
409 my $global_unique = $conf->config('global_unique-phonenum') || 'none';
410 return '' if $global_unique eq 'disabled';
415 grep { !$self->svcnum || $_->svcnum != $self->svcnum }
416 qsearch( 'svc_phone', {
417 'countrycode' => $self->countrycode,
418 'phonenum' => $self->phonenum,
421 return gettext('phonenum_in_use')
422 if $global_unique eq 'countrycode+phonenum' && @dup_ccphonenum;
424 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
425 unless ( $part_svc ) {
426 return 'unknown svcpart '. $self->svcpart;
429 if ( @dup_ccphonenum ) {
431 my $exports = FS::part_export::export_info('svc_phone');
432 my %conflict_ccphonenum_svcpart = ( $self->svcpart => 'SELF', );
434 foreach my $part_export ( $part_svc->part_export ) {
436 #this will catch to the same exact export
437 my @svcparts = map { $_->svcpart } $part_export->export_svc;
439 $conflict_ccphonenum_svcpart{$_} = $part_export->exportnum
444 foreach my $dup_ccphonenum ( @dup_ccphonenum ) {
445 my $dup_svcpart = $dup_ccphonenum->cust_svc->svcpart;
446 if ( exists($conflict_ccphonenum_svcpart{$dup_svcpart}) ) {
447 return "duplicate phone number ".
448 $self->countrycode. ' '. $self->phonenum.
449 ": conflicts with svcnum ". $dup_ccphonenum->svcnum.
450 " via exportnum ". $conflict_ccphonenum_svcpart{$dup_svcpart};
462 Checks the supplied PIN against the PIN in the database. Returns true for a
463 sucessful authentication, false if no match.
468 my($self, $check_pin) = @_;
469 length($self->pin) && $check_pin eq $self->pin;
478 #XXX Session-Timeout! holy shit, need rlm_perl to ask for this in realtime
490 my $conf = new FS::Conf;
492 $check{'User-Password'} = $conf->config('svc_phone-radius-default_password');
503 Returns any FS::phone_device records associated with this service.
509 qsearch('phone_device', { 'svcnum' => $self->svcnum } );
512 #override location_Mixin version cause we want to try the cust_pkg location
513 #in between us and cust_main
514 # XXX what to do in the unlinked case??? return a pseudo-object that returns
516 sub cust_location_or_main {
518 return $self->cust_location if $self->locationnum;
519 my $cust_pkg = $self->cust_svc->cust_pkg;
520 $cust_pkg ? $cust_pkg->cust_location_or_main : '';
529 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
530 L<FS::cust_pkg>, schema.html from the base documentation.