4 use vars qw( @ISA $DEBUG $me $conf $skip_fuzzyfiles
5 $dir_prefix @shells $usernamemin
6 $usernamemax $passwordmin $passwordmax
7 $username_ampersand $username_letter $username_letterfirst
8 $username_noperiod $username_nounderscore $username_nodash
9 $username_uppercase $username_percent
10 $password_noampersand $password_noexclamation
11 $welcome_template $welcome_from
12 $welcome_subject $welcome_subject_template $welcome_mimetype
13 $warning_template $warning_from $warning_subject $warning_mimetype
16 $radius_password $radius_ip
22 use Crypt::PasswdMD5 1.2;
24 use FS::UID qw( datasrc driver_name );
26 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
27 use FS::Msgcat qw(gettext);
28 use FS::UI::bytecount;
34 use FS::cust_main_invoice;
38 use FS::radius_usergroup;
45 @ISA = qw( FS::svc_Common );
48 $me = '[FS::svc_acct]';
50 #ask FS::UID to run this stuff for us later
51 $FS::UID::callback{'FS::svc_acct'} = sub {
53 $dir_prefix = $conf->config('home');
54 @shells = $conf->config('shells');
55 $usernamemin = $conf->config('usernamemin') || 2;
56 $usernamemax = $conf->config('usernamemax');
57 $passwordmin = $conf->config('passwordmin') || 6;
58 $passwordmax = $conf->config('passwordmax') || 8;
59 $username_letter = $conf->exists('username-letter');
60 $username_letterfirst = $conf->exists('username-letterfirst');
61 $username_noperiod = $conf->exists('username-noperiod');
62 $username_nounderscore = $conf->exists('username-nounderscore');
63 $username_nodash = $conf->exists('username-nodash');
64 $username_uppercase = $conf->exists('username-uppercase');
65 $username_ampersand = $conf->exists('username-ampersand');
66 $username_percent = $conf->exists('username-percent');
67 $password_noampersand = $conf->exists('password-noexclamation');
68 $password_noexclamation = $conf->exists('password-noexclamation');
69 $dirhash = $conf->config('dirhash') || 0;
70 if ( $conf->exists('welcome_email') ) {
71 $welcome_template = new Text::Template (
73 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
74 ) or warn "can't create welcome email template: $Text::Template::ERROR";
75 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
76 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
77 $welcome_subject_template = new Text::Template (
79 SOURCE => $welcome_subject,
80 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
81 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
83 $welcome_template = '';
85 $welcome_subject = '';
86 $welcome_mimetype = '';
88 if ( $conf->exists('warning_email') ) {
89 $warning_template = new Text::Template (
91 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
92 ) or warn "can't create warning email template: $Text::Template::ERROR";
93 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
94 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
95 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
96 $warning_cc = $conf->config('warning_email-cc');
98 $warning_template = '';
100 $warning_subject = '';
101 $warning_mimetype = '';
104 $smtpmachine = $conf->config('smtpmachine');
105 $radius_password = $conf->config('radius-password') || 'Password';
106 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
107 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
110 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
111 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
115 my ( $hashref, $cache ) = @_;
116 if ( $hashref->{'svc_acct_svcnum'} ) {
117 $self->{'_domsvc'} = FS::svc_domain->new( {
118 'svcnum' => $hashref->{'domsvc'},
119 'domain' => $hashref->{'svc_acct_domain'},
120 'catchall' => $hashref->{'svc_acct_catchall'},
127 FS::svc_acct - Object methods for svc_acct records
133 $record = new FS::svc_acct \%hash;
134 $record = new FS::svc_acct { 'column' => 'value' };
136 $error = $record->insert;
138 $error = $new_record->replace($old_record);
140 $error = $record->delete;
142 $error = $record->check;
144 $error = $record->suspend;
146 $error = $record->unsuspend;
148 $error = $record->cancel;
150 %hash = $record->radius;
152 %hash = $record->radius_reply;
154 %hash = $record->radius_check;
156 $domain = $record->domain;
158 $svc_domain = $record->svc_domain;
160 $email = $record->email;
162 $seconds_since = $record->seconds_since($timestamp);
166 An FS::svc_acct object represents an account. FS::svc_acct inherits from
167 FS::svc_Common. The following fields are currently supported:
171 =item svcnum - primary key (assigned automatcially for new accounts)
175 =item _password - generated if blank
177 =item sec_phrase - security phrase
179 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
187 =item dir - set automatically if blank (and uid is not)
191 =item quota - (unimplementd)
193 =item slipip - IP address
203 =item domsvc - svcnum from svc_domain
205 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
207 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
217 Creates a new account. To add the account to the database, see L<"insert">.
224 'longname_plural' => 'Access accounts and mailboxes',
225 'sorts' => [ 'username', 'uid', 'last_login', ],
226 'display_weight' => 10,
227 'cancel_weight' => 50,
229 'dir' => 'Home directory',
232 def_label => 'UID (set to fixed and blank for no UIDs)',
235 'slipip' => 'IP address',
236 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
238 label => 'Access number',
240 select_table => 'svc_acct_pop',
241 select_key => 'popnum',
242 select_label => 'city',
248 disable_default => 1,
255 disable_inventory => 1,
258 '_password' => 'Password',
261 def_label => 'GID (when blank, defaults to UID)',
265 #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file, set to blank for no shell tracking)',
267 def_label=> 'Shell (set to blank for no shell tracking)',
269 select_list => [ $conf->config('shells') ],
270 disable_inventory => 1,
273 'finger' => 'Real name', # (GECOS)',
276 #def_label => 'svcnum from svc_domain',
278 select_table => 'svc_domain',
279 select_key => 'svcnum',
280 select_label => 'domain',
281 disable_inventory => 1,
285 label => 'RADIUS groups',
286 type => 'radius_usergroup_selector',
287 disable_inventory => 1,
290 'seconds' => { label => 'Seconds',
292 disable_inventory => 1,
294 disable_part_svc_column => 1,
296 'upbytes' => { label => 'Upload',
298 disable_inventory => 1,
300 'format' => \&FS::UI::bytecount::display_bytecount,
301 'parse' => \&FS::UI::bytecount::parse_bytecount,
302 disable_part_svc_column => 1,
304 'downbytes' => { label => 'Download',
306 disable_inventory => 1,
308 'format' => \&FS::UI::bytecount::display_bytecount,
309 'parse' => \&FS::UI::bytecount::parse_bytecount,
310 disable_part_svc_column => 1,
312 'totalbytes'=> { label => 'Total up and download',
314 disable_inventory => 1,
316 'format' => \&FS::UI::bytecount::display_bytecount,
317 'parse' => \&FS::UI::bytecount::parse_bytecount,
318 disable_part_svc_column => 1,
320 'seconds_threshold' => { label => 'Seconds threshold',
322 disable_inventory => 1,
324 disable_part_svc_column => 1,
326 'upbytes_threshold' => { label => 'Upload threshold',
328 disable_inventory => 1,
330 'format' => \&FS::UI::bytecount::display_bytecount,
331 'parse' => \&FS::UI::bytecount::parse_bytecount,
332 disable_part_svc_column => 1,
334 'downbytes_threshold' => { label => 'Download threshold',
336 disable_inventory => 1,
338 'format' => \&FS::UI::bytecount::display_bytecount,
339 'parse' => \&FS::UI::bytecount::parse_bytecount,
340 disable_part_svc_column => 1,
342 'totalbytes_threshold'=> { label => 'Total up and download threshold',
344 disable_inventory => 1,
346 'format' => \&FS::UI::bytecount::display_bytecount,
347 'parse' => \&FS::UI::bytecount::parse_bytecount,
348 disable_part_svc_column => 1,
351 label => 'Last login',
355 label => 'Last logout',
362 sub table { 'svc_acct'; }
366 #false laziness with edit/svc_acct.cgi
368 my( $self, $groups ) = @_;
369 if ( ref($groups) eq 'ARRAY' ) {
371 } elsif ( length($groups) ) {
372 [ split(/\s*,\s*/, $groups) ];
381 shift->_lastlog('in', @_);
385 shift->_lastlog('out', @_);
389 my( $self, $op, $time ) = @_;
391 if ( defined($time) ) {
392 warn "$me last_log$op called on svcnum ". $self->svcnum.
393 ' ('. $self->email. "): $time\n"
398 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
402 my $sth = $dbh->prepare( $sql )
403 or die "Error preparing $sql: ". $dbh->errstr;
404 my $rv = $sth->execute($time, $self->svcnum);
405 die "Error executing $sql: ". $sth->errstr
407 die "Can't update last_log$op for svcnum". $self->svcnum
410 $self->{'Hash'}->{"last_log$op"} = $time;
412 $self->getfield("last_log$op");
416 =item search_sql STRING
418 Class method which returns an SQL fragment to search for the given string.
423 my( $class, $string ) = @_;
424 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
425 my( $username, $domain ) = ( $1, $2 );
426 my $q_username = dbh->quote($username);
427 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
429 "svc_acct.username = $q_username AND ( ".
430 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
435 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
437 $class->search_sql_field('slipip', $string ).
439 $class->search_sql_field('username', $string ).
442 $class->search_sql_field('username', $string);
446 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
448 Returns the "username@domain" string for this account.
450 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
460 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
462 Returns a longer string label for this acccount ("Real Name <username@domain>"
463 if available, or "username@domain").
465 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
472 ( $self->finger =~ /\S/ )
473 ? $self->finger. ' <'.$self->label(@_).'>'
477 =item insert [ , OPTION => VALUE ... ]
479 Adds this account to the database. If there is an error, returns the error,
480 otherwise returns false.
482 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
483 defined. An FS::cust_svc record will be created and inserted.
485 The additional field I<usergroup> can optionally be defined; if so it should
486 contain an arrayref of group names. See L<FS::radius_usergroup>.
488 The additional field I<child_objects> can optionally be defined; if so it
489 should contain an arrayref of FS::tablename objects. They will have their
490 svcnum fields set and will be inserted after this record, but before any
491 exports are run. Each element of the array can also optionally be a
492 two-element array reference containing the child object and the name of an
493 alternate field to be filled in with the newly-inserted svcnum, for example
494 C<[ $svc_forward, 'srcsvc' ]>
496 Currently available options are: I<depend_jobnum>
498 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
499 jobnums), all provisioning jobs will have a dependancy on the supplied
500 jobnum(s) (they will not run until the specific job(s) complete(s)).
502 (TODOC: L<FS::queue> and L<freeside-queued>)
504 (TODOC: new exports!)
513 warn "[$me] insert called on $self: ". Dumper($self).
514 "\nwith options: ". Dumper(%options);
517 local $SIG{HUP} = 'IGNORE';
518 local $SIG{INT} = 'IGNORE';
519 local $SIG{QUIT} = 'IGNORE';
520 local $SIG{TERM} = 'IGNORE';
521 local $SIG{TSTP} = 'IGNORE';
522 local $SIG{PIPE} = 'IGNORE';
524 my $oldAutoCommit = $FS::UID::AutoCommit;
525 local $FS::UID::AutoCommit = 0;
528 my $error = $self->check;
529 return $error if $error;
531 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
532 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
533 unless ( $cust_svc ) {
534 $dbh->rollback if $oldAutoCommit;
535 return "no cust_svc record found for svcnum ". $self->svcnum;
537 $self->pkgnum($cust_svc->pkgnum);
538 $self->svcpart($cust_svc->svcpart);
541 $error = $self->_check_duplicate;
543 $dbh->rollback if $oldAutoCommit;
547 # set usage fields and thresholds if unset but set in a package def
548 if ( $self->pkgnum ) {
549 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
550 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
551 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
553 my %values = $part_pkg->usage_valuehash;
554 my $multiplier = $conf->exists('svc_acct-usage_threshold')
555 ? 1 - $conf->config('svc_acct-usage_threshold')/100
558 foreach ( keys %values ) {
559 next if $self->getfield($_);
560 $self->setfield( $_, $values{$_} );
561 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) );
568 $error = $self->SUPER::insert(
569 'jobnums' => \@jobnums,
570 'child_objects' => $self->child_objects,
574 $dbh->rollback if $oldAutoCommit;
578 if ( $self->usergroup ) {
579 foreach my $groupname ( @{$self->usergroup} ) {
580 my $radius_usergroup = new FS::radius_usergroup ( {
581 svcnum => $self->svcnum,
582 groupname => $groupname,
584 my $error = $radius_usergroup->insert;
586 $dbh->rollback if $oldAutoCommit;
592 unless ( $skip_fuzzyfiles ) {
593 $error = $self->queue_fuzzyfiles_update;
595 $dbh->rollback if $oldAutoCommit;
596 return "updating fuzzy search cache: $error";
600 my $cust_pkg = $self->cust_svc->cust_pkg;
603 my $cust_main = $cust_pkg->cust_main;
605 if ( $conf->exists('emailinvoiceautoalways')
606 || $conf->exists('emailinvoiceauto')
607 && ! $cust_main->invoicing_list_emailonly
609 my @invoicing_list = $cust_main->invoicing_list;
610 push @invoicing_list, $self->email;
611 $cust_main->invoicing_list(\@invoicing_list);
616 if ( $welcome_template && $cust_pkg ) {
617 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
621 'custnum' => $self->custnum,
622 'username' => $self->username,
623 'password' => $self->_password,
624 'first' => $cust_main->first,
625 'last' => $cust_main->getfield('last'),
626 'pkg' => $cust_pkg->part_pkg->pkg,
628 my $wqueue = new FS::queue {
629 'svcnum' => $self->svcnum,
630 'job' => 'FS::svc_acct::send_email'
632 my $error = $wqueue->insert(
634 'from' => $welcome_from,
635 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
636 'mimetype' => $welcome_mimetype,
637 'body' => $welcome_template->fill_in( HASH => \%hash, ),
640 $dbh->rollback if $oldAutoCommit;
641 return "error queuing welcome email: $error";
644 if ( $options{'depend_jobnum'} ) {
645 warn "$me depend_jobnum found; adding to welcome email dependancies"
647 if ( ref($options{'depend_jobnum'}) ) {
648 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
649 "to welcome email dependancies"
651 push @jobnums, @{ $options{'depend_jobnum'} };
653 warn "$me adding job $options{'depend_jobnum'} ".
654 "to welcome email dependancies"
656 push @jobnums, $options{'depend_jobnum'};
660 foreach my $jobnum ( @jobnums ) {
661 my $error = $wqueue->depend_insert($jobnum);
663 $dbh->rollback if $oldAutoCommit;
664 return "error queuing welcome email job dependancy: $error";
674 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
680 Deletes this account from the database. If there is an error, returns the
681 error, otherwise returns false.
683 The corresponding FS::cust_svc record will be deleted as well.
685 (TODOC: new exports!)
692 return "can't delete system account" if $self->_check_system;
694 return "Can't delete an account which is a (svc_forward) source!"
695 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
697 return "Can't delete an account which is a (svc_forward) destination!"
698 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
700 return "Can't delete an account with (svc_www) web service!"
701 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
703 # what about records in session ? (they should refer to history table)
705 local $SIG{HUP} = 'IGNORE';
706 local $SIG{INT} = 'IGNORE';
707 local $SIG{QUIT} = 'IGNORE';
708 local $SIG{TERM} = 'IGNORE';
709 local $SIG{TSTP} = 'IGNORE';
710 local $SIG{PIPE} = 'IGNORE';
712 my $oldAutoCommit = $FS::UID::AutoCommit;
713 local $FS::UID::AutoCommit = 0;
716 foreach my $cust_main_invoice (
717 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
719 unless ( defined($cust_main_invoice) ) {
720 warn "WARNING: something's wrong with qsearch";
723 my %hash = $cust_main_invoice->hash;
724 $hash{'dest'} = $self->email;
725 my $new = new FS::cust_main_invoice \%hash;
726 my $error = $new->replace($cust_main_invoice);
728 $dbh->rollback if $oldAutoCommit;
733 foreach my $svc_domain (
734 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
736 my %hash = new FS::svc_domain->hash;
737 $hash{'catchall'} = '';
738 my $new = new FS::svc_domain \%hash;
739 my $error = $new->replace($svc_domain);
741 $dbh->rollback if $oldAutoCommit;
746 my $error = $self->SUPER::delete;
748 $dbh->rollback if $oldAutoCommit;
752 foreach my $radius_usergroup (
753 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
755 my $error = $radius_usergroup->delete;
757 $dbh->rollback if $oldAutoCommit;
762 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
766 =item replace OLD_RECORD
768 Replaces OLD_RECORD with this one in the database. If there is an error,
769 returns the error, otherwise returns false.
771 The additional field I<usergroup> can optionally be defined; if so it should
772 contain an arrayref of group names. See L<FS::radius_usergroup>.
778 my ( $new, $old ) = ( shift, shift );
780 warn "$me replacing $old with $new\n" if $DEBUG;
782 # We absolutely have to have an old vs. new record to make this work.
783 if (!defined($old)) {
784 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
787 return "can't modify system account" if $old->_check_system;
790 #no warnings 'numeric'; #alas, a 5.006-ism
793 foreach my $xid (qw( uid gid )) {
795 return "Can't change $xid!"
796 if ! $conf->exists("svc_acct-edit_$xid")
797 && $old->$xid() != $new->$xid()
798 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
803 #change homdir when we change username
804 $new->setfield('dir', '') if $old->username ne $new->username;
806 local $SIG{HUP} = 'IGNORE';
807 local $SIG{INT} = 'IGNORE';
808 local $SIG{QUIT} = 'IGNORE';
809 local $SIG{TERM} = 'IGNORE';
810 local $SIG{TSTP} = 'IGNORE';
811 local $SIG{PIPE} = 'IGNORE';
813 my $oldAutoCommit = $FS::UID::AutoCommit;
814 local $FS::UID::AutoCommit = 0;
817 # redundant, but so $new->usergroup gets set
818 $error = $new->check;
819 return $error if $error;
821 $old->usergroup( [ $old->radius_groups ] );
823 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
824 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
826 if ( $new->usergroup ) {
827 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
828 my @newgroups = @{$new->usergroup};
829 foreach my $oldgroup ( @{$old->usergroup} ) {
830 if ( grep { $oldgroup eq $_ } @newgroups ) {
831 @newgroups = grep { $oldgroup ne $_ } @newgroups;
834 my $radius_usergroup = qsearchs('radius_usergroup', {
835 svcnum => $old->svcnum,
836 groupname => $oldgroup,
838 my $error = $radius_usergroup->delete;
840 $dbh->rollback if $oldAutoCommit;
841 return "error deleting radius_usergroup $oldgroup: $error";
845 foreach my $newgroup ( @newgroups ) {
846 my $radius_usergroup = new FS::radius_usergroup ( {
847 svcnum => $new->svcnum,
848 groupname => $newgroup,
850 my $error = $radius_usergroup->insert;
852 $dbh->rollback if $oldAutoCommit;
853 return "error adding radius_usergroup $newgroup: $error";
859 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
860 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
861 $error = $new->_check_duplicate;
863 $dbh->rollback if $oldAutoCommit;
868 $error = $new->SUPER::replace($old);
870 $dbh->rollback if $oldAutoCommit;
871 return $error if $error;
874 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
875 $error = $new->queue_fuzzyfiles_update;
877 $dbh->rollback if $oldAutoCommit;
878 return "updating fuzzy search cache: $error";
882 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
886 =item queue_fuzzyfiles_update
888 Used by insert & replace to update the fuzzy search cache
892 sub queue_fuzzyfiles_update {
895 local $SIG{HUP} = 'IGNORE';
896 local $SIG{INT} = 'IGNORE';
897 local $SIG{QUIT} = 'IGNORE';
898 local $SIG{TERM} = 'IGNORE';
899 local $SIG{TSTP} = 'IGNORE';
900 local $SIG{PIPE} = 'IGNORE';
902 my $oldAutoCommit = $FS::UID::AutoCommit;
903 local $FS::UID::AutoCommit = 0;
906 my $queue = new FS::queue {
907 'svcnum' => $self->svcnum,
908 'job' => 'FS::svc_acct::append_fuzzyfiles'
910 my $error = $queue->insert($self->username);
912 $dbh->rollback if $oldAutoCommit;
913 return "queueing job (transaction rolled back): $error";
916 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
924 Suspends this account by calling export-specific suspend hooks. If there is
925 an error, returns the error, otherwise returns false.
927 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
933 return "can't suspend system account" if $self->_check_system;
934 $self->SUPER::suspend;
939 Unsuspends this account by by calling export-specific suspend hooks. If there
940 is an error, returns the error, otherwise returns false.
942 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
948 my %hash = $self->hash;
949 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
950 $hash{_password} = $1;
951 my $new = new FS::svc_acct ( \%hash );
952 my $error = $new->replace($self);
953 return $error if $error;
956 $self->SUPER::unsuspend;
961 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
963 If the B<auto_unset_catchall> configuration option is set, this method will
964 automatically remove any references to the canceled service in the catchall
965 field of svc_domain. This allows packages that contain both a svc_domain and
966 its catchall svc_acct to be canceled in one step.
971 # Only one thing to do at this level
973 foreach my $svc_domain (
974 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
975 if($conf->exists('auto_unset_catchall')) {
976 my %hash = $svc_domain->hash;
977 $hash{catchall} = '';
978 my $new = new FS::svc_domain ( \%hash );
979 my $error = $new->replace($svc_domain);
980 return $error if $error;
982 return "cannot unprovision svc_acct #".$self->svcnum.
983 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
987 $self->SUPER::cancel;
993 Checks all fields to make sure this is a valid service. If there is an error,
994 returns the error, otherwise returns false. Called by the insert and replace
997 Sets any fixed values; see L<FS::part_svc>.
1004 my($recref) = $self->hashref;
1006 my $x = $self->setfixed( $self->_fieldhandlers );
1007 return $x unless ref($x);
1010 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1012 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1015 my $error = $self->ut_numbern('svcnum')
1016 #|| $self->ut_number('domsvc')
1017 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1018 || $self->ut_textn('sec_phrase')
1019 || $self->ut_snumbern('seconds')
1020 || $self->ut_snumbern('upbytes')
1021 || $self->ut_snumbern('downbytes')
1022 || $self->ut_snumbern('totalbytes')
1024 return $error if $error;
1026 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1027 if ( $username_uppercase ) {
1028 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
1029 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1030 $recref->{username} = $1;
1032 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
1033 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1034 $recref->{username} = $1;
1037 if ( $username_letterfirst ) {
1038 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1039 } elsif ( $username_letter ) {
1040 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1042 if ( $username_noperiod ) {
1043 $recref->{username} =~ /\./ and return gettext('illegal_username');
1045 if ( $username_nounderscore ) {
1046 $recref->{username} =~ /_/ and return gettext('illegal_username');
1048 if ( $username_nodash ) {
1049 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1051 unless ( $username_ampersand ) {
1052 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1054 if ( $password_noampersand ) {
1055 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1057 if ( $password_noexclamation ) {
1058 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1060 unless ( $username_percent ) {
1061 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1064 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1065 $recref->{popnum} = $1;
1066 return "Unknown popnum" unless
1067 ! $recref->{popnum} ||
1068 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1070 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1072 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1073 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1075 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1076 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1077 #not all systems use gid=uid
1078 #you can set a fixed gid in part_svc
1080 return "Only root can have uid 0"
1081 if $recref->{uid} == 0
1082 && $recref->{username} !~ /^(root|toor|smtp)$/;
1084 unless ( $recref->{username} eq 'sync' ) {
1085 if ( grep $_ eq $recref->{shell}, @shells ) {
1086 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1088 return "Illegal shell \`". $self->shell. "\'; ".
1089 $conf->dir. "/shells contains: @shells";
1092 $recref->{shell} = '/bin/sync';
1096 $recref->{gid} ne '' ?
1097 return "Can't have gid without uid" : ( $recref->{gid}='' );
1098 #$recref->{dir} ne '' ?
1099 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1100 $recref->{shell} ne '' ?
1101 return "Can't have shell without uid" : ( $recref->{shell}='' );
1104 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1106 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1107 or return "Illegal directory: ". $recref->{dir};
1108 $recref->{dir} = $1;
1109 return "Illegal directory"
1110 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1111 return "Illegal directory"
1112 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1113 unless ( $recref->{dir} ) {
1114 $recref->{dir} = $dir_prefix . '/';
1115 if ( $dirhash > 0 ) {
1116 for my $h ( 1 .. $dirhash ) {
1117 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1119 } elsif ( $dirhash < 0 ) {
1120 for my $h ( reverse $dirhash .. -1 ) {
1121 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1124 $recref->{dir} .= $recref->{username};
1130 # $error = $self->ut_textn('finger');
1131 # return $error if $error;
1132 if ( $self->getfield('finger') eq '' ) {
1133 my $cust_pkg = $self->svcnum
1134 ? $self->cust_svc->cust_pkg
1135 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1137 my $cust_main = $cust_pkg->cust_main;
1138 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1141 $self->getfield('finger') =~
1142 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1143 or return "Illegal finger: ". $self->getfield('finger');
1144 $self->setfield('finger', $1);
1146 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1147 $recref->{quota} = $1;
1149 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1150 if ( $recref->{slipip} eq '' ) {
1151 $recref->{slipip} = '';
1152 } elsif ( $recref->{slipip} eq '0e0' ) {
1153 $recref->{slipip} = '0e0';
1155 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1156 or return "Illegal slipip: ". $self->slipip;
1157 $recref->{slipip} = $1;
1162 #arbitrary RADIUS stuff; allow ut_textn for now
1163 foreach ( grep /^radius_/, fields('svc_acct') ) {
1164 $self->ut_textn($_);
1167 #generate a password if it is blank
1168 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1169 unless ( $recref->{_password} );
1171 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1172 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1173 $recref->{_password} = $1.$3;
1174 #uncomment this to encrypt password immediately upon entry, or run
1175 #bin/crypt_pw in cron to give new users a window during which their
1176 #password is available to techs, for faxing, etc. (also be aware of
1178 #$recref->{password} = $1.
1179 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1181 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1182 $recref->{_password} = $1.$3;
1183 } elsif ( $recref->{_password} eq '*' ) {
1184 $recref->{_password} = '*';
1185 } elsif ( $recref->{_password} eq '!' ) {
1186 $recref->{_password} = '!';
1187 } elsif ( $recref->{_password} eq '!!' ) {
1188 $recref->{_password} = '!!';
1190 #return "Illegal password";
1191 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1192 FS::Msgcat::_gettext('illegal_password_characters').
1193 ": ". $recref->{_password};
1196 $self->SUPER::check;
1201 Internal function to check the username against the list of system usernames
1202 from the I<system_usernames> configuration value. Returns true if the username
1203 is listed on the system username list.
1209 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1210 $conf->config('system_usernames')
1214 =item _check_duplicate
1216 Internal function to check for duplicates usernames, username@domain pairs and
1219 If the I<global_unique-username> configuration value is set to B<username> or
1220 B<username@domain>, enforces global username or username@domain uniqueness.
1222 In all cases, check for duplicate uids and usernames or username@domain pairs
1223 per export and with identical I<svcpart> values.
1227 sub _check_duplicate {
1230 my $global_unique = $conf->config('global_unique-username') || 'none';
1231 return '' if $global_unique eq 'disabled';
1233 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1234 if ( driver_name =~ /^Pg/i ) {
1235 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1237 } elsif ( driver_name =~ /^mysql/i ) {
1238 dbh->do("SELECT * FROM duplicate_lock
1239 WHERE lockname = 'svc_acct'
1241 ) or die dbh->errstr;
1243 die "unknown database ". driver_name.
1244 "; don't know how to lock for duplicate search";
1246 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1248 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1249 unless ( $part_svc ) {
1250 return 'unknown svcpart '. $self->svcpart;
1253 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1254 qsearch( 'svc_acct', { 'username' => $self->username } );
1255 return gettext('username_in_use')
1256 if $global_unique eq 'username' && @dup_user;
1258 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1259 qsearch( 'svc_acct', { 'username' => $self->username,
1260 'domsvc' => $self->domsvc } );
1261 return gettext('username_in_use')
1262 if $global_unique eq 'username@domain' && @dup_userdomain;
1265 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1266 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1267 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1268 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1273 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1274 my $exports = FS::part_export::export_info('svc_acct');
1275 my %conflict_user_svcpart;
1276 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1278 foreach my $part_export ( $part_svc->part_export ) {
1280 #this will catch to the same exact export
1281 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1283 #this will catch to exports w/same exporthost+type ???
1284 #my @other_part_export = qsearch('part_export', {
1285 # 'machine' => $part_export->machine,
1286 # 'exporttype' => $part_export->exporttype,
1288 #foreach my $other_part_export ( @other_part_export ) {
1289 # push @svcparts, map { $_->svcpart }
1290 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1293 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1294 #silly kludge to avoid uninitialized value errors
1295 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1296 ? $exports->{$part_export->exporttype}{'nodomain'}
1298 if ( $nodomain =~ /^Y/i ) {
1299 $conflict_user_svcpart{$_} = $part_export->exportnum
1302 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1307 foreach my $dup_user ( @dup_user ) {
1308 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1309 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1310 return "duplicate username ". $self->username.
1311 ": conflicts with svcnum ". $dup_user->svcnum.
1312 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1316 foreach my $dup_userdomain ( @dup_userdomain ) {
1317 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1318 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1319 return "duplicate username\@domain ". $self->email.
1320 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1321 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1325 foreach my $dup_uid ( @dup_uid ) {
1326 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1327 if ( exists($conflict_user_svcpart{$dup_svcpart})
1328 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1329 return "duplicate uid ". $self->uid.
1330 ": conflicts with svcnum ". $dup_uid->svcnum.
1332 ( $conflict_user_svcpart{$dup_svcpart}
1333 || $conflict_userdomain_svcpart{$dup_svcpart} );
1345 Depriciated, use radius_reply instead.
1350 carp "FS::svc_acct::radius depriciated, use radius_reply";
1351 $_[0]->radius_reply;
1356 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1357 reply attributes of this record.
1359 Note that this is now the preferred method for reading RADIUS attributes -
1360 accessing the columns directly is discouraged, as the column names are
1361 expected to change in the future.
1368 return %{ $self->{'radius_reply'} }
1369 if exists $self->{'radius_reply'};
1374 my($column, $attrib) = ($1, $2);
1375 #$attrib =~ s/_/\-/g;
1376 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1377 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1379 if ( $self->slipip && $self->slipip ne '0e0' ) {
1380 $reply{$radius_ip} = $self->slipip;
1383 if ( $self->seconds !~ /^$/ ) {
1384 $reply{'Session-Timeout'} = $self->seconds;
1392 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1393 check attributes of this record.
1395 Note that this is now the preferred method for reading RADIUS attributes -
1396 accessing the columns directly is discouraged, as the column names are
1397 expected to change in the future.
1404 return %{ $self->{'radius_check'} }
1405 if exists $self->{'radius_check'};
1410 my($column, $attrib) = ($1, $2);
1411 #$attrib =~ s/_/\-/g;
1412 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1413 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1415 my $password = $self->_password;
1416 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1418 my $cust_svc = $self->cust_svc;
1419 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1421 my $cust_pkg = $cust_svc->cust_pkg;
1422 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1423 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1432 This method instructs the object to "snapshot" or freeze RADIUS check and
1433 reply attributes to the current values.
1437 #bah, my english is too broken this morning
1438 #Of note is the "Expiration" attribute, which, for accounts in prepaid packages, is typically defined on-the-fly as the associated packages cust_pkg.bill. (This is used by
1439 #the FS::cust_pkg's replace method to trigger the correct export updates when
1440 #package dates change)
1445 $self->{$_} = { $self->$_() }
1446 foreach qw( radius_reply radius_check );
1450 =item forget_snapshot
1452 This methos instructs the object to forget any previously snapshotted
1453 RADIUS check and reply attributes.
1457 sub forget_snapshot {
1461 foreach qw( radius_reply radius_check );
1465 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1467 Returns the domain associated with this account.
1469 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1476 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1477 my $svc_domain = $self->svc_domain(@_)
1478 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1479 $svc_domain->domain;
1484 Returns the FS::svc_domain record for this account's domain (see
1489 # FS::h_svc_acct has a history-aware svc_domain override
1494 ? $self->{'_domsvc'}
1495 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1500 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1504 #inherited from svc_Common
1506 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1508 Returns an email address associated with the account.
1510 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1517 $self->username. '@'. $self->domain(@_);
1522 Returns an array of FS::acct_snarf records associated with the account.
1523 If the acct_snarf table does not exist or there are no associated records,
1524 an empty list is returned
1530 return () unless dbdef->table('acct_snarf');
1531 eval "use FS::acct_snarf;";
1533 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1536 =item decrement_upbytes OCTETS
1538 Decrements the I<upbytes> field of this record by the given amount. If there
1539 is an error, returns the error, otherwise returns false.
1543 sub decrement_upbytes {
1544 shift->_op_usage('-', 'upbytes', @_);
1547 =item increment_upbytes OCTETS
1549 Increments the I<upbytes> field of this record by the given amount. If there
1550 is an error, returns the error, otherwise returns false.
1554 sub increment_upbytes {
1555 shift->_op_usage('+', 'upbytes', @_);
1558 =item decrement_downbytes OCTETS
1560 Decrements the I<downbytes> field of this record by the given amount. If there
1561 is an error, returns the error, otherwise returns false.
1565 sub decrement_downbytes {
1566 shift->_op_usage('-', 'downbytes', @_);
1569 =item increment_downbytes OCTETS
1571 Increments the I<downbytes> field of this record by the given amount. If there
1572 is an error, returns the error, otherwise returns false.
1576 sub increment_downbytes {
1577 shift->_op_usage('+', 'downbytes', @_);
1580 =item decrement_totalbytes OCTETS
1582 Decrements the I<totalbytes> field of this record by the given amount. If there
1583 is an error, returns the error, otherwise returns false.
1587 sub decrement_totalbytes {
1588 shift->_op_usage('-', 'totalbytes', @_);
1591 =item increment_totalbytes OCTETS
1593 Increments the I<totalbytes> field of this record by the given amount. If there
1594 is an error, returns the error, otherwise returns false.
1598 sub increment_totalbytes {
1599 shift->_op_usage('+', 'totalbytes', @_);
1602 =item decrement_seconds SECONDS
1604 Decrements the I<seconds> field of this record by the given amount. If there
1605 is an error, returns the error, otherwise returns false.
1609 sub decrement_seconds {
1610 shift->_op_usage('-', 'seconds', @_);
1613 =item increment_seconds SECONDS
1615 Increments the I<seconds> field of this record by the given amount. If there
1616 is an error, returns the error, otherwise returns false.
1620 sub increment_seconds {
1621 shift->_op_usage('+', 'seconds', @_);
1629 my %op2condition = (
1630 '-' => sub { my($self, $column, $amount) = @_;
1631 $self->$column - $amount <= 0;
1633 '+' => sub { my($self, $column, $amount) = @_;
1634 $self->$column + $amount > 0;
1637 my %op2warncondition = (
1638 '-' => sub { my($self, $column, $amount) = @_;
1639 my $threshold = $column . '_threshold';
1640 $self->$column - $amount <= $self->$threshold + 0;
1642 '+' => sub { my($self, $column, $amount) = @_;
1643 $self->$column + $amount > 0;
1648 my( $self, $op, $column, $amount ) = @_;
1650 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1651 ' ('. $self->email. "): $op $amount\n"
1654 return '' unless $amount;
1656 local $SIG{HUP} = 'IGNORE';
1657 local $SIG{INT} = 'IGNORE';
1658 local $SIG{QUIT} = 'IGNORE';
1659 local $SIG{TERM} = 'IGNORE';
1660 local $SIG{TSTP} = 'IGNORE';
1661 local $SIG{PIPE} = 'IGNORE';
1663 my $oldAutoCommit = $FS::UID::AutoCommit;
1664 local $FS::UID::AutoCommit = 0;
1667 my $sql = "UPDATE svc_acct SET $column = ".
1668 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1669 " $op ? WHERE svcnum = ?";
1673 my $sth = $dbh->prepare( $sql )
1674 or die "Error preparing $sql: ". $dbh->errstr;
1675 my $rv = $sth->execute($amount, $self->svcnum);
1676 die "Error executing $sql: ". $sth->errstr
1677 unless defined($rv);
1678 die "Can't update $column for svcnum". $self->svcnum
1681 my $action = $op2action{$op};
1683 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1684 ( $action eq 'suspend' && !$self->overlimit
1685 || $action eq 'unsuspend' && $self->overlimit )
1687 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1688 if ($part_export->option('overlimit_groups')) {
1690 my $other = new FS::svc_acct $self->hashref;
1691 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1692 ($self, $part_export->option('overlimit_groups'));
1693 $other->usergroup( $groups );
1694 if ($action eq 'suspend'){
1695 $new = $other; $old = $self;
1697 $new = $self; $old = $other;
1699 my $error = $part_export->export_replace($new, $old);
1700 $error ||= $self->overlimit($action);
1702 $dbh->rollback if $oldAutoCommit;
1703 return "Error replacing radius groups in export, ${op}: $error";
1709 if ( $conf->exists("svc_acct-usage_$action")
1710 && &{$op2condition{$op}}($self, $column, $amount) ) {
1711 #my $error = $self->$action();
1712 my $error = $self->cust_svc->cust_pkg->$action();
1713 # $error ||= $self->overlimit($action);
1715 $dbh->rollback if $oldAutoCommit;
1716 return "Error ${action}ing: $error";
1720 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1721 my $wqueue = new FS::queue {
1722 'svcnum' => $self->svcnum,
1723 'job' => 'FS::svc_acct::reached_threshold',
1728 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1732 my $error = $wqueue->insert(
1733 'svcnum' => $self->svcnum,
1735 'column' => $column,
1739 $dbh->rollback if $oldAutoCommit;
1740 return "Error queuing threshold activity: $error";
1744 warn "$me update successful; committing\n"
1746 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1752 my( $self, $valueref, %options ) = @_;
1754 warn "$me set_usage called for svcnum ". $self->svcnum.
1755 ' ('. $self->email. "): ".
1756 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1759 local $SIG{HUP} = 'IGNORE';
1760 local $SIG{INT} = 'IGNORE';
1761 local $SIG{QUIT} = 'IGNORE';
1762 local $SIG{TERM} = 'IGNORE';
1763 local $SIG{TSTP} = 'IGNORE';
1764 local $SIG{PIPE} = 'IGNORE';
1766 local $FS::svc_Common::noexport_hack = 1;
1767 my $oldAutoCommit = $FS::UID::AutoCommit;
1768 local $FS::UID::AutoCommit = 0;
1773 if ( $options{null} ) {
1774 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1775 qw( seconds upbytes downbytes totalbytes )
1778 foreach my $field (keys %$valueref){
1779 $reset = 1 if $valueref->{$field};
1780 $self->setfield($field, $valueref->{$field});
1781 $self->setfield( $field.'_threshold',
1782 int($self->getfield($field)
1783 * ( $conf->exists('svc_acct-usage_threshold')
1784 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1789 $handyhash{$field} = $self->getfield($field);
1790 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1792 #my $error = $self->replace; #NO! we avoid the call to ->check for
1793 #die $error if $error; #services not explicity changed via the UI
1795 my $sql = "UPDATE svc_acct SET " .
1796 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1797 " WHERE svcnum = ". $self->svcnum;
1802 if (scalar(keys %handyhash)) {
1803 my $sth = $dbh->prepare( $sql )
1804 or die "Error preparing $sql: ". $dbh->errstr;
1805 my $rv = $sth->execute();
1806 die "Error executing $sql: ". $sth->errstr
1807 unless defined($rv);
1808 die "Can't update usage for svcnum ". $self->svcnum
1815 if ($self->overlimit) {
1816 $error = $self->overlimit('unsuspend');
1817 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1818 if ($part_export->option('overlimit_groups')) {
1819 my $old = new FS::svc_acct $self->hashref;
1820 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1821 ($self, $part_export->option('overlimit_groups'));
1822 $old->usergroup( $groups );
1823 $error ||= $part_export->export_replace($self, $old);
1828 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1829 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1832 $dbh->rollback if $oldAutoCommit;
1833 return "Error unsuspending: $error";
1837 warn "$me update successful; committing\n"
1839 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1845 =item recharge HASHREF
1847 Increments usage columns by the amount specified in HASHREF as
1848 column=>amount pairs.
1853 my ($self, $vhash) = @_;
1856 warn "[$me] recharge called on $self: ". Dumper($self).
1857 "\nwith vhash: ". Dumper($vhash);
1860 my $oldAutoCommit = $FS::UID::AutoCommit;
1861 local $FS::UID::AutoCommit = 0;
1865 foreach my $column (keys %$vhash){
1866 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1870 $dbh->rollback if $oldAutoCommit;
1872 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1877 =item is_rechargeable
1879 Returns true if this svc_account can be "recharged" and false otherwise.
1883 sub is_rechargable {
1885 $self->seconds ne ''
1886 || $self->upbytes ne ''
1887 || $self->downbytes ne ''
1888 || $self->totalbytes ne '';
1891 =item seconds_since TIMESTAMP
1893 Returns the number of seconds this account has been online since TIMESTAMP,
1894 according to the session monitor (see L<FS::Session>).
1896 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1897 L<Time::Local> and L<Date::Parse> for conversion functions.
1901 #note: POD here, implementation in FS::cust_svc
1904 $self->cust_svc->seconds_since(@_);
1907 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1909 Returns the numbers of seconds this account has been online between
1910 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1911 external SQL radacct table, specified via sqlradius export. Sessions which
1912 started in the specified range but are still open are counted from session
1913 start to the end of the range (unless they are over 1 day old, in which case
1914 they are presumed missing their stop record and not counted). Also, sessions
1915 which end in the range but started earlier are counted from the start of the
1916 range to session end. Finally, sessions which start before the range but end
1917 after are counted for the entire range.
1919 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1920 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1925 #note: POD here, implementation in FS::cust_svc
1926 sub seconds_since_sqlradacct {
1928 $self->cust_svc->seconds_since_sqlradacct(@_);
1931 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1933 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1934 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1935 TIMESTAMP_END (exclusive).
1937 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1938 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1943 #note: POD here, implementation in FS::cust_svc
1944 sub attribute_since_sqlradacct {
1946 $self->cust_svc->attribute_since_sqlradacct(@_);
1949 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1951 Returns an array of hash references of this customers login history for the
1952 given time range. (document this better)
1956 sub get_session_history {
1958 $self->cust_svc->get_session_history(@_);
1961 =item last_login_text
1963 Returns text describing the time of last login.
1967 sub last_login_text {
1969 $self->last_login ? ctime($self->last_login) : 'unknown';
1972 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
1977 my($self, $start, $end, %opt ) = @_;
1979 my $did = $self->username; #yup
1981 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
1983 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
1985 #SELECT $for_update * FROM cdr
1986 # WHERE calldate >= $start #need a conversion
1987 # AND calldate < $end #ditto
1988 # AND ( charged_party = "$did"
1989 # OR charged_party = "$prefix$did" #if length($prefix);
1990 # OR ( ( charged_party IS NULL OR charged_party = '' )
1992 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
1995 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
1998 if ( length($prefix) ) {
2000 " AND ( charged_party = '$did'
2001 OR charged_party = '$prefix$did'
2002 OR ( ( charged_party IS NULL OR charged_party = '' )
2004 ( src = '$did' OR src = '$prefix$did' )
2010 " AND ( charged_party = '$did'
2011 OR ( ( charged_party IS NULL OR charged_party = '' )
2021 'select' => "$for_update *",
2024 #( freesidestatus IS NULL OR freesidestatus = '' )
2025 'freesidestatus' => '',
2027 'extra_sql' => $charged_or_src,
2035 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2041 if ( $self->usergroup ) {
2042 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2043 unless ref($self->usergroup) eq 'ARRAY';
2044 #when provisioning records, export callback runs in svc_Common.pm before
2045 #radius_usergroup records can be inserted...
2046 @{$self->usergroup};
2048 map { $_->groupname }
2049 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2053 =item clone_suspended
2055 Constructor used by FS::part_export::_export_suspend fallback. Document
2060 sub clone_suspended {
2062 my %hash = $self->hash;
2063 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2064 new FS::svc_acct \%hash;
2067 =item clone_kludge_unsuspend
2069 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2074 sub clone_kludge_unsuspend {
2076 my %hash = $self->hash;
2077 $hash{_password} = '';
2078 new FS::svc_acct \%hash;
2081 =item check_password
2083 Checks the supplied password against the (possibly encrypted) password in the
2084 database. Returns true for a successful authentication, false for no match.
2086 Currently supported encryptions are: classic DES crypt() and MD5
2090 sub check_password {
2091 my($self, $check_password) = @_;
2093 #remove old-style SUSPENDED kludge, they should be allowed to login to
2094 #self-service and pay up
2095 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2097 #eventually should check a "password-encoding" field
2098 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2100 } elsif ( length($password) < 13 ) { #plaintext
2101 $check_password eq $password;
2102 } elsif ( length($password) == 13 ) { #traditional DES crypt
2103 crypt($check_password, $password) eq $password;
2104 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2105 unix_md5_crypt($check_password, $password) eq $password;
2106 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2107 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2108 $self->svcnum. "\n";
2111 warn "Can't check password: Unrecognized encryption for svcnum ".
2112 $self->svcnum. "\n";
2118 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2120 Returns an encrypted password, either by passing through an encrypted password
2121 in the database or by encrypting a plaintext password from the database.
2123 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2124 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2125 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2126 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2127 encryption type is only used if the password is not already encrypted in the
2132 sub crypt_password {
2134 #eventually should check a "password-encoding" field
2135 if ( length($self->_password) == 13
2136 || $self->_password =~ /^\$(1|2a?)\$/
2137 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2142 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2143 if ( $encryption eq 'crypt' ) {
2146 $saltset[int(rand(64))].$saltset[int(rand(64))]
2148 } elsif ( $encryption eq 'md5' ) {
2149 unix_md5_crypt( $self->_password );
2150 } elsif ( $encryption eq 'blowfish' ) {
2151 croak "unknown encryption method $encryption";
2153 croak "unknown encryption method $encryption";
2158 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2160 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2161 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2162 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2164 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2165 to work the same as the B</crypt_password> method.
2171 #eventually should check a "password-encoding" field
2172 if ( length($self->_password) == 13 ) { #crypt
2173 return '{CRYPT}'. $self->_password;
2174 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2176 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2177 warn "Blowfish encryption not supported in this context, svcnum ".
2178 $self->svcnum. "\n";
2179 return '{CRYPT}*'; #unsupported, should not auth
2180 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2181 return '{SSHA}'. $1;
2182 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2183 return '{NS-MTA-MD5}'. $1;
2185 return '{PLAIN}'. $self->_password;
2186 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2187 #if ( $encryption eq 'crypt' ) {
2188 # return '{CRYPT}'. crypt(
2190 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2192 #} elsif ( $encryption eq 'md5' ) {
2193 # unix_md5_crypt( $self->_password );
2194 #} elsif ( $encryption eq 'blowfish' ) {
2195 # croak "unknown encryption method $encryption";
2197 # croak "unknown encryption method $encryption";
2202 =item domain_slash_username
2204 Returns $domain/$username/
2208 sub domain_slash_username {
2210 $self->domain. '/'. $self->username. '/';
2213 =item virtual_maildir
2215 Returns $domain/maildirs/$username/
2219 sub virtual_maildir {
2221 $self->domain. '/maildirs/'. $self->username. '/';
2232 This is the FS::svc_acct job-queue-able version. It still uses
2233 FS::Misc::send_email under-the-hood.
2240 eval "use FS::Misc qw(send_email)";
2243 $opt{mimetype} ||= 'text/plain';
2244 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2246 my $error = send_email(
2247 'from' => $opt{from},
2249 'subject' => $opt{subject},
2250 'content-type' => $opt{mimetype},
2251 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2253 die $error if $error;
2256 =item check_and_rebuild_fuzzyfiles
2260 sub check_and_rebuild_fuzzyfiles {
2261 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2262 -e "$dir/svc_acct.username"
2263 or &rebuild_fuzzyfiles;
2266 =item rebuild_fuzzyfiles
2270 sub rebuild_fuzzyfiles {
2272 use Fcntl qw(:flock);
2274 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2278 open(USERNAMELOCK,">>$dir/svc_acct.username")
2279 or die "can't open $dir/svc_acct.username: $!";
2280 flock(USERNAMELOCK,LOCK_EX)
2281 or die "can't lock $dir/svc_acct.username: $!";
2283 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2285 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2286 or die "can't open $dir/svc_acct.username.tmp: $!";
2287 print USERNAMECACHE join("\n", @all_username), "\n";
2288 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2290 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2300 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2301 open(USERNAMECACHE,"<$dir/svc_acct.username")
2302 or die "can't open $dir/svc_acct.username: $!";
2303 my @array = map { chomp; $_; } <USERNAMECACHE>;
2304 close USERNAMECACHE;
2308 =item append_fuzzyfiles USERNAME
2312 sub append_fuzzyfiles {
2313 my $username = shift;
2315 &check_and_rebuild_fuzzyfiles;
2317 use Fcntl qw(:flock);
2319 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2321 open(USERNAME,">>$dir/svc_acct.username")
2322 or die "can't open $dir/svc_acct.username: $!";
2323 flock(USERNAME,LOCK_EX)
2324 or die "can't lock $dir/svc_acct.username: $!";
2326 print USERNAME "$username\n";
2328 flock(USERNAME,LOCK_UN)
2329 or die "can't unlock $dir/svc_acct.username: $!";
2337 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2341 sub radius_usergroup_selector {
2342 my $sel_groups = shift;
2343 my %sel_groups = map { $_=>1 } @$sel_groups;
2345 my $selectname = shift || 'radius_usergroup';
2348 my $sth = $dbh->prepare(
2349 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2350 ) or die $dbh->errstr;
2351 $sth->execute() or die $sth->errstr;
2352 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2356 function ${selectname}_doadd(object) {
2357 var myvalue = object.${selectname}_add.value;
2358 var optionName = new Option(myvalue,myvalue,false,true);
2359 var length = object.$selectname.length;
2360 object.$selectname.options[length] = optionName;
2361 object.${selectname}_add.value = "";
2364 <SELECT MULTIPLE NAME="$selectname">
2367 foreach my $group ( @all_groups ) {
2368 $html .= qq(<OPTION VALUE="$group");
2369 if ( $sel_groups{$group} ) {
2370 $html .= ' SELECTED';
2371 $sel_groups{$group} = 0;
2373 $html .= ">$group</OPTION>\n";
2375 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2376 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2378 $html .= '</SELECT>';
2380 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2381 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2386 =item reached_threshold
2388 Performs some activities when svc_acct thresholds (such as number of seconds
2389 remaining) are reached.
2393 sub reached_threshold {
2396 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2397 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2399 if ( $opt{'op'} eq '+' ){
2400 $svc_acct->setfield( $opt{'column'}.'_threshold',
2401 int($svc_acct->getfield($opt{'column'})
2402 * ( $conf->exists('svc_acct-usage_threshold')
2403 ? $conf->config('svc_acct-usage_threshold')/100
2408 my $error = $svc_acct->replace;
2409 die $error if $error;
2410 }elsif ( $opt{'op'} eq '-' ){
2412 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2413 return '' if ($threshold eq '' );
2415 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2416 my $error = $svc_acct->replace;
2417 die $error if $error; # email next time, i guess
2419 if ( $warning_template ) {
2420 eval "use FS::Misc qw(send_email)";
2423 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2424 my $cust_main = $cust_pkg->cust_main;
2426 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2427 $cust_main->invoicing_list,
2428 ($opt{'to'} ? $opt{'to'} : ())
2431 my $mimetype = $warning_mimetype;
2432 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2434 my $body = $warning_template->fill_in( HASH => {
2435 'custnum' => $cust_main->custnum,
2436 'username' => $svc_acct->username,
2437 'password' => $svc_acct->_password,
2438 'first' => $cust_main->first,
2439 'last' => $cust_main->getfield('last'),
2440 'pkg' => $cust_pkg->part_pkg->pkg,
2441 'column' => $opt{'column'},
2442 'amount' => $opt{'column'} =~/bytes/
2443 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2444 : $svc_acct->getfield($opt{'column'}),
2445 'threshold' => $opt{'column'} =~/bytes/
2446 ? FS::UI::bytecount::display_bytecount($threshold)
2451 my $error = send_email(
2452 'from' => $warning_from,
2454 'subject' => $warning_subject,
2455 'content-type' => $mimetype,
2456 'body' => [ map "$_\n", split("\n", $body) ],
2458 die $error if $error;
2461 die "unknown op: " . $opt{'op'};
2469 The $recref stuff in sub check should be cleaned up.
2471 The suspend, unsuspend and cancel methods update the database, but not the
2472 current object. This is probably a bug as it's unexpected and
2475 radius_usergroup_selector? putting web ui components in here? they should
2476 probably live somewhere else...
2478 insertion of RADIUS group stuff in insert could be done with child_objects now
2479 (would probably clean up export of them too)
2483 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2484 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2485 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2486 L<freeside-queued>), L<FS::svc_acct_pop>,
2487 schema.html from the base documentation.
2491 =item domain_select_hash %OPTIONS
2493 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2494 may at present purchase.
2496 Currently available options are: I<pkgnum> I<svcpart>
2500 sub domain_select_hash {
2501 my ($self, %options) = @_;
2507 $part_svc = $self->part_svc;
2508 $cust_pkg = $self->cust_svc->cust_pkg
2512 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2513 if $options{'svcpart'};
2515 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2516 if $options{'pkgnum'};
2518 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2519 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2520 %domains = map { $_->svcnum => $_->domain }
2521 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2522 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2523 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2524 %domains = map { $_->svcnum => $_->domain }
2525 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2526 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2527 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2529 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2532 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2533 my $svc_domain = qsearchs('svc_domain',
2534 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2535 if ( $svc_domain ) {
2536 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2538 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2539 $part_svc->part_svc_column('domsvc')->columnvalue;