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 $username_colon
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 $username_colon = $conf->exists('username-colon');
69 $password_noampersand = $conf->exists('password-noexclamation');
70 $password_noexclamation = $conf->exists('password-noexclamation');
71 $dirhash = $conf->config('dirhash') || 0;
72 if ( $conf->exists('welcome_email') ) {
73 $welcome_template = new Text::Template (
75 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
76 ) or warn "can't create welcome email template: $Text::Template::ERROR";
77 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
78 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
79 $welcome_subject_template = new Text::Template (
81 SOURCE => $welcome_subject,
82 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
83 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
85 $welcome_template = '';
87 $welcome_subject = '';
88 $welcome_mimetype = '';
90 if ( $conf->exists('warning_email') ) {
91 $warning_template = new Text::Template (
93 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
94 ) or warn "can't create warning email template: $Text::Template::ERROR";
95 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
96 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
97 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
98 $warning_cc = $conf->config('warning_email-cc');
100 $warning_template = '';
102 $warning_subject = '';
103 $warning_mimetype = '';
106 $smtpmachine = $conf->config('smtpmachine');
107 $radius_password = $conf->config('radius-password') || 'Password';
108 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
109 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
112 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
113 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
117 my ( $hashref, $cache ) = @_;
118 if ( $hashref->{'svc_acct_svcnum'} ) {
119 $self->{'_domsvc'} = FS::svc_domain->new( {
120 'svcnum' => $hashref->{'domsvc'},
121 'domain' => $hashref->{'svc_acct_domain'},
122 'catchall' => $hashref->{'svc_acct_catchall'},
129 FS::svc_acct - Object methods for svc_acct records
135 $record = new FS::svc_acct \%hash;
136 $record = new FS::svc_acct { 'column' => 'value' };
138 $error = $record->insert;
140 $error = $new_record->replace($old_record);
142 $error = $record->delete;
144 $error = $record->check;
146 $error = $record->suspend;
148 $error = $record->unsuspend;
150 $error = $record->cancel;
152 %hash = $record->radius;
154 %hash = $record->radius_reply;
156 %hash = $record->radius_check;
158 $domain = $record->domain;
160 $svc_domain = $record->svc_domain;
162 $email = $record->email;
164 $seconds_since = $record->seconds_since($timestamp);
168 An FS::svc_acct object represents an account. FS::svc_acct inherits from
169 FS::svc_Common. The following fields are currently supported:
173 =item svcnum - primary key (assigned automatcially for new accounts)
177 =item _password - generated if blank
179 =item sec_phrase - security phrase
181 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
189 =item dir - set automatically if blank (and uid is not)
193 =item quota - (unimplementd)
195 =item slipip - IP address
205 =item domsvc - svcnum from svc_domain
207 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
209 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
219 Creates a new account. To add the account to the database, see L<"insert">.
226 'longname_plural' => 'Access accounts and mailboxes',
227 'sorts' => [ 'username', 'uid', 'last_login', ],
228 'display_weight' => 10,
229 'cancel_weight' => 50,
231 'dir' => 'Home directory',
234 def_label => 'UID (set to fixed and blank for no UIDs)',
237 'slipip' => 'IP address',
238 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
240 label => 'Access number',
242 select_table => 'svc_acct_pop',
243 select_key => 'popnum',
244 select_label => 'city',
250 disable_default => 1,
257 disable_inventory => 1,
260 '_password' => 'Password',
263 def_label => 'GID (when blank, defaults to UID)',
267 #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)',
269 def_label=> 'Shell (set to blank for no shell tracking)',
271 select_list => [ $conf->config('shells') ],
272 disable_inventory => 1,
275 'finger' => 'Real name', # (GECOS)',
278 #def_label => 'svcnum from svc_domain',
280 select_table => 'svc_domain',
281 select_key => 'svcnum',
282 select_label => 'domain',
283 disable_inventory => 1,
287 label => 'RADIUS groups',
288 type => 'radius_usergroup_selector',
289 disable_inventory => 1,
292 'seconds' => { label => 'Seconds',
294 disable_inventory => 1,
296 disable_part_svc_column => 1,
298 'upbytes' => { label => 'Upload',
300 disable_inventory => 1,
302 'format' => \&FS::UI::bytecount::display_bytecount,
303 'parse' => \&FS::UI::bytecount::parse_bytecount,
304 disable_part_svc_column => 1,
306 'downbytes' => { label => 'Download',
308 disable_inventory => 1,
310 'format' => \&FS::UI::bytecount::display_bytecount,
311 'parse' => \&FS::UI::bytecount::parse_bytecount,
312 disable_part_svc_column => 1,
314 'totalbytes'=> { label => 'Total up and download',
316 disable_inventory => 1,
318 'format' => \&FS::UI::bytecount::display_bytecount,
319 'parse' => \&FS::UI::bytecount::parse_bytecount,
320 disable_part_svc_column => 1,
322 'seconds_threshold' => { label => 'Seconds threshold',
324 disable_inventory => 1,
326 disable_part_svc_column => 1,
328 'upbytes_threshold' => { label => 'Upload threshold',
330 disable_inventory => 1,
332 'format' => \&FS::UI::bytecount::display_bytecount,
333 'parse' => \&FS::UI::bytecount::parse_bytecount,
334 disable_part_svc_column => 1,
336 'downbytes_threshold' => { label => 'Download threshold',
338 disable_inventory => 1,
340 'format' => \&FS::UI::bytecount::display_bytecount,
341 'parse' => \&FS::UI::bytecount::parse_bytecount,
342 disable_part_svc_column => 1,
344 'totalbytes_threshold'=> { label => 'Total up and download threshold',
346 disable_inventory => 1,
348 'format' => \&FS::UI::bytecount::display_bytecount,
349 'parse' => \&FS::UI::bytecount::parse_bytecount,
350 disable_part_svc_column => 1,
353 label => 'Last login',
357 label => 'Last logout',
364 sub table { 'svc_acct'; }
368 #false laziness with edit/svc_acct.cgi
370 my( $self, $groups ) = @_;
371 if ( ref($groups) eq 'ARRAY' ) {
373 } elsif ( length($groups) ) {
374 [ split(/\s*,\s*/, $groups) ];
383 shift->_lastlog('in', @_);
387 shift->_lastlog('out', @_);
391 my( $self, $op, $time ) = @_;
393 if ( defined($time) ) {
394 warn "$me last_log$op called on svcnum ". $self->svcnum.
395 ' ('. $self->email. "): $time\n"
400 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
404 my $sth = $dbh->prepare( $sql )
405 or die "Error preparing $sql: ". $dbh->errstr;
406 my $rv = $sth->execute($time, $self->svcnum);
407 die "Error executing $sql: ". $sth->errstr
409 die "Can't update last_log$op for svcnum". $self->svcnum
412 $self->{'Hash'}->{"last_log$op"} = $time;
414 $self->getfield("last_log$op");
418 =item search_sql STRING
420 Class method which returns an SQL fragment to search for the given string.
425 my( $class, $string ) = @_;
426 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
427 my( $username, $domain ) = ( $1, $2 );
428 my $q_username = dbh->quote($username);
429 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
431 "svc_acct.username = $q_username AND ( ".
432 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
437 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
439 $class->search_sql_field('slipip', $string ).
441 $class->search_sql_field('username', $string ).
444 $class->search_sql_field('username', $string);
448 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
450 Returns the "username@domain" string for this account.
452 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
462 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
464 Returns a longer string label for this acccount ("Real Name <username@domain>"
465 if available, or "username@domain").
467 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
474 my $label = $self->label(@_);
475 my $finger = $self->finger;
476 return $label unless $finger =~ /\S/;
477 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
478 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
482 =item insert [ , OPTION => VALUE ... ]
484 Adds this account to the database. If there is an error, returns the error,
485 otherwise returns false.
487 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
488 defined. An FS::cust_svc record will be created and inserted.
490 The additional field I<usergroup> can optionally be defined; if so it should
491 contain an arrayref of group names. See L<FS::radius_usergroup>.
493 The additional field I<child_objects> can optionally be defined; if so it
494 should contain an arrayref of FS::tablename objects. They will have their
495 svcnum fields set and will be inserted after this record, but before any
496 exports are run. Each element of the array can also optionally be a
497 two-element array reference containing the child object and the name of an
498 alternate field to be filled in with the newly-inserted svcnum, for example
499 C<[ $svc_forward, 'srcsvc' ]>
501 Currently available options are: I<depend_jobnum>
503 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
504 jobnums), all provisioning jobs will have a dependancy on the supplied
505 jobnum(s) (they will not run until the specific job(s) complete(s)).
507 (TODOC: L<FS::queue> and L<freeside-queued>)
509 (TODOC: new exports!)
518 warn "[$me] insert called on $self: ". Dumper($self).
519 "\nwith options: ". Dumper(%options);
522 local $SIG{HUP} = 'IGNORE';
523 local $SIG{INT} = 'IGNORE';
524 local $SIG{QUIT} = 'IGNORE';
525 local $SIG{TERM} = 'IGNORE';
526 local $SIG{TSTP} = 'IGNORE';
527 local $SIG{PIPE} = 'IGNORE';
529 my $oldAutoCommit = $FS::UID::AutoCommit;
530 local $FS::UID::AutoCommit = 0;
533 my $error = $self->check;
534 return $error if $error;
536 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
537 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
538 unless ( $cust_svc ) {
539 $dbh->rollback if $oldAutoCommit;
540 return "no cust_svc record found for svcnum ". $self->svcnum;
542 $self->pkgnum($cust_svc->pkgnum);
543 $self->svcpart($cust_svc->svcpart);
546 $error = $self->_check_duplicate;
548 $dbh->rollback if $oldAutoCommit;
552 # set usage fields and thresholds if unset but set in a package def
553 if ( $self->pkgnum ) {
554 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
555 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
556 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
558 my %values = $part_pkg->usage_valuehash;
559 my $multiplier = $conf->exists('svc_acct-usage_threshold')
560 ? 1 - $conf->config('svc_acct-usage_threshold')/100
563 foreach ( keys %values ) {
564 next if $self->getfield($_);
565 $self->setfield( $_, $values{$_} );
566 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) );
573 $error = $self->SUPER::insert(
574 'jobnums' => \@jobnums,
575 'child_objects' => $self->child_objects,
579 $dbh->rollback if $oldAutoCommit;
583 if ( $self->usergroup ) {
584 foreach my $groupname ( @{$self->usergroup} ) {
585 my $radius_usergroup = new FS::radius_usergroup ( {
586 svcnum => $self->svcnum,
587 groupname => $groupname,
589 my $error = $radius_usergroup->insert;
591 $dbh->rollback if $oldAutoCommit;
597 unless ( $skip_fuzzyfiles ) {
598 $error = $self->queue_fuzzyfiles_update;
600 $dbh->rollback if $oldAutoCommit;
601 return "updating fuzzy search cache: $error";
605 my $cust_pkg = $self->cust_svc->cust_pkg;
608 my $cust_main = $cust_pkg->cust_main;
610 if ( $conf->exists('emailinvoiceautoalways')
611 || $conf->exists('emailinvoiceauto')
612 && ! $cust_main->invoicing_list_emailonly
614 my @invoicing_list = $cust_main->invoicing_list;
615 push @invoicing_list, $self->email;
616 $cust_main->invoicing_list(\@invoicing_list);
621 if ( $welcome_template && $cust_pkg ) {
622 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
626 'custnum' => $self->custnum,
627 'username' => $self->username,
628 'password' => $self->_password,
629 'first' => $cust_main->first,
630 'last' => $cust_main->getfield('last'),
631 'pkg' => $cust_pkg->part_pkg->pkg,
633 my $wqueue = new FS::queue {
634 'svcnum' => $self->svcnum,
635 'job' => 'FS::svc_acct::send_email'
637 my $error = $wqueue->insert(
639 'from' => $welcome_from,
640 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
641 'mimetype' => $welcome_mimetype,
642 'body' => $welcome_template->fill_in( HASH => \%hash, ),
645 $dbh->rollback if $oldAutoCommit;
646 return "error queuing welcome email: $error";
649 if ( $options{'depend_jobnum'} ) {
650 warn "$me depend_jobnum found; adding to welcome email dependancies"
652 if ( ref($options{'depend_jobnum'}) ) {
653 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
654 "to welcome email dependancies"
656 push @jobnums, @{ $options{'depend_jobnum'} };
658 warn "$me adding job $options{'depend_jobnum'} ".
659 "to welcome email dependancies"
661 push @jobnums, $options{'depend_jobnum'};
665 foreach my $jobnum ( @jobnums ) {
666 my $error = $wqueue->depend_insert($jobnum);
668 $dbh->rollback if $oldAutoCommit;
669 return "error queuing welcome email job dependancy: $error";
679 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
685 Deletes this account from the database. If there is an error, returns the
686 error, otherwise returns false.
688 The corresponding FS::cust_svc record will be deleted as well.
690 (TODOC: new exports!)
697 return "can't delete system account" if $self->_check_system;
699 return "Can't delete an account which is a (svc_forward) source!"
700 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
702 return "Can't delete an account which is a (svc_forward) destination!"
703 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
705 return "Can't delete an account with (svc_www) web service!"
706 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
708 # what about records in session ? (they should refer to history table)
710 local $SIG{HUP} = 'IGNORE';
711 local $SIG{INT} = 'IGNORE';
712 local $SIG{QUIT} = 'IGNORE';
713 local $SIG{TERM} = 'IGNORE';
714 local $SIG{TSTP} = 'IGNORE';
715 local $SIG{PIPE} = 'IGNORE';
717 my $oldAutoCommit = $FS::UID::AutoCommit;
718 local $FS::UID::AutoCommit = 0;
721 foreach my $cust_main_invoice (
722 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
724 unless ( defined($cust_main_invoice) ) {
725 warn "WARNING: something's wrong with qsearch";
728 my %hash = $cust_main_invoice->hash;
729 $hash{'dest'} = $self->email;
730 my $new = new FS::cust_main_invoice \%hash;
731 my $error = $new->replace($cust_main_invoice);
733 $dbh->rollback if $oldAutoCommit;
738 foreach my $svc_domain (
739 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
741 my %hash = new FS::svc_domain->hash;
742 $hash{'catchall'} = '';
743 my $new = new FS::svc_domain \%hash;
744 my $error = $new->replace($svc_domain);
746 $dbh->rollback if $oldAutoCommit;
751 my $error = $self->SUPER::delete;
753 $dbh->rollback if $oldAutoCommit;
757 foreach my $radius_usergroup (
758 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
760 my $error = $radius_usergroup->delete;
762 $dbh->rollback if $oldAutoCommit;
767 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
771 =item replace OLD_RECORD
773 Replaces OLD_RECORD with this one in the database. If there is an error,
774 returns the error, otherwise returns false.
776 The additional field I<usergroup> can optionally be defined; if so it should
777 contain an arrayref of group names. See L<FS::radius_usergroup>.
783 my ( $new, $old ) = ( shift, shift );
785 warn "$me replacing $old with $new\n" if $DEBUG;
787 # We absolutely have to have an old vs. new record to make this work.
788 if (!defined($old)) {
789 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
792 return "can't modify system account" if $old->_check_system;
795 #no warnings 'numeric'; #alas, a 5.006-ism
798 foreach my $xid (qw( uid gid )) {
800 return "Can't change $xid!"
801 if ! $conf->exists("svc_acct-edit_$xid")
802 && $old->$xid() != $new->$xid()
803 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
808 #change homdir when we change username
809 $new->setfield('dir', '') if $old->username ne $new->username;
811 local $SIG{HUP} = 'IGNORE';
812 local $SIG{INT} = 'IGNORE';
813 local $SIG{QUIT} = 'IGNORE';
814 local $SIG{TERM} = 'IGNORE';
815 local $SIG{TSTP} = 'IGNORE';
816 local $SIG{PIPE} = 'IGNORE';
818 my $oldAutoCommit = $FS::UID::AutoCommit;
819 local $FS::UID::AutoCommit = 0;
822 # redundant, but so $new->usergroup gets set
823 $error = $new->check;
824 return $error if $error;
826 $old->usergroup( [ $old->radius_groups ] );
828 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
829 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
831 if ( $new->usergroup ) {
832 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
833 my @newgroups = @{$new->usergroup};
834 foreach my $oldgroup ( @{$old->usergroup} ) {
835 if ( grep { $oldgroup eq $_ } @newgroups ) {
836 @newgroups = grep { $oldgroup ne $_ } @newgroups;
839 my $radius_usergroup = qsearchs('radius_usergroup', {
840 svcnum => $old->svcnum,
841 groupname => $oldgroup,
843 my $error = $radius_usergroup->delete;
845 $dbh->rollback if $oldAutoCommit;
846 return "error deleting radius_usergroup $oldgroup: $error";
850 foreach my $newgroup ( @newgroups ) {
851 my $radius_usergroup = new FS::radius_usergroup ( {
852 svcnum => $new->svcnum,
853 groupname => $newgroup,
855 my $error = $radius_usergroup->insert;
857 $dbh->rollback if $oldAutoCommit;
858 return "error adding radius_usergroup $newgroup: $error";
864 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
865 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
866 $error = $new->_check_duplicate;
868 $dbh->rollback if $oldAutoCommit;
873 $error = $new->SUPER::replace($old);
875 $dbh->rollback if $oldAutoCommit;
876 return $error if $error;
879 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
880 $error = $new->queue_fuzzyfiles_update;
882 $dbh->rollback if $oldAutoCommit;
883 return "updating fuzzy search cache: $error";
887 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
891 =item queue_fuzzyfiles_update
893 Used by insert & replace to update the fuzzy search cache
897 sub queue_fuzzyfiles_update {
900 local $SIG{HUP} = 'IGNORE';
901 local $SIG{INT} = 'IGNORE';
902 local $SIG{QUIT} = 'IGNORE';
903 local $SIG{TERM} = 'IGNORE';
904 local $SIG{TSTP} = 'IGNORE';
905 local $SIG{PIPE} = 'IGNORE';
907 my $oldAutoCommit = $FS::UID::AutoCommit;
908 local $FS::UID::AutoCommit = 0;
911 my $queue = new FS::queue {
912 'svcnum' => $self->svcnum,
913 'job' => 'FS::svc_acct::append_fuzzyfiles'
915 my $error = $queue->insert($self->username);
917 $dbh->rollback if $oldAutoCommit;
918 return "queueing job (transaction rolled back): $error";
921 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
929 Suspends this account by calling export-specific suspend hooks. If there is
930 an error, returns the error, otherwise returns false.
932 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
938 return "can't suspend system account" if $self->_check_system;
939 $self->SUPER::suspend;
944 Unsuspends this account by by calling export-specific suspend hooks. If there
945 is an error, returns the error, otherwise returns false.
947 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
953 my %hash = $self->hash;
954 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
955 $hash{_password} = $1;
956 my $new = new FS::svc_acct ( \%hash );
957 my $error = $new->replace($self);
958 return $error if $error;
961 $self->SUPER::unsuspend;
966 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
968 If the B<auto_unset_catchall> configuration option is set, this method will
969 automatically remove any references to the canceled service in the catchall
970 field of svc_domain. This allows packages that contain both a svc_domain and
971 its catchall svc_acct to be canceled in one step.
976 # Only one thing to do at this level
978 foreach my $svc_domain (
979 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
980 if($conf->exists('auto_unset_catchall')) {
981 my %hash = $svc_domain->hash;
982 $hash{catchall} = '';
983 my $new = new FS::svc_domain ( \%hash );
984 my $error = $new->replace($svc_domain);
985 return $error if $error;
987 return "cannot unprovision svc_acct #".$self->svcnum.
988 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
992 $self->SUPER::cancel;
998 Checks all fields to make sure this is a valid service. If there is an error,
999 returns the error, otherwise returns false. Called by the insert and replace
1002 Sets any fixed values; see L<FS::part_svc>.
1009 my($recref) = $self->hashref;
1011 my $x = $self->setfixed( $self->_fieldhandlers );
1012 return $x unless ref($x);
1015 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1017 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1020 my $error = $self->ut_numbern('svcnum')
1021 #|| $self->ut_number('domsvc')
1022 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1023 || $self->ut_textn('sec_phrase')
1024 || $self->ut_snumbern('seconds')
1025 || $self->ut_snumbern('upbytes')
1026 || $self->ut_snumbern('downbytes')
1027 || $self->ut_snumbern('totalbytes')
1029 return $error if $error;
1031 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1032 if ( $username_uppercase ) {
1033 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1034 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1035 $recref->{username} = $1;
1037 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1038 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1039 $recref->{username} = $1;
1042 if ( $username_letterfirst ) {
1043 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1044 } elsif ( $username_letter ) {
1045 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1047 if ( $username_noperiod ) {
1048 $recref->{username} =~ /\./ and return gettext('illegal_username');
1050 if ( $username_nounderscore ) {
1051 $recref->{username} =~ /_/ and return gettext('illegal_username');
1053 if ( $username_nodash ) {
1054 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1056 unless ( $username_ampersand ) {
1057 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1059 if ( $password_noampersand ) {
1060 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1062 if ( $password_noexclamation ) {
1063 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1065 unless ( $username_percent ) {
1066 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1068 unless ( $username_colon ) {
1069 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1072 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1073 $recref->{popnum} = $1;
1074 return "Unknown popnum" unless
1075 ! $recref->{popnum} ||
1076 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1078 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1080 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1081 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1083 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1084 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1085 #not all systems use gid=uid
1086 #you can set a fixed gid in part_svc
1088 return "Only root can have uid 0"
1089 if $recref->{uid} == 0
1090 && $recref->{username} !~ /^(root|toor|smtp)$/;
1092 unless ( $recref->{username} eq 'sync' ) {
1093 if ( grep $_ eq $recref->{shell}, @shells ) {
1094 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1096 return "Illegal shell \`". $self->shell. "\'; ".
1097 $conf->dir. "/shells contains: @shells";
1100 $recref->{shell} = '/bin/sync';
1104 $recref->{gid} ne '' ?
1105 return "Can't have gid without uid" : ( $recref->{gid}='' );
1106 #$recref->{dir} ne '' ?
1107 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1108 $recref->{shell} ne '' ?
1109 return "Can't have shell without uid" : ( $recref->{shell}='' );
1112 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1114 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1115 or return "Illegal directory: ". $recref->{dir};
1116 $recref->{dir} = $1;
1117 return "Illegal directory"
1118 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1119 return "Illegal directory"
1120 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1121 unless ( $recref->{dir} ) {
1122 $recref->{dir} = $dir_prefix . '/';
1123 if ( $dirhash > 0 ) {
1124 for my $h ( 1 .. $dirhash ) {
1125 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1127 } elsif ( $dirhash < 0 ) {
1128 for my $h ( reverse $dirhash .. -1 ) {
1129 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1132 $recref->{dir} .= $recref->{username};
1138 # $error = $self->ut_textn('finger');
1139 # return $error if $error;
1140 if ( $self->getfield('finger') eq '' ) {
1141 my $cust_pkg = $self->svcnum
1142 ? $self->cust_svc->cust_pkg
1143 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1145 my $cust_main = $cust_pkg->cust_main;
1146 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1149 $self->getfield('finger') =~
1150 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1151 or return "Illegal finger: ". $self->getfield('finger');
1152 $self->setfield('finger', $1);
1154 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1155 $recref->{quota} = $1;
1157 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1158 if ( $recref->{slipip} eq '' ) {
1159 $recref->{slipip} = '';
1160 } elsif ( $recref->{slipip} eq '0e0' ) {
1161 $recref->{slipip} = '0e0';
1163 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1164 or return "Illegal slipip: ". $self->slipip;
1165 $recref->{slipip} = $1;
1170 #arbitrary RADIUS stuff; allow ut_textn for now
1171 foreach ( grep /^radius_/, fields('svc_acct') ) {
1172 $self->ut_textn($_);
1175 #generate a password if it is blank
1176 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1177 unless ( $recref->{_password} );
1179 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1180 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1181 $recref->{_password} = $1.$3;
1182 #uncomment this to encrypt password immediately upon entry, or run
1183 #bin/crypt_pw in cron to give new users a window during which their
1184 #password is available to techs, for faxing, etc. (also be aware of
1186 #$recref->{password} = $1.
1187 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1189 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1190 $recref->{_password} = $1.$3;
1191 } elsif ( $recref->{_password} eq '*' ) {
1192 $recref->{_password} = '*';
1193 } elsif ( $recref->{_password} eq '!' ) {
1194 $recref->{_password} = '!';
1195 } elsif ( $recref->{_password} eq '!!' ) {
1196 $recref->{_password} = '!!';
1198 #return "Illegal password";
1199 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1200 FS::Msgcat::_gettext('illegal_password_characters').
1201 ": ". $recref->{_password};
1204 $self->SUPER::check;
1209 Internal function to check the username against the list of system usernames
1210 from the I<system_usernames> configuration value. Returns true if the username
1211 is listed on the system username list.
1217 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1218 $conf->config('system_usernames')
1222 =item _check_duplicate
1224 Internal function to check for duplicates usernames, username@domain pairs and
1227 If the I<global_unique-username> configuration value is set to B<username> or
1228 B<username@domain>, enforces global username or username@domain uniqueness.
1230 In all cases, check for duplicate uids and usernames or username@domain pairs
1231 per export and with identical I<svcpart> values.
1235 sub _check_duplicate {
1238 my $global_unique = $conf->config('global_unique-username') || 'none';
1239 return '' if $global_unique eq 'disabled';
1241 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1242 if ( driver_name =~ /^Pg/i ) {
1243 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1245 } elsif ( driver_name =~ /^mysql/i ) {
1246 dbh->do("SELECT * FROM duplicate_lock
1247 WHERE lockname = 'svc_acct'
1249 ) or die dbh->errstr;
1251 die "unknown database ". driver_name.
1252 "; don't know how to lock for duplicate search";
1254 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1256 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1257 unless ( $part_svc ) {
1258 return 'unknown svcpart '. $self->svcpart;
1261 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1262 qsearch( 'svc_acct', { 'username' => $self->username } );
1263 return gettext('username_in_use')
1264 if $global_unique eq 'username' && @dup_user;
1266 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1267 qsearch( 'svc_acct', { 'username' => $self->username,
1268 'domsvc' => $self->domsvc } );
1269 return gettext('username_in_use')
1270 if $global_unique eq 'username@domain' && @dup_userdomain;
1273 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1274 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1275 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1276 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1281 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1282 my $exports = FS::part_export::export_info('svc_acct');
1283 my %conflict_user_svcpart;
1284 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1286 foreach my $part_export ( $part_svc->part_export ) {
1288 #this will catch to the same exact export
1289 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1291 #this will catch to exports w/same exporthost+type ???
1292 #my @other_part_export = qsearch('part_export', {
1293 # 'machine' => $part_export->machine,
1294 # 'exporttype' => $part_export->exporttype,
1296 #foreach my $other_part_export ( @other_part_export ) {
1297 # push @svcparts, map { $_->svcpart }
1298 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1301 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1302 #silly kludge to avoid uninitialized value errors
1303 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1304 ? $exports->{$part_export->exporttype}{'nodomain'}
1306 if ( $nodomain =~ /^Y/i ) {
1307 $conflict_user_svcpart{$_} = $part_export->exportnum
1310 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1315 foreach my $dup_user ( @dup_user ) {
1316 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1317 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1318 return "duplicate username ". $self->username.
1319 ": conflicts with svcnum ". $dup_user->svcnum.
1320 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1324 foreach my $dup_userdomain ( @dup_userdomain ) {
1325 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1326 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1327 return "duplicate username\@domain ". $self->email.
1328 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1329 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1333 foreach my $dup_uid ( @dup_uid ) {
1334 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1335 if ( exists($conflict_user_svcpart{$dup_svcpart})
1336 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1337 return "duplicate uid ". $self->uid.
1338 ": conflicts with svcnum ". $dup_uid->svcnum.
1340 ( $conflict_user_svcpart{$dup_svcpart}
1341 || $conflict_userdomain_svcpart{$dup_svcpart} );
1353 Depriciated, use radius_reply instead.
1358 carp "FS::svc_acct::radius depriciated, use radius_reply";
1359 $_[0]->radius_reply;
1364 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1365 reply attributes of this record.
1367 Note that this is now the preferred method for reading RADIUS attributes -
1368 accessing the columns directly is discouraged, as the column names are
1369 expected to change in the future.
1376 return %{ $self->{'radius_reply'} }
1377 if exists $self->{'radius_reply'};
1382 my($column, $attrib) = ($1, $2);
1383 #$attrib =~ s/_/\-/g;
1384 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1385 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1387 if ( $self->slipip && $self->slipip ne '0e0' ) {
1388 $reply{$radius_ip} = $self->slipip;
1391 if ( $self->seconds !~ /^$/ ) {
1392 $reply{'Session-Timeout'} = $self->seconds;
1400 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1401 check attributes of this record.
1403 Note that this is now the preferred method for reading RADIUS attributes -
1404 accessing the columns directly is discouraged, as the column names are
1405 expected to change in the future.
1412 return %{ $self->{'radius_check'} }
1413 if exists $self->{'radius_check'};
1418 my($column, $attrib) = ($1, $2);
1419 #$attrib =~ s/_/\-/g;
1420 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1421 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1423 my $password = $self->_password;
1424 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1426 my $cust_svc = $self->cust_svc;
1427 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1429 my $cust_pkg = $cust_svc->cust_pkg;
1430 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1431 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1440 This method instructs the object to "snapshot" or freeze RADIUS check and
1441 reply attributes to the current values.
1445 #bah, my english is too broken this morning
1446 #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
1447 #the FS::cust_pkg's replace method to trigger the correct export updates when
1448 #package dates change)
1453 $self->{$_} = { $self->$_() }
1454 foreach qw( radius_reply radius_check );
1458 =item forget_snapshot
1460 This methos instructs the object to forget any previously snapshotted
1461 RADIUS check and reply attributes.
1465 sub forget_snapshot {
1469 foreach qw( radius_reply radius_check );
1473 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1475 Returns the domain associated with this account.
1477 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1484 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1485 my $svc_domain = $self->svc_domain(@_)
1486 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1487 $svc_domain->domain;
1492 Returns the FS::svc_domain record for this account's domain (see
1497 # FS::h_svc_acct has a history-aware svc_domain override
1502 ? $self->{'_domsvc'}
1503 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1508 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1512 #inherited from svc_Common
1514 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1516 Returns an email address associated with the account.
1518 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1525 $self->username. '@'. $self->domain(@_);
1530 Returns an array of FS::acct_snarf records associated with the account.
1531 If the acct_snarf table does not exist or there are no associated records,
1532 an empty list is returned
1538 return () unless dbdef->table('acct_snarf');
1539 eval "use FS::acct_snarf;";
1541 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1544 =item decrement_upbytes OCTETS
1546 Decrements the I<upbytes> field of this record by the given amount. If there
1547 is an error, returns the error, otherwise returns false.
1551 sub decrement_upbytes {
1552 shift->_op_usage('-', 'upbytes', @_);
1555 =item increment_upbytes OCTETS
1557 Increments the I<upbytes> field of this record by the given amount. If there
1558 is an error, returns the error, otherwise returns false.
1562 sub increment_upbytes {
1563 shift->_op_usage('+', 'upbytes', @_);
1566 =item decrement_downbytes OCTETS
1568 Decrements the I<downbytes> field of this record by the given amount. If there
1569 is an error, returns the error, otherwise returns false.
1573 sub decrement_downbytes {
1574 shift->_op_usage('-', 'downbytes', @_);
1577 =item increment_downbytes OCTETS
1579 Increments the I<downbytes> field of this record by the given amount. If there
1580 is an error, returns the error, otherwise returns false.
1584 sub increment_downbytes {
1585 shift->_op_usage('+', 'downbytes', @_);
1588 =item decrement_totalbytes OCTETS
1590 Decrements the I<totalbytes> field of this record by the given amount. If there
1591 is an error, returns the error, otherwise returns false.
1595 sub decrement_totalbytes {
1596 shift->_op_usage('-', 'totalbytes', @_);
1599 =item increment_totalbytes OCTETS
1601 Increments the I<totalbytes> field of this record by the given amount. If there
1602 is an error, returns the error, otherwise returns false.
1606 sub increment_totalbytes {
1607 shift->_op_usage('+', 'totalbytes', @_);
1610 =item decrement_seconds SECONDS
1612 Decrements the I<seconds> field of this record by the given amount. If there
1613 is an error, returns the error, otherwise returns false.
1617 sub decrement_seconds {
1618 shift->_op_usage('-', 'seconds', @_);
1621 =item increment_seconds SECONDS
1623 Increments the I<seconds> field of this record by the given amount. If there
1624 is an error, returns the error, otherwise returns false.
1628 sub increment_seconds {
1629 shift->_op_usage('+', 'seconds', @_);
1637 my %op2condition = (
1638 '-' => sub { my($self, $column, $amount) = @_;
1639 $self->$column - $amount <= 0;
1641 '+' => sub { my($self, $column, $amount) = @_;
1642 $self->$column + $amount > 0;
1645 my %op2warncondition = (
1646 '-' => sub { my($self, $column, $amount) = @_;
1647 my $threshold = $column . '_threshold';
1648 $self->$column - $amount <= $self->$threshold + 0;
1650 '+' => sub { my($self, $column, $amount) = @_;
1651 $self->$column + $amount > 0;
1656 my( $self, $op, $column, $amount ) = @_;
1658 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1659 ' ('. $self->email. "): $op $amount\n"
1662 return '' unless $amount;
1664 local $SIG{HUP} = 'IGNORE';
1665 local $SIG{INT} = 'IGNORE';
1666 local $SIG{QUIT} = 'IGNORE';
1667 local $SIG{TERM} = 'IGNORE';
1668 local $SIG{TSTP} = 'IGNORE';
1669 local $SIG{PIPE} = 'IGNORE';
1671 my $oldAutoCommit = $FS::UID::AutoCommit;
1672 local $FS::UID::AutoCommit = 0;
1675 my $sql = "UPDATE svc_acct SET $column = ".
1676 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1677 " $op ? WHERE svcnum = ?";
1681 my $sth = $dbh->prepare( $sql )
1682 or die "Error preparing $sql: ". $dbh->errstr;
1683 my $rv = $sth->execute($amount, $self->svcnum);
1684 die "Error executing $sql: ". $sth->errstr
1685 unless defined($rv);
1686 die "Can't update $column for svcnum". $self->svcnum
1689 my $action = $op2action{$op};
1691 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1692 ( $action eq 'suspend' && !$self->overlimit
1693 || $action eq 'unsuspend' && $self->overlimit )
1695 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1696 if ($part_export->option('overlimit_groups')) {
1698 my $other = new FS::svc_acct $self->hashref;
1699 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1700 ($self, $part_export->option('overlimit_groups'));
1701 $other->usergroup( $groups );
1702 if ($action eq 'suspend'){
1703 $new = $other; $old = $self;
1705 $new = $self; $old = $other;
1707 my $error = $part_export->export_replace($new, $old);
1708 $error ||= $self->overlimit($action);
1710 $dbh->rollback if $oldAutoCommit;
1711 return "Error replacing radius groups in export, ${op}: $error";
1717 if ( $conf->exists("svc_acct-usage_$action")
1718 && &{$op2condition{$op}}($self, $column, $amount) ) {
1719 #my $error = $self->$action();
1720 my $error = $self->cust_svc->cust_pkg->$action();
1721 # $error ||= $self->overlimit($action);
1723 $dbh->rollback if $oldAutoCommit;
1724 return "Error ${action}ing: $error";
1728 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1729 my $wqueue = new FS::queue {
1730 'svcnum' => $self->svcnum,
1731 'job' => 'FS::svc_acct::reached_threshold',
1736 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1740 my $error = $wqueue->insert(
1741 'svcnum' => $self->svcnum,
1743 'column' => $column,
1747 $dbh->rollback if $oldAutoCommit;
1748 return "Error queuing threshold activity: $error";
1752 warn "$me update successful; committing\n"
1754 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1760 my( $self, $valueref, %options ) = @_;
1762 warn "$me set_usage called for svcnum ". $self->svcnum.
1763 ' ('. $self->email. "): ".
1764 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1767 local $SIG{HUP} = 'IGNORE';
1768 local $SIG{INT} = 'IGNORE';
1769 local $SIG{QUIT} = 'IGNORE';
1770 local $SIG{TERM} = 'IGNORE';
1771 local $SIG{TSTP} = 'IGNORE';
1772 local $SIG{PIPE} = 'IGNORE';
1774 local $FS::svc_Common::noexport_hack = 1;
1775 my $oldAutoCommit = $FS::UID::AutoCommit;
1776 local $FS::UID::AutoCommit = 0;
1781 if ( $options{null} ) {
1782 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1783 qw( seconds upbytes downbytes totalbytes )
1786 foreach my $field (keys %$valueref){
1787 $reset = 1 if $valueref->{$field};
1788 $self->setfield($field, $valueref->{$field});
1789 $self->setfield( $field.'_threshold',
1790 int($self->getfield($field)
1791 * ( $conf->exists('svc_acct-usage_threshold')
1792 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1797 $handyhash{$field} = $self->getfield($field);
1798 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1800 #my $error = $self->replace; #NO! we avoid the call to ->check for
1801 #die $error if $error; #services not explicity changed via the UI
1803 my $sql = "UPDATE svc_acct SET " .
1804 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1805 " WHERE svcnum = ". $self->svcnum;
1810 if (scalar(keys %handyhash)) {
1811 my $sth = $dbh->prepare( $sql )
1812 or die "Error preparing $sql: ". $dbh->errstr;
1813 my $rv = $sth->execute();
1814 die "Error executing $sql: ". $sth->errstr
1815 unless defined($rv);
1816 die "Can't update usage for svcnum ". $self->svcnum
1823 if ($self->overlimit) {
1824 $error = $self->overlimit('unsuspend');
1825 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1826 if ($part_export->option('overlimit_groups')) {
1827 my $old = new FS::svc_acct $self->hashref;
1828 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1829 ($self, $part_export->option('overlimit_groups'));
1830 $old->usergroup( $groups );
1831 $error ||= $part_export->export_replace($self, $old);
1836 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1837 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1840 $dbh->rollback if $oldAutoCommit;
1841 return "Error unsuspending: $error";
1845 warn "$me update successful; committing\n"
1847 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1853 =item recharge HASHREF
1855 Increments usage columns by the amount specified in HASHREF as
1856 column=>amount pairs.
1861 my ($self, $vhash) = @_;
1864 warn "[$me] recharge called on $self: ". Dumper($self).
1865 "\nwith vhash: ". Dumper($vhash);
1868 my $oldAutoCommit = $FS::UID::AutoCommit;
1869 local $FS::UID::AutoCommit = 0;
1873 foreach my $column (keys %$vhash){
1874 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1878 $dbh->rollback if $oldAutoCommit;
1880 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1885 =item is_rechargeable
1887 Returns true if this svc_account can be "recharged" and false otherwise.
1891 sub is_rechargable {
1893 $self->seconds ne ''
1894 || $self->upbytes ne ''
1895 || $self->downbytes ne ''
1896 || $self->totalbytes ne '';
1899 =item seconds_since TIMESTAMP
1901 Returns the number of seconds this account has been online since TIMESTAMP,
1902 according to the session monitor (see L<FS::Session>).
1904 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1905 L<Time::Local> and L<Date::Parse> for conversion functions.
1909 #note: POD here, implementation in FS::cust_svc
1912 $self->cust_svc->seconds_since(@_);
1915 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1917 Returns the numbers of seconds this account has been online between
1918 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1919 external SQL radacct table, specified via sqlradius export. Sessions which
1920 started in the specified range but are still open are counted from session
1921 start to the end of the range (unless they are over 1 day old, in which case
1922 they are presumed missing their stop record and not counted). Also, sessions
1923 which end in the range but started earlier are counted from the start of the
1924 range to session end. Finally, sessions which start before the range but end
1925 after are counted for the entire range.
1927 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1928 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1933 #note: POD here, implementation in FS::cust_svc
1934 sub seconds_since_sqlradacct {
1936 $self->cust_svc->seconds_since_sqlradacct(@_);
1939 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1941 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1942 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1943 TIMESTAMP_END (exclusive).
1945 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1946 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1951 #note: POD here, implementation in FS::cust_svc
1952 sub attribute_since_sqlradacct {
1954 $self->cust_svc->attribute_since_sqlradacct(@_);
1957 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1959 Returns an array of hash references of this customers login history for the
1960 given time range. (document this better)
1964 sub get_session_history {
1966 $self->cust_svc->get_session_history(@_);
1969 =item last_login_text
1971 Returns text describing the time of last login.
1975 sub last_login_text {
1977 $self->last_login ? ctime($self->last_login) : 'unknown';
1980 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
1985 my($self, $start, $end, %opt ) = @_;
1987 my $did = $self->username; #yup
1989 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
1991 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
1993 #SELECT $for_update * FROM cdr
1994 # WHERE calldate >= $start #need a conversion
1995 # AND calldate < $end #ditto
1996 # AND ( charged_party = "$did"
1997 # OR charged_party = "$prefix$did" #if length($prefix);
1998 # OR ( ( charged_party IS NULL OR charged_party = '' )
2000 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2003 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2006 if ( length($prefix) ) {
2008 " AND ( charged_party = '$did'
2009 OR charged_party = '$prefix$did'
2010 OR ( ( charged_party IS NULL OR charged_party = '' )
2012 ( src = '$did' OR src = '$prefix$did' )
2018 " AND ( charged_party = '$did'
2019 OR ( ( charged_party IS NULL OR charged_party = '' )
2029 'select' => "$for_update *",
2032 #( freesidestatus IS NULL OR freesidestatus = '' )
2033 'freesidestatus' => '',
2035 'extra_sql' => $charged_or_src,
2043 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2049 if ( $self->usergroup ) {
2050 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2051 unless ref($self->usergroup) eq 'ARRAY';
2052 #when provisioning records, export callback runs in svc_Common.pm before
2053 #radius_usergroup records can be inserted...
2054 @{$self->usergroup};
2056 map { $_->groupname }
2057 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2061 =item clone_suspended
2063 Constructor used by FS::part_export::_export_suspend fallback. Document
2068 sub clone_suspended {
2070 my %hash = $self->hash;
2071 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2072 new FS::svc_acct \%hash;
2075 =item clone_kludge_unsuspend
2077 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2082 sub clone_kludge_unsuspend {
2084 my %hash = $self->hash;
2085 $hash{_password} = '';
2086 new FS::svc_acct \%hash;
2089 =item check_password
2091 Checks the supplied password against the (possibly encrypted) password in the
2092 database. Returns true for a successful authentication, false for no match.
2094 Currently supported encryptions are: classic DES crypt() and MD5
2098 sub check_password {
2099 my($self, $check_password) = @_;
2101 #remove old-style SUSPENDED kludge, they should be allowed to login to
2102 #self-service and pay up
2103 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2105 #eventually should check a "password-encoding" field
2106 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2108 } elsif ( length($password) < 13 ) { #plaintext
2109 $check_password eq $password;
2110 } elsif ( length($password) == 13 ) { #traditional DES crypt
2111 crypt($check_password, $password) eq $password;
2112 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2113 unix_md5_crypt($check_password, $password) eq $password;
2114 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2115 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2116 $self->svcnum. "\n";
2119 warn "Can't check password: Unrecognized encryption for svcnum ".
2120 $self->svcnum. "\n";
2126 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2128 Returns an encrypted password, either by passing through an encrypted password
2129 in the database or by encrypting a plaintext password from the database.
2131 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2132 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2133 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2134 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2135 encryption type is only used if the password is not already encrypted in the
2140 sub crypt_password {
2142 #eventually should check a "password-encoding" field
2143 if ( length($self->_password) == 13
2144 || $self->_password =~ /^\$(1|2a?)\$/
2145 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2150 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2151 if ( $encryption eq 'crypt' ) {
2154 $saltset[int(rand(64))].$saltset[int(rand(64))]
2156 } elsif ( $encryption eq 'md5' ) {
2157 unix_md5_crypt( $self->_password );
2158 } elsif ( $encryption eq 'blowfish' ) {
2159 croak "unknown encryption method $encryption";
2161 croak "unknown encryption method $encryption";
2166 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2168 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2169 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2170 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2172 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2173 to work the same as the B</crypt_password> method.
2179 #eventually should check a "password-encoding" field
2180 if ( length($self->_password) == 13 ) { #crypt
2181 return '{CRYPT}'. $self->_password;
2182 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2184 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2185 warn "Blowfish encryption not supported in this context, svcnum ".
2186 $self->svcnum. "\n";
2187 return '{CRYPT}*'; #unsupported, should not auth
2188 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2189 return '{SSHA}'. $1;
2190 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2191 return '{NS-MTA-MD5}'. $1;
2193 return '{PLAIN}'. $self->_password;
2194 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2195 #if ( $encryption eq 'crypt' ) {
2196 # return '{CRYPT}'. crypt(
2198 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2200 #} elsif ( $encryption eq 'md5' ) {
2201 # unix_md5_crypt( $self->_password );
2202 #} elsif ( $encryption eq 'blowfish' ) {
2203 # croak "unknown encryption method $encryption";
2205 # croak "unknown encryption method $encryption";
2210 =item domain_slash_username
2212 Returns $domain/$username/
2216 sub domain_slash_username {
2218 $self->domain. '/'. $self->username. '/';
2221 =item virtual_maildir
2223 Returns $domain/maildirs/$username/
2227 sub virtual_maildir {
2229 $self->domain. '/maildirs/'. $self->username. '/';
2240 This is the FS::svc_acct job-queue-able version. It still uses
2241 FS::Misc::send_email under-the-hood.
2248 eval "use FS::Misc qw(send_email)";
2251 $opt{mimetype} ||= 'text/plain';
2252 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2254 my $error = send_email(
2255 'from' => $opt{from},
2257 'subject' => $opt{subject},
2258 'content-type' => $opt{mimetype},
2259 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2261 die $error if $error;
2264 =item check_and_rebuild_fuzzyfiles
2268 sub check_and_rebuild_fuzzyfiles {
2269 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2270 -e "$dir/svc_acct.username"
2271 or &rebuild_fuzzyfiles;
2274 =item rebuild_fuzzyfiles
2278 sub rebuild_fuzzyfiles {
2280 use Fcntl qw(:flock);
2282 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2286 open(USERNAMELOCK,">>$dir/svc_acct.username")
2287 or die "can't open $dir/svc_acct.username: $!";
2288 flock(USERNAMELOCK,LOCK_EX)
2289 or die "can't lock $dir/svc_acct.username: $!";
2291 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2293 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2294 or die "can't open $dir/svc_acct.username.tmp: $!";
2295 print USERNAMECACHE join("\n", @all_username), "\n";
2296 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2298 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2308 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2309 open(USERNAMECACHE,"<$dir/svc_acct.username")
2310 or die "can't open $dir/svc_acct.username: $!";
2311 my @array = map { chomp; $_; } <USERNAMECACHE>;
2312 close USERNAMECACHE;
2316 =item append_fuzzyfiles USERNAME
2320 sub append_fuzzyfiles {
2321 my $username = shift;
2323 &check_and_rebuild_fuzzyfiles;
2325 use Fcntl qw(:flock);
2327 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2329 open(USERNAME,">>$dir/svc_acct.username")
2330 or die "can't open $dir/svc_acct.username: $!";
2331 flock(USERNAME,LOCK_EX)
2332 or die "can't lock $dir/svc_acct.username: $!";
2334 print USERNAME "$username\n";
2336 flock(USERNAME,LOCK_UN)
2337 or die "can't unlock $dir/svc_acct.username: $!";
2345 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2349 sub radius_usergroup_selector {
2350 my $sel_groups = shift;
2351 my %sel_groups = map { $_=>1 } @$sel_groups;
2353 my $selectname = shift || 'radius_usergroup';
2356 my $sth = $dbh->prepare(
2357 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2358 ) or die $dbh->errstr;
2359 $sth->execute() or die $sth->errstr;
2360 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2364 function ${selectname}_doadd(object) {
2365 var myvalue = object.${selectname}_add.value;
2366 var optionName = new Option(myvalue,myvalue,false,true);
2367 var length = object.$selectname.length;
2368 object.$selectname.options[length] = optionName;
2369 object.${selectname}_add.value = "";
2372 <SELECT MULTIPLE NAME="$selectname">
2375 foreach my $group ( @all_groups ) {
2376 $html .= qq(<OPTION VALUE="$group");
2377 if ( $sel_groups{$group} ) {
2378 $html .= ' SELECTED';
2379 $sel_groups{$group} = 0;
2381 $html .= ">$group</OPTION>\n";
2383 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2384 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2386 $html .= '</SELECT>';
2388 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2389 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2394 =item reached_threshold
2396 Performs some activities when svc_acct thresholds (such as number of seconds
2397 remaining) are reached.
2401 sub reached_threshold {
2404 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2405 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2407 if ( $opt{'op'} eq '+' ){
2408 $svc_acct->setfield( $opt{'column'}.'_threshold',
2409 int($svc_acct->getfield($opt{'column'})
2410 * ( $conf->exists('svc_acct-usage_threshold')
2411 ? $conf->config('svc_acct-usage_threshold')/100
2416 my $error = $svc_acct->replace;
2417 die $error if $error;
2418 }elsif ( $opt{'op'} eq '-' ){
2420 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2421 return '' if ($threshold eq '' );
2423 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2424 my $error = $svc_acct->replace;
2425 die $error if $error; # email next time, i guess
2427 if ( $warning_template ) {
2428 eval "use FS::Misc qw(send_email)";
2431 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2432 my $cust_main = $cust_pkg->cust_main;
2434 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2435 $cust_main->invoicing_list,
2436 ($opt{'to'} ? $opt{'to'} : ())
2439 my $mimetype = $warning_mimetype;
2440 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2442 my $body = $warning_template->fill_in( HASH => {
2443 'custnum' => $cust_main->custnum,
2444 'username' => $svc_acct->username,
2445 'password' => $svc_acct->_password,
2446 'first' => $cust_main->first,
2447 'last' => $cust_main->getfield('last'),
2448 'pkg' => $cust_pkg->part_pkg->pkg,
2449 'column' => $opt{'column'},
2450 'amount' => $opt{'column'} =~/bytes/
2451 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2452 : $svc_acct->getfield($opt{'column'}),
2453 'threshold' => $opt{'column'} =~/bytes/
2454 ? FS::UI::bytecount::display_bytecount($threshold)
2459 my $error = send_email(
2460 'from' => $warning_from,
2462 'subject' => $warning_subject,
2463 'content-type' => $mimetype,
2464 'body' => [ map "$_\n", split("\n", $body) ],
2466 die $error if $error;
2469 die "unknown op: " . $opt{'op'};
2477 The $recref stuff in sub check should be cleaned up.
2479 The suspend, unsuspend and cancel methods update the database, but not the
2480 current object. This is probably a bug as it's unexpected and
2483 radius_usergroup_selector? putting web ui components in here? they should
2484 probably live somewhere else...
2486 insertion of RADIUS group stuff in insert could be done with child_objects now
2487 (would probably clean up export of them too)
2491 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2492 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2493 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2494 L<freeside-queued>), L<FS::svc_acct_pop>,
2495 schema.html from the base documentation.
2499 =item domain_select_hash %OPTIONS
2501 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2502 may at present purchase.
2504 Currently available options are: I<pkgnum> I<svcpart>
2508 sub domain_select_hash {
2509 my ($self, %options) = @_;
2515 $part_svc = $self->part_svc;
2516 $cust_pkg = $self->cust_svc->cust_pkg
2520 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2521 if $options{'svcpart'};
2523 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2524 if $options{'pkgnum'};
2526 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2527 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2528 %domains = map { $_->svcnum => $_->domain }
2529 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2530 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2531 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2532 %domains = map { $_->svcnum => $_->domain }
2533 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2534 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2535 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2537 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2540 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2541 my $svc_domain = qsearchs('svc_domain',
2542 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2543 if ( $svc_domain ) {
2544 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2546 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2547 $part_svc->part_svc_column('domsvc')->columnvalue;