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;
25 use FS::UID qw( datasrc driver_name );
27 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
28 use FS::Msgcat qw(gettext);
29 use FS::UI::bytecount;
35 use FS::cust_main_invoice;
39 use FS::radius_usergroup;
46 @ISA = qw( FS::svc_Common );
49 $me = '[FS::svc_acct]';
51 #ask FS::UID to run this stuff for us later
52 $FS::UID::callback{'FS::svc_acct'} = sub {
54 $dir_prefix = $conf->config('home');
55 @shells = $conf->config('shells');
56 $usernamemin = $conf->config('usernamemin') || 2;
57 $usernamemax = $conf->config('usernamemax');
58 $passwordmin = $conf->config('passwordmin') || 6;
59 $passwordmax = $conf->config('passwordmax') || 8;
60 $username_letter = $conf->exists('username-letter');
61 $username_letterfirst = $conf->exists('username-letterfirst');
62 $username_noperiod = $conf->exists('username-noperiod');
63 $username_nounderscore = $conf->exists('username-nounderscore');
64 $username_nodash = $conf->exists('username-nodash');
65 $username_uppercase = $conf->exists('username-uppercase');
66 $username_ampersand = $conf->exists('username-ampersand');
67 $username_percent = $conf->exists('username-percent');
68 $password_noampersand = $conf->exists('password-noexclamation');
69 $password_noexclamation = $conf->exists('password-noexclamation');
70 $dirhash = $conf->config('dirhash') || 0;
71 if ( $conf->exists('welcome_email') ) {
72 $welcome_template = new Text::Template (
74 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
75 ) or warn "can't create welcome email template: $Text::Template::ERROR";
76 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
77 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
78 $welcome_subject_template = new Text::Template (
80 SOURCE => $welcome_subject,
81 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
82 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
84 $welcome_template = '';
86 $welcome_subject = '';
87 $welcome_mimetype = '';
89 if ( $conf->exists('warning_email') ) {
90 $warning_template = new Text::Template (
92 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
93 ) or warn "can't create warning email template: $Text::Template::ERROR";
94 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
95 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
96 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
97 $warning_cc = $conf->config('warning_email-cc');
99 $warning_template = '';
101 $warning_subject = '';
102 $warning_mimetype = '';
105 $smtpmachine = $conf->config('smtpmachine');
106 $radius_password = $conf->config('radius-password') || 'Password';
107 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
108 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
111 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
112 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
116 my ( $hashref, $cache ) = @_;
117 if ( $hashref->{'svc_acct_svcnum'} ) {
118 $self->{'_domsvc'} = FS::svc_domain->new( {
119 'svcnum' => $hashref->{'domsvc'},
120 'domain' => $hashref->{'svc_acct_domain'},
121 'catchall' => $hashref->{'svc_acct_catchall'},
128 FS::svc_acct - Object methods for svc_acct records
134 $record = new FS::svc_acct \%hash;
135 $record = new FS::svc_acct { 'column' => 'value' };
137 $error = $record->insert;
139 $error = $new_record->replace($old_record);
141 $error = $record->delete;
143 $error = $record->check;
145 $error = $record->suspend;
147 $error = $record->unsuspend;
149 $error = $record->cancel;
151 %hash = $record->radius;
153 %hash = $record->radius_reply;
155 %hash = $record->radius_check;
157 $domain = $record->domain;
159 $svc_domain = $record->svc_domain;
161 $email = $record->email;
163 $seconds_since = $record->seconds_since($timestamp);
167 An FS::svc_acct object represents an account. FS::svc_acct inherits from
168 FS::svc_Common. The following fields are currently supported:
172 =item svcnum - primary key (assigned automatcially for new accounts)
176 =item _password - generated if blank
178 =item sec_phrase - security phrase
180 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
188 =item dir - set automatically if blank (and uid is not)
192 =item quota - (unimplementd)
194 =item slipip - IP address
204 =item domsvc - svcnum from svc_domain
206 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
208 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
218 Creates a new account. To add the account to the database, see L<"insert">.
225 'longname_plural' => 'Access accounts and mailboxes',
226 'sorts' => [ 'username', 'uid', 'last_login', ],
227 'display_weight' => 10,
228 'cancel_weight' => 50,
230 'dir' => 'Home directory',
233 def_label => 'UID (set to fixed and blank for no UIDs)',
236 'slipip' => 'IP address',
237 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
239 label => 'Access number',
241 select_table => 'svc_acct_pop',
242 select_key => 'popnum',
243 select_label => 'city',
249 disable_default => 1,
256 disable_inventory => 1,
259 '_password' => 'Password',
262 def_label => 'GID (when blank, defaults to UID)',
266 #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)',
268 def_label=> 'Shell (set to blank for no shell tracking)',
270 select_list => [ $conf->config('shells') ],
271 disable_inventory => 1,
274 'finger' => 'Real name', # (GECOS)',
277 #def_label => 'svcnum from svc_domain',
279 select_table => 'svc_domain',
280 select_key => 'svcnum',
281 select_label => 'domain',
282 disable_inventory => 1,
286 label => 'RADIUS groups',
287 type => 'radius_usergroup_selector',
288 disable_inventory => 1,
291 'seconds' => { label => 'Seconds',
293 disable_inventory => 1,
295 disable_part_svc_column => 1,
297 'upbytes' => { label => 'Upload',
299 disable_inventory => 1,
301 'format' => \&FS::UI::bytecount::display_bytecount,
302 'parse' => \&FS::UI::bytecount::parse_bytecount,
303 disable_part_svc_column => 1,
305 'downbytes' => { label => 'Download',
307 disable_inventory => 1,
309 'format' => \&FS::UI::bytecount::display_bytecount,
310 'parse' => \&FS::UI::bytecount::parse_bytecount,
311 disable_part_svc_column => 1,
313 'totalbytes'=> { label => 'Total up and download',
315 disable_inventory => 1,
317 'format' => \&FS::UI::bytecount::display_bytecount,
318 'parse' => \&FS::UI::bytecount::parse_bytecount,
319 disable_part_svc_column => 1,
321 'seconds_threshold' => { label => 'Seconds threshold',
323 disable_inventory => 1,
325 disable_part_svc_column => 1,
327 'upbytes_threshold' => { label => 'Upload threshold',
329 disable_inventory => 1,
331 'format' => \&FS::UI::bytecount::display_bytecount,
332 'parse' => \&FS::UI::bytecount::parse_bytecount,
333 disable_part_svc_column => 1,
335 'downbytes_threshold' => { label => 'Download threshold',
337 disable_inventory => 1,
339 'format' => \&FS::UI::bytecount::display_bytecount,
340 'parse' => \&FS::UI::bytecount::parse_bytecount,
341 disable_part_svc_column => 1,
343 'totalbytes_threshold'=> { label => 'Total up and download threshold',
345 disable_inventory => 1,
347 'format' => \&FS::UI::bytecount::display_bytecount,
348 'parse' => \&FS::UI::bytecount::parse_bytecount,
349 disable_part_svc_column => 1,
352 label => 'Last login',
356 label => 'Last logout',
363 sub table { 'svc_acct'; }
367 #false laziness with edit/svc_acct.cgi
369 my( $self, $groups ) = @_;
370 if ( ref($groups) eq 'ARRAY' ) {
372 } elsif ( length($groups) ) {
373 [ split(/\s*,\s*/, $groups) ];
382 shift->_lastlog('in', @_);
386 shift->_lastlog('out', @_);
390 my( $self, $op, $time ) = @_;
392 if ( defined($time) ) {
393 warn "$me last_log$op called on svcnum ". $self->svcnum.
394 ' ('. $self->email. "): $time\n"
399 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
403 my $sth = $dbh->prepare( $sql )
404 or die "Error preparing $sql: ". $dbh->errstr;
405 my $rv = $sth->execute($time, $self->svcnum);
406 die "Error executing $sql: ". $sth->errstr
408 die "Can't update last_log$op for svcnum". $self->svcnum
411 $self->{'Hash'}->{"last_log$op"} = $time;
413 $self->getfield("last_log$op");
417 =item search_sql STRING
419 Class method which returns an SQL fragment to search for the given string.
424 my( $class, $string ) = @_;
425 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
426 my( $username, $domain ) = ( $1, $2 );
427 my $q_username = dbh->quote($username);
428 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
430 "svc_acct.username = $q_username AND ( ".
431 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
436 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
438 $class->search_sql_field('slipip', $string ).
440 $class->search_sql_field('username', $string ).
443 $class->search_sql_field('username', $string);
447 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
449 Returns the "username@domain" string for this account.
451 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
461 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
463 Returns a longer string label for this acccount ("Real Name <username@domain>"
464 if available, or "username@domain").
466 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
473 my $label = $self->label(@_);
474 my $finger = $self->finger;
475 return $label unless $finger =~ /\S/;
476 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
477 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
481 =item insert [ , OPTION => VALUE ... ]
483 Adds this account to the database. If there is an error, returns the error,
484 otherwise returns false.
486 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
487 defined. An FS::cust_svc record will be created and inserted.
489 The additional field I<usergroup> can optionally be defined; if so it should
490 contain an arrayref of group names. See L<FS::radius_usergroup>.
492 The additional field I<child_objects> can optionally be defined; if so it
493 should contain an arrayref of FS::tablename objects. They will have their
494 svcnum fields set and will be inserted after this record, but before any
495 exports are run. Each element of the array can also optionally be a
496 two-element array reference containing the child object and the name of an
497 alternate field to be filled in with the newly-inserted svcnum, for example
498 C<[ $svc_forward, 'srcsvc' ]>
500 Currently available options are: I<depend_jobnum>
502 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
503 jobnums), all provisioning jobs will have a dependancy on the supplied
504 jobnum(s) (they will not run until the specific job(s) complete(s)).
506 (TODOC: L<FS::queue> and L<freeside-queued>)
508 (TODOC: new exports!)
517 warn "[$me] insert called on $self: ". Dumper($self).
518 "\nwith options: ". Dumper(%options);
521 local $SIG{HUP} = 'IGNORE';
522 local $SIG{INT} = 'IGNORE';
523 local $SIG{QUIT} = 'IGNORE';
524 local $SIG{TERM} = 'IGNORE';
525 local $SIG{TSTP} = 'IGNORE';
526 local $SIG{PIPE} = 'IGNORE';
528 my $oldAutoCommit = $FS::UID::AutoCommit;
529 local $FS::UID::AutoCommit = 0;
532 my $error = $self->check;
533 return $error if $error;
535 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
536 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
537 unless ( $cust_svc ) {
538 $dbh->rollback if $oldAutoCommit;
539 return "no cust_svc record found for svcnum ". $self->svcnum;
541 $self->pkgnum($cust_svc->pkgnum);
542 $self->svcpart($cust_svc->svcpart);
545 $error = $self->_check_duplicate;
547 $dbh->rollback if $oldAutoCommit;
551 # set usage fields and thresholds if unset but set in a package def
552 if ( $self->pkgnum ) {
553 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
554 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
555 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
557 my %values = $part_pkg->usage_valuehash;
558 my $multiplier = $conf->exists('svc_acct-usage_threshold')
559 ? 1 - $conf->config('svc_acct-usage_threshold')/100
562 foreach ( keys %values ) {
563 next if $self->getfield($_);
564 $self->setfield( $_, $values{$_} );
565 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) );
572 $error = $self->SUPER::insert(
573 'jobnums' => \@jobnums,
574 'child_objects' => $self->child_objects,
578 $dbh->rollback if $oldAutoCommit;
582 if ( $self->usergroup ) {
583 foreach my $groupname ( @{$self->usergroup} ) {
584 my $radius_usergroup = new FS::radius_usergroup ( {
585 svcnum => $self->svcnum,
586 groupname => $groupname,
588 my $error = $radius_usergroup->insert;
590 $dbh->rollback if $oldAutoCommit;
596 unless ( $skip_fuzzyfiles ) {
597 $error = $self->queue_fuzzyfiles_update;
599 $dbh->rollback if $oldAutoCommit;
600 return "updating fuzzy search cache: $error";
604 my $cust_pkg = $self->cust_svc->cust_pkg;
607 my $cust_main = $cust_pkg->cust_main;
609 if ( $conf->exists('emailinvoiceautoalways')
610 || $conf->exists('emailinvoiceauto')
611 && ! $cust_main->invoicing_list_emailonly
613 my @invoicing_list = $cust_main->invoicing_list;
614 push @invoicing_list, $self->email;
615 $cust_main->invoicing_list(\@invoicing_list);
620 if ( $welcome_template && $cust_pkg ) {
621 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
625 'custnum' => $self->custnum,
626 'username' => $self->username,
627 'password' => $self->_password,
628 'first' => $cust_main->first,
629 'last' => $cust_main->getfield('last'),
630 'pkg' => $cust_pkg->part_pkg->pkg,
632 my $wqueue = new FS::queue {
633 'svcnum' => $self->svcnum,
634 'job' => 'FS::svc_acct::send_email'
636 my $error = $wqueue->insert(
638 'from' => $welcome_from,
639 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
640 'mimetype' => $welcome_mimetype,
641 'body' => $welcome_template->fill_in( HASH => \%hash, ),
644 $dbh->rollback if $oldAutoCommit;
645 return "error queuing welcome email: $error";
648 if ( $options{'depend_jobnum'} ) {
649 warn "$me depend_jobnum found; adding to welcome email dependancies"
651 if ( ref($options{'depend_jobnum'}) ) {
652 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
653 "to welcome email dependancies"
655 push @jobnums, @{ $options{'depend_jobnum'} };
657 warn "$me adding job $options{'depend_jobnum'} ".
658 "to welcome email dependancies"
660 push @jobnums, $options{'depend_jobnum'};
664 foreach my $jobnum ( @jobnums ) {
665 my $error = $wqueue->depend_insert($jobnum);
667 $dbh->rollback if $oldAutoCommit;
668 return "error queuing welcome email job dependancy: $error";
678 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
684 Deletes this account from the database. If there is an error, returns the
685 error, otherwise returns false.
687 The corresponding FS::cust_svc record will be deleted as well.
689 (TODOC: new exports!)
696 return "can't delete system account" if $self->_check_system;
698 return "Can't delete an account which is a (svc_forward) source!"
699 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
701 return "Can't delete an account which is a (svc_forward) destination!"
702 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
704 return "Can't delete an account with (svc_www) web service!"
705 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
707 # what about records in session ? (they should refer to history table)
709 local $SIG{HUP} = 'IGNORE';
710 local $SIG{INT} = 'IGNORE';
711 local $SIG{QUIT} = 'IGNORE';
712 local $SIG{TERM} = 'IGNORE';
713 local $SIG{TSTP} = 'IGNORE';
714 local $SIG{PIPE} = 'IGNORE';
716 my $oldAutoCommit = $FS::UID::AutoCommit;
717 local $FS::UID::AutoCommit = 0;
720 foreach my $cust_main_invoice (
721 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
723 unless ( defined($cust_main_invoice) ) {
724 warn "WARNING: something's wrong with qsearch";
727 my %hash = $cust_main_invoice->hash;
728 $hash{'dest'} = $self->email;
729 my $new = new FS::cust_main_invoice \%hash;
730 my $error = $new->replace($cust_main_invoice);
732 $dbh->rollback if $oldAutoCommit;
737 foreach my $svc_domain (
738 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
740 my %hash = new FS::svc_domain->hash;
741 $hash{'catchall'} = '';
742 my $new = new FS::svc_domain \%hash;
743 my $error = $new->replace($svc_domain);
745 $dbh->rollback if $oldAutoCommit;
750 my $error = $self->SUPER::delete;
752 $dbh->rollback if $oldAutoCommit;
756 foreach my $radius_usergroup (
757 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
759 my $error = $radius_usergroup->delete;
761 $dbh->rollback if $oldAutoCommit;
766 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
770 =item replace OLD_RECORD
772 Replaces OLD_RECORD with this one in the database. If there is an error,
773 returns the error, otherwise returns false.
775 The additional field I<usergroup> can optionally be defined; if so it should
776 contain an arrayref of group names. See L<FS::radius_usergroup>.
782 my ( $new, $old ) = ( shift, shift );
784 warn "$me replacing $old with $new\n" if $DEBUG;
786 # We absolutely have to have an old vs. new record to make this work.
787 if (!defined($old)) {
788 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
791 return "can't modify system account" if $old->_check_system;
794 #no warnings 'numeric'; #alas, a 5.006-ism
797 foreach my $xid (qw( uid gid )) {
799 return "Can't change $xid!"
800 if ! $conf->exists("svc_acct-edit_$xid")
801 && $old->$xid() != $new->$xid()
802 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
807 #change homdir when we change username
808 $new->setfield('dir', '') if $old->username ne $new->username;
810 local $SIG{HUP} = 'IGNORE';
811 local $SIG{INT} = 'IGNORE';
812 local $SIG{QUIT} = 'IGNORE';
813 local $SIG{TERM} = 'IGNORE';
814 local $SIG{TSTP} = 'IGNORE';
815 local $SIG{PIPE} = 'IGNORE';
817 my $oldAutoCommit = $FS::UID::AutoCommit;
818 local $FS::UID::AutoCommit = 0;
821 # redundant, but so $new->usergroup gets set
822 $error = $new->check;
823 return $error if $error;
825 $old->usergroup( [ $old->radius_groups ] );
827 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
828 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
830 if ( $new->usergroup ) {
831 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
832 my @newgroups = @{$new->usergroup};
833 foreach my $oldgroup ( @{$old->usergroup} ) {
834 if ( grep { $oldgroup eq $_ } @newgroups ) {
835 @newgroups = grep { $oldgroup ne $_ } @newgroups;
838 my $radius_usergroup = qsearchs('radius_usergroup', {
839 svcnum => $old->svcnum,
840 groupname => $oldgroup,
842 my $error = $radius_usergroup->delete;
844 $dbh->rollback if $oldAutoCommit;
845 return "error deleting radius_usergroup $oldgroup: $error";
849 foreach my $newgroup ( @newgroups ) {
850 my $radius_usergroup = new FS::radius_usergroup ( {
851 svcnum => $new->svcnum,
852 groupname => $newgroup,
854 my $error = $radius_usergroup->insert;
856 $dbh->rollback if $oldAutoCommit;
857 return "error adding radius_usergroup $newgroup: $error";
863 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
864 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
865 $error = $new->_check_duplicate;
867 $dbh->rollback if $oldAutoCommit;
872 $error = $new->SUPER::replace($old);
874 $dbh->rollback if $oldAutoCommit;
875 return $error if $error;
878 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
879 $error = $new->queue_fuzzyfiles_update;
881 $dbh->rollback if $oldAutoCommit;
882 return "updating fuzzy search cache: $error";
886 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
890 =item queue_fuzzyfiles_update
892 Used by insert & replace to update the fuzzy search cache
896 sub queue_fuzzyfiles_update {
899 local $SIG{HUP} = 'IGNORE';
900 local $SIG{INT} = 'IGNORE';
901 local $SIG{QUIT} = 'IGNORE';
902 local $SIG{TERM} = 'IGNORE';
903 local $SIG{TSTP} = 'IGNORE';
904 local $SIG{PIPE} = 'IGNORE';
906 my $oldAutoCommit = $FS::UID::AutoCommit;
907 local $FS::UID::AutoCommit = 0;
910 my $queue = new FS::queue {
911 'svcnum' => $self->svcnum,
912 'job' => 'FS::svc_acct::append_fuzzyfiles'
914 my $error = $queue->insert($self->username);
916 $dbh->rollback if $oldAutoCommit;
917 return "queueing job (transaction rolled back): $error";
920 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
928 Suspends this account by calling export-specific suspend hooks. If there is
929 an error, returns the error, otherwise returns false.
931 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
937 return "can't suspend system account" if $self->_check_system;
938 $self->SUPER::suspend;
943 Unsuspends this account by by calling export-specific suspend hooks. If there
944 is an error, returns the error, otherwise returns false.
946 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
952 my %hash = $self->hash;
953 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
954 $hash{_password} = $1;
955 my $new = new FS::svc_acct ( \%hash );
956 my $error = $new->replace($self);
957 return $error if $error;
960 $self->SUPER::unsuspend;
965 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
967 If the B<auto_unset_catchall> configuration option is set, this method will
968 automatically remove any references to the canceled service in the catchall
969 field of svc_domain. This allows packages that contain both a svc_domain and
970 its catchall svc_acct to be canceled in one step.
975 # Only one thing to do at this level
977 foreach my $svc_domain (
978 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
979 if($conf->exists('auto_unset_catchall')) {
980 my %hash = $svc_domain->hash;
981 $hash{catchall} = '';
982 my $new = new FS::svc_domain ( \%hash );
983 my $error = $new->replace($svc_domain);
984 return $error if $error;
986 return "cannot unprovision svc_acct #".$self->svcnum.
987 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
991 $self->SUPER::cancel;
997 Checks all fields to make sure this is a valid service. If there is an error,
998 returns the error, otherwise returns false. Called by the insert and replace
1001 Sets any fixed values; see L<FS::part_svc>.
1008 my($recref) = $self->hashref;
1010 my $x = $self->setfixed( $self->_fieldhandlers );
1011 return $x unless ref($x);
1014 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1016 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1019 my $error = $self->ut_numbern('svcnum')
1020 #|| $self->ut_number('domsvc')
1021 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1022 || $self->ut_textn('sec_phrase')
1023 || $self->ut_snumbern('seconds')
1024 || $self->ut_snumbern('upbytes')
1025 || $self->ut_snumbern('downbytes')
1026 || $self->ut_snumbern('totalbytes')
1028 return $error if $error;
1030 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1031 if ( $username_uppercase ) {
1032 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/i
1033 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1034 $recref->{username} = $1;
1036 $recref->{username} =~ /^([a-z0-9_\-\.\&\%]{$usernamemin,$ulen})$/
1037 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1038 $recref->{username} = $1;
1041 if ( $username_letterfirst ) {
1042 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1043 } elsif ( $username_letter ) {
1044 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1046 if ( $username_noperiod ) {
1047 $recref->{username} =~ /\./ and return gettext('illegal_username');
1049 if ( $username_nounderscore ) {
1050 $recref->{username} =~ /_/ and return gettext('illegal_username');
1052 if ( $username_nodash ) {
1053 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1055 unless ( $username_ampersand ) {
1056 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1058 if ( $password_noampersand ) {
1059 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1061 if ( $password_noexclamation ) {
1062 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1064 unless ( $username_percent ) {
1065 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1068 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1069 $recref->{popnum} = $1;
1070 return "Unknown popnum" unless
1071 ! $recref->{popnum} ||
1072 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1074 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1076 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1077 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1079 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1080 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1081 #not all systems use gid=uid
1082 #you can set a fixed gid in part_svc
1084 return "Only root can have uid 0"
1085 if $recref->{uid} == 0
1086 && $recref->{username} !~ /^(root|toor|smtp)$/;
1088 unless ( $recref->{username} eq 'sync' ) {
1089 if ( grep $_ eq $recref->{shell}, @shells ) {
1090 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1092 return "Illegal shell \`". $self->shell. "\'; ".
1093 $conf->dir. "/shells contains: @shells";
1096 $recref->{shell} = '/bin/sync';
1100 $recref->{gid} ne '' ?
1101 return "Can't have gid without uid" : ( $recref->{gid}='' );
1102 #$recref->{dir} ne '' ?
1103 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1104 $recref->{shell} ne '' ?
1105 return "Can't have shell without uid" : ( $recref->{shell}='' );
1108 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1110 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1111 or return "Illegal directory: ". $recref->{dir};
1112 $recref->{dir} = $1;
1113 return "Illegal directory"
1114 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1115 return "Illegal directory"
1116 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1117 unless ( $recref->{dir} ) {
1118 $recref->{dir} = $dir_prefix . '/';
1119 if ( $dirhash > 0 ) {
1120 for my $h ( 1 .. $dirhash ) {
1121 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1123 } elsif ( $dirhash < 0 ) {
1124 for my $h ( reverse $dirhash .. -1 ) {
1125 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1128 $recref->{dir} .= $recref->{username};
1134 # $error = $self->ut_textn('finger');
1135 # return $error if $error;
1136 if ( $self->getfield('finger') eq '' ) {
1137 my $cust_pkg = $self->svcnum
1138 ? $self->cust_svc->cust_pkg
1139 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1141 my $cust_main = $cust_pkg->cust_main;
1142 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1145 $self->getfield('finger') =~
1146 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1147 or return "Illegal finger: ". $self->getfield('finger');
1148 $self->setfield('finger', $1);
1150 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1151 $recref->{quota} = $1;
1153 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1154 if ( $recref->{slipip} eq '' ) {
1155 $recref->{slipip} = '';
1156 } elsif ( $recref->{slipip} eq '0e0' ) {
1157 $recref->{slipip} = '0e0';
1159 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1160 or return "Illegal slipip: ". $self->slipip;
1161 $recref->{slipip} = $1;
1166 #arbitrary RADIUS stuff; allow ut_textn for now
1167 foreach ( grep /^radius_/, fields('svc_acct') ) {
1168 $self->ut_textn($_);
1171 #generate a password if it is blank
1172 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1173 unless ( $recref->{_password} );
1175 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1176 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1177 $recref->{_password} = $1.$3;
1178 #uncomment this to encrypt password immediately upon entry, or run
1179 #bin/crypt_pw in cron to give new users a window during which their
1180 #password is available to techs, for faxing, etc. (also be aware of
1182 #$recref->{password} = $1.
1183 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1185 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1186 $recref->{_password} = $1.$3;
1187 } elsif ( $recref->{_password} eq '*' ) {
1188 $recref->{_password} = '*';
1189 } elsif ( $recref->{_password} eq '!' ) {
1190 $recref->{_password} = '!';
1191 } elsif ( $recref->{_password} eq '!!' ) {
1192 $recref->{_password} = '!!';
1194 #return "Illegal password";
1195 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1196 FS::Msgcat::_gettext('illegal_password_characters').
1197 ": ". $recref->{_password};
1200 $self->SUPER::check;
1205 Internal function to check the username against the list of system usernames
1206 from the I<system_usernames> configuration value. Returns true if the username
1207 is listed on the system username list.
1213 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1214 $conf->config('system_usernames')
1218 =item _check_duplicate
1220 Internal function to check for duplicates usernames, username@domain pairs and
1223 If the I<global_unique-username> configuration value is set to B<username> or
1224 B<username@domain>, enforces global username or username@domain uniqueness.
1226 In all cases, check for duplicate uids and usernames or username@domain pairs
1227 per export and with identical I<svcpart> values.
1231 sub _check_duplicate {
1234 my $global_unique = $conf->config('global_unique-username') || 'none';
1235 return '' if $global_unique eq 'disabled';
1237 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1238 if ( driver_name =~ /^Pg/i ) {
1239 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1241 } elsif ( driver_name =~ /^mysql/i ) {
1242 dbh->do("SELECT * FROM duplicate_lock
1243 WHERE lockname = 'svc_acct'
1245 ) or die dbh->errstr;
1247 die "unknown database ". driver_name.
1248 "; don't know how to lock for duplicate search";
1250 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1252 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1253 unless ( $part_svc ) {
1254 return 'unknown svcpart '. $self->svcpart;
1257 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1258 qsearch( 'svc_acct', { 'username' => $self->username } );
1259 return gettext('username_in_use')
1260 if $global_unique eq 'username' && @dup_user;
1262 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1263 qsearch( 'svc_acct', { 'username' => $self->username,
1264 'domsvc' => $self->domsvc } );
1265 return gettext('username_in_use')
1266 if $global_unique eq 'username@domain' && @dup_userdomain;
1269 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1270 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1271 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1272 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1277 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1278 my $exports = FS::part_export::export_info('svc_acct');
1279 my %conflict_user_svcpart;
1280 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1282 foreach my $part_export ( $part_svc->part_export ) {
1284 #this will catch to the same exact export
1285 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1287 #this will catch to exports w/same exporthost+type ???
1288 #my @other_part_export = qsearch('part_export', {
1289 # 'machine' => $part_export->machine,
1290 # 'exporttype' => $part_export->exporttype,
1292 #foreach my $other_part_export ( @other_part_export ) {
1293 # push @svcparts, map { $_->svcpart }
1294 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1297 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1298 #silly kludge to avoid uninitialized value errors
1299 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1300 ? $exports->{$part_export->exporttype}{'nodomain'}
1302 if ( $nodomain =~ /^Y/i ) {
1303 $conflict_user_svcpart{$_} = $part_export->exportnum
1306 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1311 foreach my $dup_user ( @dup_user ) {
1312 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1313 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1314 return "duplicate username ". $self->username.
1315 ": conflicts with svcnum ". $dup_user->svcnum.
1316 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1320 foreach my $dup_userdomain ( @dup_userdomain ) {
1321 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1322 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1323 return "duplicate username\@domain ". $self->email.
1324 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1325 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1329 foreach my $dup_uid ( @dup_uid ) {
1330 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1331 if ( exists($conflict_user_svcpart{$dup_svcpart})
1332 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1333 return "duplicate uid ". $self->uid.
1334 ": conflicts with svcnum ". $dup_uid->svcnum.
1336 ( $conflict_user_svcpart{$dup_svcpart}
1337 || $conflict_userdomain_svcpart{$dup_svcpart} );
1349 Depriciated, use radius_reply instead.
1354 carp "FS::svc_acct::radius depriciated, use radius_reply";
1355 $_[0]->radius_reply;
1360 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1361 reply attributes of this record.
1363 Note that this is now the preferred method for reading RADIUS attributes -
1364 accessing the columns directly is discouraged, as the column names are
1365 expected to change in the future.
1372 return %{ $self->{'radius_reply'} }
1373 if exists $self->{'radius_reply'};
1378 my($column, $attrib) = ($1, $2);
1379 #$attrib =~ s/_/\-/g;
1380 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1381 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1383 if ( $self->slipip && $self->slipip ne '0e0' ) {
1384 $reply{$radius_ip} = $self->slipip;
1387 if ( $self->seconds !~ /^$/ ) {
1388 $reply{'Session-Timeout'} = $self->seconds;
1396 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1397 check attributes of this record.
1399 Note that this is now the preferred method for reading RADIUS attributes -
1400 accessing the columns directly is discouraged, as the column names are
1401 expected to change in the future.
1408 return %{ $self->{'radius_check'} }
1409 if exists $self->{'radius_check'};
1414 my($column, $attrib) = ($1, $2);
1415 #$attrib =~ s/_/\-/g;
1416 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1417 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1419 my $password = $self->_password;
1420 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1422 my $cust_svc = $self->cust_svc;
1423 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1425 my $cust_pkg = $cust_svc->cust_pkg;
1426 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1427 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1436 This method instructs the object to "snapshot" or freeze RADIUS check and
1437 reply attributes to the current values.
1441 #bah, my english is too broken this morning
1442 #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
1443 #the FS::cust_pkg's replace method to trigger the correct export updates when
1444 #package dates change)
1449 $self->{$_} = { $self->$_() }
1450 foreach qw( radius_reply radius_check );
1454 =item forget_snapshot
1456 This methos instructs the object to forget any previously snapshotted
1457 RADIUS check and reply attributes.
1461 sub forget_snapshot {
1465 foreach qw( radius_reply radius_check );
1469 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1471 Returns the domain associated with this account.
1473 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1480 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1481 my $svc_domain = $self->svc_domain(@_)
1482 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1483 $svc_domain->domain;
1488 Returns the FS::svc_domain record for this account's domain (see
1493 # FS::h_svc_acct has a history-aware svc_domain override
1498 ? $self->{'_domsvc'}
1499 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1504 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1508 #inherited from svc_Common
1510 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1512 Returns an email address associated with the account.
1514 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1521 $self->username. '@'. $self->domain(@_);
1526 Returns an array of FS::acct_snarf records associated with the account.
1527 If the acct_snarf table does not exist or there are no associated records,
1528 an empty list is returned
1534 return () unless dbdef->table('acct_snarf');
1535 eval "use FS::acct_snarf;";
1537 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1540 =item decrement_upbytes OCTETS
1542 Decrements the I<upbytes> field of this record by the given amount. If there
1543 is an error, returns the error, otherwise returns false.
1547 sub decrement_upbytes {
1548 shift->_op_usage('-', 'upbytes', @_);
1551 =item increment_upbytes OCTETS
1553 Increments the I<upbytes> field of this record by the given amount. If there
1554 is an error, returns the error, otherwise returns false.
1558 sub increment_upbytes {
1559 shift->_op_usage('+', 'upbytes', @_);
1562 =item decrement_downbytes OCTETS
1564 Decrements the I<downbytes> field of this record by the given amount. If there
1565 is an error, returns the error, otherwise returns false.
1569 sub decrement_downbytes {
1570 shift->_op_usage('-', 'downbytes', @_);
1573 =item increment_downbytes OCTETS
1575 Increments the I<downbytes> field of this record by the given amount. If there
1576 is an error, returns the error, otherwise returns false.
1580 sub increment_downbytes {
1581 shift->_op_usage('+', 'downbytes', @_);
1584 =item decrement_totalbytes OCTETS
1586 Decrements the I<totalbytes> field of this record by the given amount. If there
1587 is an error, returns the error, otherwise returns false.
1591 sub decrement_totalbytes {
1592 shift->_op_usage('-', 'totalbytes', @_);
1595 =item increment_totalbytes OCTETS
1597 Increments the I<totalbytes> field of this record by the given amount. If there
1598 is an error, returns the error, otherwise returns false.
1602 sub increment_totalbytes {
1603 shift->_op_usage('+', 'totalbytes', @_);
1606 =item decrement_seconds SECONDS
1608 Decrements the I<seconds> field of this record by the given amount. If there
1609 is an error, returns the error, otherwise returns false.
1613 sub decrement_seconds {
1614 shift->_op_usage('-', 'seconds', @_);
1617 =item increment_seconds SECONDS
1619 Increments the I<seconds> field of this record by the given amount. If there
1620 is an error, returns the error, otherwise returns false.
1624 sub increment_seconds {
1625 shift->_op_usage('+', 'seconds', @_);
1633 my %op2condition = (
1634 '-' => sub { my($self, $column, $amount) = @_;
1635 $self->$column - $amount <= 0;
1637 '+' => sub { my($self, $column, $amount) = @_;
1638 $self->$column + $amount > 0;
1641 my %op2warncondition = (
1642 '-' => sub { my($self, $column, $amount) = @_;
1643 my $threshold = $column . '_threshold';
1644 $self->$column - $amount <= $self->$threshold + 0;
1646 '+' => sub { my($self, $column, $amount) = @_;
1647 $self->$column + $amount > 0;
1652 my( $self, $op, $column, $amount ) = @_;
1654 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1655 ' ('. $self->email. "): $op $amount\n"
1658 return '' unless $amount;
1660 local $SIG{HUP} = 'IGNORE';
1661 local $SIG{INT} = 'IGNORE';
1662 local $SIG{QUIT} = 'IGNORE';
1663 local $SIG{TERM} = 'IGNORE';
1664 local $SIG{TSTP} = 'IGNORE';
1665 local $SIG{PIPE} = 'IGNORE';
1667 my $oldAutoCommit = $FS::UID::AutoCommit;
1668 local $FS::UID::AutoCommit = 0;
1671 my $sql = "UPDATE svc_acct SET $column = ".
1672 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1673 " $op ? WHERE svcnum = ?";
1677 my $sth = $dbh->prepare( $sql )
1678 or die "Error preparing $sql: ". $dbh->errstr;
1679 my $rv = $sth->execute($amount, $self->svcnum);
1680 die "Error executing $sql: ". $sth->errstr
1681 unless defined($rv);
1682 die "Can't update $column for svcnum". $self->svcnum
1685 my $action = $op2action{$op};
1687 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1688 ( $action eq 'suspend' && !$self->overlimit
1689 || $action eq 'unsuspend' && $self->overlimit )
1691 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1692 if ($part_export->option('overlimit_groups')) {
1694 my $other = new FS::svc_acct $self->hashref;
1695 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1696 ($self, $part_export->option('overlimit_groups'));
1697 $other->usergroup( $groups );
1698 if ($action eq 'suspend'){
1699 $new = $other; $old = $self;
1701 $new = $self; $old = $other;
1703 my $error = $part_export->export_replace($new, $old);
1704 $error ||= $self->overlimit($action);
1706 $dbh->rollback if $oldAutoCommit;
1707 return "Error replacing radius groups in export, ${op}: $error";
1713 if ( $conf->exists("svc_acct-usage_$action")
1714 && &{$op2condition{$op}}($self, $column, $amount) ) {
1715 #my $error = $self->$action();
1716 my $error = $self->cust_svc->cust_pkg->$action();
1717 # $error ||= $self->overlimit($action);
1719 $dbh->rollback if $oldAutoCommit;
1720 return "Error ${action}ing: $error";
1724 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1725 my $wqueue = new FS::queue {
1726 'svcnum' => $self->svcnum,
1727 'job' => 'FS::svc_acct::reached_threshold',
1732 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1736 my $error = $wqueue->insert(
1737 'svcnum' => $self->svcnum,
1739 'column' => $column,
1743 $dbh->rollback if $oldAutoCommit;
1744 return "Error queuing threshold activity: $error";
1748 warn "$me update successful; committing\n"
1750 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1756 my( $self, $valueref, %options ) = @_;
1758 warn "$me set_usage called for svcnum ". $self->svcnum.
1759 ' ('. $self->email. "): ".
1760 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1763 local $SIG{HUP} = 'IGNORE';
1764 local $SIG{INT} = 'IGNORE';
1765 local $SIG{QUIT} = 'IGNORE';
1766 local $SIG{TERM} = 'IGNORE';
1767 local $SIG{TSTP} = 'IGNORE';
1768 local $SIG{PIPE} = 'IGNORE';
1770 local $FS::svc_Common::noexport_hack = 1;
1771 my $oldAutoCommit = $FS::UID::AutoCommit;
1772 local $FS::UID::AutoCommit = 0;
1777 if ( $options{null} ) {
1778 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1779 qw( seconds upbytes downbytes totalbytes )
1782 foreach my $field (keys %$valueref){
1783 $reset = 1 if $valueref->{$field};
1784 $self->setfield($field, $valueref->{$field});
1785 $self->setfield( $field.'_threshold',
1786 int($self->getfield($field)
1787 * ( $conf->exists('svc_acct-usage_threshold')
1788 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1793 $handyhash{$field} = $self->getfield($field);
1794 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1796 #my $error = $self->replace; #NO! we avoid the call to ->check for
1797 #die $error if $error; #services not explicity changed via the UI
1799 my $sql = "UPDATE svc_acct SET " .
1800 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1801 " WHERE svcnum = ". $self->svcnum;
1806 if (scalar(keys %handyhash)) {
1807 my $sth = $dbh->prepare( $sql )
1808 or die "Error preparing $sql: ". $dbh->errstr;
1809 my $rv = $sth->execute();
1810 die "Error executing $sql: ". $sth->errstr
1811 unless defined($rv);
1812 die "Can't update usage for svcnum ". $self->svcnum
1819 if ($self->overlimit) {
1820 $error = $self->overlimit('unsuspend');
1821 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1822 if ($part_export->option('overlimit_groups')) {
1823 my $old = new FS::svc_acct $self->hashref;
1824 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1825 ($self, $part_export->option('overlimit_groups'));
1826 $old->usergroup( $groups );
1827 $error ||= $part_export->export_replace($self, $old);
1832 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1833 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1836 $dbh->rollback if $oldAutoCommit;
1837 return "Error unsuspending: $error";
1841 warn "$me update successful; committing\n"
1843 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1849 =item recharge HASHREF
1851 Increments usage columns by the amount specified in HASHREF as
1852 column=>amount pairs.
1857 my ($self, $vhash) = @_;
1860 warn "[$me] recharge called on $self: ". Dumper($self).
1861 "\nwith vhash: ". Dumper($vhash);
1864 my $oldAutoCommit = $FS::UID::AutoCommit;
1865 local $FS::UID::AutoCommit = 0;
1869 foreach my $column (keys %$vhash){
1870 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1874 $dbh->rollback if $oldAutoCommit;
1876 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1881 =item is_rechargeable
1883 Returns true if this svc_account can be "recharged" and false otherwise.
1887 sub is_rechargable {
1889 $self->seconds ne ''
1890 || $self->upbytes ne ''
1891 || $self->downbytes ne ''
1892 || $self->totalbytes ne '';
1895 =item seconds_since TIMESTAMP
1897 Returns the number of seconds this account has been online since TIMESTAMP,
1898 according to the session monitor (see L<FS::Session>).
1900 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1901 L<Time::Local> and L<Date::Parse> for conversion functions.
1905 #note: POD here, implementation in FS::cust_svc
1908 $self->cust_svc->seconds_since(@_);
1911 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1913 Returns the numbers of seconds this account has been online between
1914 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1915 external SQL radacct table, specified via sqlradius export. Sessions which
1916 started in the specified range but are still open are counted from session
1917 start to the end of the range (unless they are over 1 day old, in which case
1918 they are presumed missing their stop record and not counted). Also, sessions
1919 which end in the range but started earlier are counted from the start of the
1920 range to session end. Finally, sessions which start before the range but end
1921 after are counted for the entire range.
1923 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1924 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1929 #note: POD here, implementation in FS::cust_svc
1930 sub seconds_since_sqlradacct {
1932 $self->cust_svc->seconds_since_sqlradacct(@_);
1935 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1937 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1938 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1939 TIMESTAMP_END (exclusive).
1941 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1942 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1947 #note: POD here, implementation in FS::cust_svc
1948 sub attribute_since_sqlradacct {
1950 $self->cust_svc->attribute_since_sqlradacct(@_);
1953 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1955 Returns an array of hash references of this customers login history for the
1956 given time range. (document this better)
1960 sub get_session_history {
1962 $self->cust_svc->get_session_history(@_);
1965 =item last_login_text
1967 Returns text describing the time of last login.
1971 sub last_login_text {
1973 $self->last_login ? ctime($self->last_login) : 'unknown';
1976 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
1981 my($self, $start, $end, %opt ) = @_;
1983 my $did = $self->username; #yup
1985 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
1987 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
1989 #SELECT $for_update * FROM cdr
1990 # WHERE calldate >= $start #need a conversion
1991 # AND calldate < $end #ditto
1992 # AND ( charged_party = "$did"
1993 # OR charged_party = "$prefix$did" #if length($prefix);
1994 # OR ( ( charged_party IS NULL OR charged_party = '' )
1996 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
1999 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2002 if ( length($prefix) ) {
2004 " AND ( charged_party = '$did'
2005 OR charged_party = '$prefix$did'
2006 OR ( ( charged_party IS NULL OR charged_party = '' )
2008 ( src = '$did' OR src = '$prefix$did' )
2014 " AND ( charged_party = '$did'
2015 OR ( ( charged_party IS NULL OR charged_party = '' )
2025 'select' => "$for_update *",
2028 #( freesidestatus IS NULL OR freesidestatus = '' )
2029 'freesidestatus' => '',
2031 'extra_sql' => $charged_or_src,
2039 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2045 if ( $self->usergroup ) {
2046 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2047 unless ref($self->usergroup) eq 'ARRAY';
2048 #when provisioning records, export callback runs in svc_Common.pm before
2049 #radius_usergroup records can be inserted...
2050 @{$self->usergroup};
2052 map { $_->groupname }
2053 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2057 =item clone_suspended
2059 Constructor used by FS::part_export::_export_suspend fallback. Document
2064 sub clone_suspended {
2066 my %hash = $self->hash;
2067 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2068 new FS::svc_acct \%hash;
2071 =item clone_kludge_unsuspend
2073 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2078 sub clone_kludge_unsuspend {
2080 my %hash = $self->hash;
2081 $hash{_password} = '';
2082 new FS::svc_acct \%hash;
2085 =item check_password
2087 Checks the supplied password against the (possibly encrypted) password in the
2088 database. Returns true for a successful authentication, false for no match.
2090 Currently supported encryptions are: classic DES crypt() and MD5
2094 sub check_password {
2095 my($self, $check_password) = @_;
2097 #remove old-style SUSPENDED kludge, they should be allowed to login to
2098 #self-service and pay up
2099 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2101 #eventually should check a "password-encoding" field
2102 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2104 } elsif ( length($password) < 13 ) { #plaintext
2105 $check_password eq $password;
2106 } elsif ( length($password) == 13 ) { #traditional DES crypt
2107 crypt($check_password, $password) eq $password;
2108 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2109 unix_md5_crypt($check_password, $password) eq $password;
2110 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2111 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2112 $self->svcnum. "\n";
2115 warn "Can't check password: Unrecognized encryption for svcnum ".
2116 $self->svcnum. "\n";
2122 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2124 Returns an encrypted password, either by passing through an encrypted password
2125 in the database or by encrypting a plaintext password from the database.
2127 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2128 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2129 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2130 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2131 encryption type is only used if the password is not already encrypted in the
2136 sub crypt_password {
2138 #eventually should check a "password-encoding" field
2139 if ( length($self->_password) == 13
2140 || $self->_password =~ /^\$(1|2a?)\$/
2141 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2146 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2147 if ( $encryption eq 'crypt' ) {
2150 $saltset[int(rand(64))].$saltset[int(rand(64))]
2152 } elsif ( $encryption eq 'md5' ) {
2153 unix_md5_crypt( $self->_password );
2154 } elsif ( $encryption eq 'blowfish' ) {
2155 croak "unknown encryption method $encryption";
2157 croak "unknown encryption method $encryption";
2162 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2164 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2165 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2166 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2168 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2169 to work the same as the B</crypt_password> method.
2175 #eventually should check a "password-encoding" field
2176 if ( length($self->_password) == 13 ) { #crypt
2177 return '{CRYPT}'. $self->_password;
2178 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2180 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2181 warn "Blowfish encryption not supported in this context, svcnum ".
2182 $self->svcnum. "\n";
2183 return '{CRYPT}*'; #unsupported, should not auth
2184 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2185 return '{SSHA}'. $1;
2186 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2187 return '{NS-MTA-MD5}'. $1;
2189 return '{PLAIN}'. $self->_password;
2190 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2191 #if ( $encryption eq 'crypt' ) {
2192 # return '{CRYPT}'. crypt(
2194 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2196 #} elsif ( $encryption eq 'md5' ) {
2197 # unix_md5_crypt( $self->_password );
2198 #} elsif ( $encryption eq 'blowfish' ) {
2199 # croak "unknown encryption method $encryption";
2201 # croak "unknown encryption method $encryption";
2206 =item domain_slash_username
2208 Returns $domain/$username/
2212 sub domain_slash_username {
2214 $self->domain. '/'. $self->username. '/';
2217 =item virtual_maildir
2219 Returns $domain/maildirs/$username/
2223 sub virtual_maildir {
2225 $self->domain. '/maildirs/'. $self->username. '/';
2236 This is the FS::svc_acct job-queue-able version. It still uses
2237 FS::Misc::send_email under-the-hood.
2244 eval "use FS::Misc qw(send_email)";
2247 $opt{mimetype} ||= 'text/plain';
2248 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2250 my $error = send_email(
2251 'from' => $opt{from},
2253 'subject' => $opt{subject},
2254 'content-type' => $opt{mimetype},
2255 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2257 die $error if $error;
2260 =item check_and_rebuild_fuzzyfiles
2264 sub check_and_rebuild_fuzzyfiles {
2265 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2266 -e "$dir/svc_acct.username"
2267 or &rebuild_fuzzyfiles;
2270 =item rebuild_fuzzyfiles
2274 sub rebuild_fuzzyfiles {
2276 use Fcntl qw(:flock);
2278 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2282 open(USERNAMELOCK,">>$dir/svc_acct.username")
2283 or die "can't open $dir/svc_acct.username: $!";
2284 flock(USERNAMELOCK,LOCK_EX)
2285 or die "can't lock $dir/svc_acct.username: $!";
2287 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2289 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2290 or die "can't open $dir/svc_acct.username.tmp: $!";
2291 print USERNAMECACHE join("\n", @all_username), "\n";
2292 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2294 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2304 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2305 open(USERNAMECACHE,"<$dir/svc_acct.username")
2306 or die "can't open $dir/svc_acct.username: $!";
2307 my @array = map { chomp; $_; } <USERNAMECACHE>;
2308 close USERNAMECACHE;
2312 =item append_fuzzyfiles USERNAME
2316 sub append_fuzzyfiles {
2317 my $username = shift;
2319 &check_and_rebuild_fuzzyfiles;
2321 use Fcntl qw(:flock);
2323 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2325 open(USERNAME,">>$dir/svc_acct.username")
2326 or die "can't open $dir/svc_acct.username: $!";
2327 flock(USERNAME,LOCK_EX)
2328 or die "can't lock $dir/svc_acct.username: $!";
2330 print USERNAME "$username\n";
2332 flock(USERNAME,LOCK_UN)
2333 or die "can't unlock $dir/svc_acct.username: $!";
2341 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2345 sub radius_usergroup_selector {
2346 my $sel_groups = shift;
2347 my %sel_groups = map { $_=>1 } @$sel_groups;
2349 my $selectname = shift || 'radius_usergroup';
2352 my $sth = $dbh->prepare(
2353 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2354 ) or die $dbh->errstr;
2355 $sth->execute() or die $sth->errstr;
2356 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2360 function ${selectname}_doadd(object) {
2361 var myvalue = object.${selectname}_add.value;
2362 var optionName = new Option(myvalue,myvalue,false,true);
2363 var length = object.$selectname.length;
2364 object.$selectname.options[length] = optionName;
2365 object.${selectname}_add.value = "";
2368 <SELECT MULTIPLE NAME="$selectname">
2371 foreach my $group ( @all_groups ) {
2372 $html .= qq(<OPTION VALUE="$group");
2373 if ( $sel_groups{$group} ) {
2374 $html .= ' SELECTED';
2375 $sel_groups{$group} = 0;
2377 $html .= ">$group</OPTION>\n";
2379 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2380 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2382 $html .= '</SELECT>';
2384 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2385 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2390 =item reached_threshold
2392 Performs some activities when svc_acct thresholds (such as number of seconds
2393 remaining) are reached.
2397 sub reached_threshold {
2400 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2401 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2403 if ( $opt{'op'} eq '+' ){
2404 $svc_acct->setfield( $opt{'column'}.'_threshold',
2405 int($svc_acct->getfield($opt{'column'})
2406 * ( $conf->exists('svc_acct-usage_threshold')
2407 ? $conf->config('svc_acct-usage_threshold')/100
2412 my $error = $svc_acct->replace;
2413 die $error if $error;
2414 }elsif ( $opt{'op'} eq '-' ){
2416 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2417 return '' if ($threshold eq '' );
2419 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2420 my $error = $svc_acct->replace;
2421 die $error if $error; # email next time, i guess
2423 if ( $warning_template ) {
2424 eval "use FS::Misc qw(send_email)";
2427 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2428 my $cust_main = $cust_pkg->cust_main;
2430 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2431 $cust_main->invoicing_list,
2432 ($opt{'to'} ? $opt{'to'} : ())
2435 my $mimetype = $warning_mimetype;
2436 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2438 my $body = $warning_template->fill_in( HASH => {
2439 'custnum' => $cust_main->custnum,
2440 'username' => $svc_acct->username,
2441 'password' => $svc_acct->_password,
2442 'first' => $cust_main->first,
2443 'last' => $cust_main->getfield('last'),
2444 'pkg' => $cust_pkg->part_pkg->pkg,
2445 'column' => $opt{'column'},
2446 'amount' => $opt{'column'} =~/bytes/
2447 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2448 : $svc_acct->getfield($opt{'column'}),
2449 'threshold' => $opt{'column'} =~/bytes/
2450 ? FS::UI::bytecount::display_bytecount($threshold)
2455 my $error = send_email(
2456 'from' => $warning_from,
2458 'subject' => $warning_subject,
2459 'content-type' => $mimetype,
2460 'body' => [ map "$_\n", split("\n", $body) ],
2462 die $error if $error;
2465 die "unknown op: " . $opt{'op'};
2473 The $recref stuff in sub check should be cleaned up.
2475 The suspend, unsuspend and cancel methods update the database, but not the
2476 current object. This is probably a bug as it's unexpected and
2479 radius_usergroup_selector? putting web ui components in here? they should
2480 probably live somewhere else...
2482 insertion of RADIUS group stuff in insert could be done with child_objects now
2483 (would probably clean up export of them too)
2487 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2488 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2489 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2490 L<freeside-queued>), L<FS::svc_acct_pop>,
2491 schema.html from the base documentation.
2495 =item domain_select_hash %OPTIONS
2497 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2498 may at present purchase.
2500 Currently available options are: I<pkgnum> I<svcpart>
2504 sub domain_select_hash {
2505 my ($self, %options) = @_;
2511 $part_svc = $self->part_svc;
2512 $cust_pkg = $self->cust_svc->cust_pkg
2516 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2517 if $options{'svcpart'};
2519 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2520 if $options{'pkgnum'};
2522 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2523 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2524 %domains = map { $_->svcnum => $_->domain }
2525 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2526 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2527 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2528 %domains = map { $_->svcnum => $_->domain }
2529 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2530 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2531 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2533 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2536 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2537 my $svc_domain = qsearchs('svc_domain',
2538 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2539 if ( $svc_domain ) {
2540 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2542 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2543 $part_svc->part_svc_column('domsvc')->columnvalue;