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
23 use Crypt::PasswdMD5 1.2;
26 use FS::UID qw( datasrc driver_name );
28 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
29 use FS::Msgcat qw(gettext);
30 use FS::UI::bytecount;
36 use FS::cust_main_invoice;
40 use FS::radius_usergroup;
47 @ISA = qw( FS::svc_Common );
50 $me = '[FS::svc_acct]';
52 #ask FS::UID to run this stuff for us later
53 $FS::UID::callback{'FS::svc_acct'} = sub {
55 $dir_prefix = $conf->config('home');
56 @shells = $conf->config('shells');
57 $usernamemin = $conf->config('usernamemin') || 2;
58 $usernamemax = $conf->config('usernamemax');
59 $passwordmin = $conf->config('passwordmin') || 6;
60 $passwordmax = $conf->config('passwordmax') || 8;
61 $username_letter = $conf->exists('username-letter');
62 $username_letterfirst = $conf->exists('username-letterfirst');
63 $username_noperiod = $conf->exists('username-noperiod');
64 $username_nounderscore = $conf->exists('username-nounderscore');
65 $username_nodash = $conf->exists('username-nodash');
66 $username_uppercase = $conf->exists('username-uppercase');
67 $username_ampersand = $conf->exists('username-ampersand');
68 $username_percent = $conf->exists('username-percent');
69 $username_colon = $conf->exists('username-colon');
70 $password_noampersand = $conf->exists('password-noexclamation');
71 $password_noexclamation = $conf->exists('password-noexclamation');
72 $dirhash = $conf->config('dirhash') || 0;
73 if ( $conf->exists('welcome_email') ) {
74 $welcome_template = new Text::Template (
76 SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
77 ) or warn "can't create welcome email template: $Text::Template::ERROR";
78 $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
79 $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
80 $welcome_subject_template = new Text::Template (
82 SOURCE => $welcome_subject,
83 ) or warn "can't create welcome email subject template: $Text::Template::ERROR";
84 $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
86 $welcome_template = '';
88 $welcome_subject = '';
89 $welcome_mimetype = '';
91 if ( $conf->exists('warning_email') ) {
92 $warning_template = new Text::Template (
94 SOURCE => [ map "$_\n", $conf->config('warning_email') ]
95 ) or warn "can't create warning email template: $Text::Template::ERROR";
96 $warning_from = $conf->config('warning_email-from'); # || 'your-isp-is-dum'
97 $warning_subject = $conf->config('warning_email-subject') || 'Warning';
98 $warning_mimetype = $conf->config('warning_email-mimetype') || 'text/plain';
99 $warning_cc = $conf->config('warning_email-cc');
101 $warning_template = '';
103 $warning_subject = '';
104 $warning_mimetype = '';
107 $smtpmachine = $conf->config('smtpmachine');
108 $radius_password = $conf->config('radius-password') || 'Password';
109 $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
110 @pw_set = ( 'A'..'Z' ) if $conf->exists('password-generated-allcaps');
113 @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
114 @pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
118 my ( $hashref, $cache ) = @_;
119 if ( $hashref->{'svc_acct_svcnum'} ) {
120 $self->{'_domsvc'} = FS::svc_domain->new( {
121 'svcnum' => $hashref->{'domsvc'},
122 'domain' => $hashref->{'svc_acct_domain'},
123 'catchall' => $hashref->{'svc_acct_catchall'},
130 FS::svc_acct - Object methods for svc_acct records
136 $record = new FS::svc_acct \%hash;
137 $record = new FS::svc_acct { 'column' => 'value' };
139 $error = $record->insert;
141 $error = $new_record->replace($old_record);
143 $error = $record->delete;
145 $error = $record->check;
147 $error = $record->suspend;
149 $error = $record->unsuspend;
151 $error = $record->cancel;
153 %hash = $record->radius;
155 %hash = $record->radius_reply;
157 %hash = $record->radius_check;
159 $domain = $record->domain;
161 $svc_domain = $record->svc_domain;
163 $email = $record->email;
165 $seconds_since = $record->seconds_since($timestamp);
169 An FS::svc_acct object represents an account. FS::svc_acct inherits from
170 FS::svc_Common. The following fields are currently supported:
174 =item svcnum - primary key (assigned automatcially for new accounts)
178 =item _password - generated if blank
180 =item sec_phrase - security phrase
182 =item popnum - Point of presence (see L<FS::svc_acct_pop>)
190 =item dir - set automatically if blank (and uid is not)
194 =item quota - (unimplementd)
196 =item slipip - IP address
206 =item domsvc - svcnum from svc_domain
208 =item radius_I<Radius_Attribute> - I<Radius-Attribute> (reply)
210 =item rc_I<Radius_Attribute> - I<Radius-Attribute> (check)
220 Creates a new account. To add the account to the database, see L<"insert">.
227 'longname_plural' => 'Access accounts and mailboxes',
228 'sorts' => [ 'username', 'uid', 'last_login', ],
229 'display_weight' => 10,
230 'cancel_weight' => 50,
232 'dir' => 'Home directory',
235 def_label => 'UID (set to fixed and blank for no UIDs)',
238 'slipip' => 'IP address',
239 # 'popnum' => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
241 label => 'Access number',
243 select_table => 'svc_acct_pop',
244 select_key => 'popnum',
245 select_label => 'city',
251 disable_default => 1,
258 disable_inventory => 1,
261 '_password' => 'Password',
264 def_label => 'GID (when blank, defaults to UID)',
268 #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)',
270 def_label=> 'Shell (set to blank for no shell tracking)',
272 select_list => [ $conf->config('shells') ],
273 disable_inventory => 1,
276 'finger' => 'Real name', # (GECOS)',
279 #def_label => 'svcnum from svc_domain',
281 select_table => 'svc_domain',
282 select_key => 'svcnum',
283 select_label => 'domain',
284 disable_inventory => 1,
288 label => 'RADIUS groups',
289 type => 'radius_usergroup_selector',
290 disable_inventory => 1,
293 'seconds' => { label => 'Seconds',
295 disable_inventory => 1,
297 disable_part_svc_column => 1,
299 'upbytes' => { label => 'Upload',
301 disable_inventory => 1,
303 'format' => \&FS::UI::bytecount::display_bytecount,
304 'parse' => \&FS::UI::bytecount::parse_bytecount,
305 disable_part_svc_column => 1,
307 'downbytes' => { label => 'Download',
309 disable_inventory => 1,
311 'format' => \&FS::UI::bytecount::display_bytecount,
312 'parse' => \&FS::UI::bytecount::parse_bytecount,
313 disable_part_svc_column => 1,
315 'totalbytes'=> { label => 'Total up and download',
317 disable_inventory => 1,
319 'format' => \&FS::UI::bytecount::display_bytecount,
320 'parse' => \&FS::UI::bytecount::parse_bytecount,
321 disable_part_svc_column => 1,
323 'seconds_threshold' => { label => 'Seconds threshold',
325 disable_inventory => 1,
327 disable_part_svc_column => 1,
329 'upbytes_threshold' => { label => 'Upload threshold',
331 disable_inventory => 1,
333 'format' => \&FS::UI::bytecount::display_bytecount,
334 'parse' => \&FS::UI::bytecount::parse_bytecount,
335 disable_part_svc_column => 1,
337 'downbytes_threshold' => { label => 'Download threshold',
339 disable_inventory => 1,
341 'format' => \&FS::UI::bytecount::display_bytecount,
342 'parse' => \&FS::UI::bytecount::parse_bytecount,
343 disable_part_svc_column => 1,
345 'totalbytes_threshold'=> { label => 'Total up and download threshold',
347 disable_inventory => 1,
349 'format' => \&FS::UI::bytecount::display_bytecount,
350 'parse' => \&FS::UI::bytecount::parse_bytecount,
351 disable_part_svc_column => 1,
354 label => 'Last login',
358 label => 'Last logout',
365 sub table { 'svc_acct'; }
369 #false laziness with edit/svc_acct.cgi
371 my( $self, $groups ) = @_;
372 if ( ref($groups) eq 'ARRAY' ) {
374 } elsif ( length($groups) ) {
375 [ split(/\s*,\s*/, $groups) ];
384 shift->_lastlog('in', @_);
388 shift->_lastlog('out', @_);
392 my( $self, $op, $time ) = @_;
394 if ( defined($time) ) {
395 warn "$me last_log$op called on svcnum ". $self->svcnum.
396 ' ('. $self->email. "): $time\n"
401 my $sql = "UPDATE svc_acct SET last_log$op = ? WHERE svcnum = ?";
405 my $sth = $dbh->prepare( $sql )
406 or die "Error preparing $sql: ". $dbh->errstr;
407 my $rv = $sth->execute($time, $self->svcnum);
408 die "Error executing $sql: ". $sth->errstr
410 die "Can't update last_log$op for svcnum". $self->svcnum
413 $self->{'Hash'}->{"last_log$op"} = $time;
415 $self->getfield("last_log$op");
419 =item search_sql STRING
421 Class method which returns an SQL fragment to search for the given string.
426 my( $class, $string ) = @_;
427 if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
428 my( $username, $domain ) = ( $1, $2 );
429 my $q_username = dbh->quote($username);
430 my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
432 "svc_acct.username = $q_username AND ( ".
433 join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
438 } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
440 $class->search_sql_field('slipip', $string ).
442 $class->search_sql_field('username', $string ).
445 $class->search_sql_field('username', $string);
449 =item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
451 Returns the "username@domain" string for this account.
453 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
463 =item label_long [ END_TIMESTAMP [ START_TIMESTAMP ] ]
465 Returns a longer string label for this acccount ("Real Name <username@domain>"
466 if available, or "username@domain").
468 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
475 my $label = $self->label(@_);
476 my $finger = $self->finger;
477 return $label unless $finger =~ /\S/;
478 my $maxlen = 40 - length($label) - length($self->cust_svc->part_svc->svc);
479 $finger = substr($finger, 0, $maxlen-3).'...' if length($finger) > $maxlen;
483 =item insert [ , OPTION => VALUE ... ]
485 Adds this account to the database. If there is an error, returns the error,
486 otherwise returns false.
488 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
489 defined. An FS::cust_svc record will be created and inserted.
491 The additional field I<usergroup> can optionally be defined; if so it should
492 contain an arrayref of group names. See L<FS::radius_usergroup>.
494 The additional field I<child_objects> can optionally be defined; if so it
495 should contain an arrayref of FS::tablename objects. They will have their
496 svcnum fields set and will be inserted after this record, but before any
497 exports are run. Each element of the array can also optionally be a
498 two-element array reference containing the child object and the name of an
499 alternate field to be filled in with the newly-inserted svcnum, for example
500 C<[ $svc_forward, 'srcsvc' ]>
502 Currently available options are: I<depend_jobnum>
504 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
505 jobnums), all provisioning jobs will have a dependancy on the supplied
506 jobnum(s) (they will not run until the specific job(s) complete(s)).
508 (TODOC: L<FS::queue> and L<freeside-queued>)
510 (TODOC: new exports!)
519 warn "[$me] insert called on $self: ". Dumper($self).
520 "\nwith options: ". Dumper(%options);
523 local $SIG{HUP} = 'IGNORE';
524 local $SIG{INT} = 'IGNORE';
525 local $SIG{QUIT} = 'IGNORE';
526 local $SIG{TERM} = 'IGNORE';
527 local $SIG{TSTP} = 'IGNORE';
528 local $SIG{PIPE} = 'IGNORE';
530 my $oldAutoCommit = $FS::UID::AutoCommit;
531 local $FS::UID::AutoCommit = 0;
534 my $error = $self->check;
535 return $error if $error;
537 if ( $self->svcnum && qsearchs('cust_svc',{'svcnum'=>$self->svcnum}) ) {
538 my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
539 unless ( $cust_svc ) {
540 $dbh->rollback if $oldAutoCommit;
541 return "no cust_svc record found for svcnum ". $self->svcnum;
543 $self->pkgnum($cust_svc->pkgnum);
544 $self->svcpart($cust_svc->svcpart);
547 $error = $self->_check_duplicate;
549 $dbh->rollback if $oldAutoCommit;
553 # set usage fields and thresholds if unset but set in a package def
554 if ( $self->pkgnum ) {
555 my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
556 my $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
557 if ( $part_pkg && $part_pkg->can('usage_valuehash') ) {
559 my %values = $part_pkg->usage_valuehash;
560 my $multiplier = $conf->exists('svc_acct-usage_threshold')
561 ? 1 - $conf->config('svc_acct-usage_threshold')/100
562 : 0.20; #doesn't matter
564 foreach ( keys %values ) {
565 next if $self->getfield($_);
566 $self->setfield( $_, $values{$_} );
567 $self->setfield( $_. '_threshold', int( $values{$_} * $multiplier ) )
568 if $conf->exists('svc_acct-usage_threshold');
575 $error = $self->SUPER::insert(
576 'jobnums' => \@jobnums,
577 'child_objects' => $self->child_objects,
581 $dbh->rollback if $oldAutoCommit;
585 if ( $self->usergroup ) {
586 foreach my $groupname ( @{$self->usergroup} ) {
587 my $radius_usergroup = new FS::radius_usergroup ( {
588 svcnum => $self->svcnum,
589 groupname => $groupname,
591 my $error = $radius_usergroup->insert;
593 $dbh->rollback if $oldAutoCommit;
599 unless ( $skip_fuzzyfiles ) {
600 $error = $self->queue_fuzzyfiles_update;
602 $dbh->rollback if $oldAutoCommit;
603 return "updating fuzzy search cache: $error";
607 my $cust_pkg = $self->cust_svc->cust_pkg;
610 my $cust_main = $cust_pkg->cust_main;
612 if ( $conf->exists('emailinvoiceautoalways')
613 || $conf->exists('emailinvoiceauto')
614 && ! $cust_main->invoicing_list_emailonly
616 my @invoicing_list = $cust_main->invoicing_list;
617 push @invoicing_list, $self->email;
618 $cust_main->invoicing_list(\@invoicing_list);
623 if ( $welcome_template && $cust_pkg ) {
624 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
628 'custnum' => $self->custnum,
629 'username' => $self->username,
630 'password' => $self->_password,
631 'first' => $cust_main->first,
632 'last' => $cust_main->getfield('last'),
633 'pkg' => $cust_pkg->part_pkg->pkg,
635 my $wqueue = new FS::queue {
636 'svcnum' => $self->svcnum,
637 'job' => 'FS::svc_acct::send_email'
639 my $error = $wqueue->insert(
641 'from' => $welcome_from,
642 'subject' => $welcome_subject_template->fill_in( HASH => \%hash, ),
643 'mimetype' => $welcome_mimetype,
644 'body' => $welcome_template->fill_in( HASH => \%hash, ),
647 $dbh->rollback if $oldAutoCommit;
648 return "error queuing welcome email: $error";
651 if ( $options{'depend_jobnum'} ) {
652 warn "$me depend_jobnum found; adding to welcome email dependancies"
654 if ( ref($options{'depend_jobnum'}) ) {
655 warn "$me adding jobs ". join(', ', @{$options{'depend_jobnum'}} ).
656 "to welcome email dependancies"
658 push @jobnums, @{ $options{'depend_jobnum'} };
660 warn "$me adding job $options{'depend_jobnum'} ".
661 "to welcome email dependancies"
663 push @jobnums, $options{'depend_jobnum'};
667 foreach my $jobnum ( @jobnums ) {
668 my $error = $wqueue->depend_insert($jobnum);
670 $dbh->rollback if $oldAutoCommit;
671 return "error queuing welcome email job dependancy: $error";
681 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
687 Deletes this account from the database. If there is an error, returns the
688 error, otherwise returns false.
690 The corresponding FS::cust_svc record will be deleted as well.
692 (TODOC: new exports!)
699 return "can't delete system account" if $self->_check_system;
701 return "Can't delete an account which is a (svc_forward) source!"
702 if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
704 return "Can't delete an account which is a (svc_forward) destination!"
705 if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
707 return "Can't delete an account with (svc_www) web service!"
708 if qsearch( 'svc_www', { 'usersvc' => $self->svcnum } );
710 # what about records in session ? (they should refer to history table)
712 local $SIG{HUP} = 'IGNORE';
713 local $SIG{INT} = 'IGNORE';
714 local $SIG{QUIT} = 'IGNORE';
715 local $SIG{TERM} = 'IGNORE';
716 local $SIG{TSTP} = 'IGNORE';
717 local $SIG{PIPE} = 'IGNORE';
719 my $oldAutoCommit = $FS::UID::AutoCommit;
720 local $FS::UID::AutoCommit = 0;
723 foreach my $cust_main_invoice (
724 qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
726 unless ( defined($cust_main_invoice) ) {
727 warn "WARNING: something's wrong with qsearch";
730 my %hash = $cust_main_invoice->hash;
731 $hash{'dest'} = $self->email;
732 my $new = new FS::cust_main_invoice \%hash;
733 my $error = $new->replace($cust_main_invoice);
735 $dbh->rollback if $oldAutoCommit;
740 foreach my $svc_domain (
741 qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
743 my %hash = new FS::svc_domain->hash;
744 $hash{'catchall'} = '';
745 my $new = new FS::svc_domain \%hash;
746 my $error = $new->replace($svc_domain);
748 $dbh->rollback if $oldAutoCommit;
753 my $error = $self->SUPER::delete;
755 $dbh->rollback if $oldAutoCommit;
759 foreach my $radius_usergroup (
760 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
762 my $error = $radius_usergroup->delete;
764 $dbh->rollback if $oldAutoCommit;
769 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
773 =item replace OLD_RECORD
775 Replaces OLD_RECORD with this one in the database. If there is an error,
776 returns the error, otherwise returns false.
778 The additional field I<usergroup> can optionally be defined; if so it should
779 contain an arrayref of group names. See L<FS::radius_usergroup>.
785 my ( $new, $old ) = ( shift, shift );
787 warn "$me replacing $old with $new\n" if $DEBUG;
789 # We absolutely have to have an old vs. new record to make this work.
790 if (!defined($old)) {
791 $old = qsearchs( 'svc_acct', { 'svcnum' => $new->svcnum } );
794 return "can't modify system account" if $old->_check_system;
797 #no warnings 'numeric'; #alas, a 5.006-ism
800 foreach my $xid (qw( uid gid )) {
802 return "Can't change $xid!"
803 if ! $conf->exists("svc_acct-edit_$xid")
804 && $old->$xid() != $new->$xid()
805 && $new->cust_svc->part_svc->part_svc_column($xid)->columnflag ne 'F'
810 #change homdir when we change username
811 $new->setfield('dir', '') if $old->username ne $new->username;
813 local $SIG{HUP} = 'IGNORE';
814 local $SIG{INT} = 'IGNORE';
815 local $SIG{QUIT} = 'IGNORE';
816 local $SIG{TERM} = 'IGNORE';
817 local $SIG{TSTP} = 'IGNORE';
818 local $SIG{PIPE} = 'IGNORE';
820 my $oldAutoCommit = $FS::UID::AutoCommit;
821 local $FS::UID::AutoCommit = 0;
824 # redundant, but so $new->usergroup gets set
825 $error = $new->check;
826 return $error if $error;
828 $old->usergroup( [ $old->radius_groups ] );
830 warn $old->email. " old groups: ". join(' ',@{$old->usergroup}). "\n";
831 warn $new->email. "new groups: ". join(' ',@{$new->usergroup}). "\n";
833 if ( $new->usergroup ) {
834 #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
835 my @newgroups = @{$new->usergroup};
836 foreach my $oldgroup ( @{$old->usergroup} ) {
837 if ( grep { $oldgroup eq $_ } @newgroups ) {
838 @newgroups = grep { $oldgroup ne $_ } @newgroups;
841 my $radius_usergroup = qsearchs('radius_usergroup', {
842 svcnum => $old->svcnum,
843 groupname => $oldgroup,
845 my $error = $radius_usergroup->delete;
847 $dbh->rollback if $oldAutoCommit;
848 return "error deleting radius_usergroup $oldgroup: $error";
852 foreach my $newgroup ( @newgroups ) {
853 my $radius_usergroup = new FS::radius_usergroup ( {
854 svcnum => $new->svcnum,
855 groupname => $newgroup,
857 my $error = $radius_usergroup->insert;
859 $dbh->rollback if $oldAutoCommit;
860 return "error adding radius_usergroup $newgroup: $error";
866 if ( $old->username ne $new->username || $old->domsvc != $new->domsvc ) {
867 $new->svcpart( $new->cust_svc->svcpart ) unless $new->svcpart;
868 $error = $new->_check_duplicate;
870 $dbh->rollback if $oldAutoCommit;
875 $error = $new->SUPER::replace($old);
877 $dbh->rollback if $oldAutoCommit;
878 return $error if $error;
881 if ( $new->username ne $old->username && ! $skip_fuzzyfiles ) {
882 $error = $new->queue_fuzzyfiles_update;
884 $dbh->rollback if $oldAutoCommit;
885 return "updating fuzzy search cache: $error";
889 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
893 =item queue_fuzzyfiles_update
895 Used by insert & replace to update the fuzzy search cache
899 sub queue_fuzzyfiles_update {
902 local $SIG{HUP} = 'IGNORE';
903 local $SIG{INT} = 'IGNORE';
904 local $SIG{QUIT} = 'IGNORE';
905 local $SIG{TERM} = 'IGNORE';
906 local $SIG{TSTP} = 'IGNORE';
907 local $SIG{PIPE} = 'IGNORE';
909 my $oldAutoCommit = $FS::UID::AutoCommit;
910 local $FS::UID::AutoCommit = 0;
913 my $queue = new FS::queue {
914 'svcnum' => $self->svcnum,
915 'job' => 'FS::svc_acct::append_fuzzyfiles'
917 my $error = $queue->insert($self->username);
919 $dbh->rollback if $oldAutoCommit;
920 return "queueing job (transaction rolled back): $error";
923 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
931 Suspends this account by calling export-specific suspend hooks. If there is
932 an error, returns the error, otherwise returns false.
934 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
940 return "can't suspend system account" if $self->_check_system;
941 $self->SUPER::suspend;
946 Unsuspends this account by by calling export-specific suspend hooks. If there
947 is an error, returns the error, otherwise returns false.
949 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
955 my %hash = $self->hash;
956 if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
957 $hash{_password} = $1;
958 my $new = new FS::svc_acct ( \%hash );
959 my $error = $new->replace($self);
960 return $error if $error;
963 $self->SUPER::unsuspend;
968 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
970 If the B<auto_unset_catchall> configuration option is set, this method will
971 automatically remove any references to the canceled service in the catchall
972 field of svc_domain. This allows packages that contain both a svc_domain and
973 its catchall svc_acct to be canceled in one step.
978 # Only one thing to do at this level
980 foreach my $svc_domain (
981 qsearch( 'svc_domain', { catchall => $self->svcnum } ) ) {
982 if($conf->exists('auto_unset_catchall')) {
983 my %hash = $svc_domain->hash;
984 $hash{catchall} = '';
985 my $new = new FS::svc_domain ( \%hash );
986 my $error = $new->replace($svc_domain);
987 return $error if $error;
989 return "cannot unprovision svc_acct #".$self->svcnum.
990 " while assigned as catchall for svc_domain #".$svc_domain->svcnum;
994 $self->SUPER::cancel;
1000 Checks all fields to make sure this is a valid service. If there is an error,
1001 returns the error, otherwise returns false. Called by the insert and replace
1004 Sets any fixed values; see L<FS::part_svc>.
1011 my($recref) = $self->hashref;
1013 my $x = $self->setfixed( $self->_fieldhandlers );
1014 return $x unless ref($x);
1017 if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
1019 [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
1022 my $error = $self->ut_numbern('svcnum')
1023 #|| $self->ut_number('domsvc')
1024 || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
1025 || $self->ut_textn('sec_phrase')
1026 || $self->ut_snumbern('seconds')
1027 || $self->ut_snumbern('upbytes')
1028 || $self->ut_snumbern('downbytes')
1029 || $self->ut_snumbern('totalbytes')
1031 return $error if $error;
1033 my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
1034 if ( $username_uppercase ) {
1035 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i
1036 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1037 $recref->{username} = $1;
1039 $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/
1040 or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
1041 $recref->{username} = $1;
1044 if ( $username_letterfirst ) {
1045 $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
1046 } elsif ( $username_letter ) {
1047 $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
1049 if ( $username_noperiod ) {
1050 $recref->{username} =~ /\./ and return gettext('illegal_username');
1052 if ( $username_nounderscore ) {
1053 $recref->{username} =~ /_/ and return gettext('illegal_username');
1055 if ( $username_nodash ) {
1056 $recref->{username} =~ /\-/ and return gettext('illegal_username');
1058 unless ( $username_ampersand ) {
1059 $recref->{username} =~ /\&/ and return gettext('illegal_username');
1061 if ( $password_noampersand ) {
1062 $recref->{_password} =~ /\&/ and return gettext('illegal_password');
1064 if ( $password_noexclamation ) {
1065 $recref->{_password} =~ /\!/ and return gettext('illegal_password');
1067 unless ( $username_percent ) {
1068 $recref->{username} =~ /\%/ and return gettext('illegal_username');
1070 unless ( $username_colon ) {
1071 $recref->{username} =~ /\:/ and return gettext('illegal_username');
1074 $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
1075 $recref->{popnum} = $1;
1076 return "Unknown popnum" unless
1077 ! $recref->{popnum} ||
1078 qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
1080 unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
1082 $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
1083 $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
1085 $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
1086 $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
1087 #not all systems use gid=uid
1088 #you can set a fixed gid in part_svc
1090 return "Only root can have uid 0"
1091 if $recref->{uid} == 0
1092 && $recref->{username} !~ /^(root|toor|smtp)$/;
1094 unless ( $recref->{username} eq 'sync' ) {
1095 if ( grep $_ eq $recref->{shell}, @shells ) {
1096 $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
1098 return "Illegal shell \`". $self->shell. "\'; ".
1099 $conf->dir. "/shells contains: @shells";
1102 $recref->{shell} = '/bin/sync';
1106 $recref->{gid} ne '' ?
1107 return "Can't have gid without uid" : ( $recref->{gid}='' );
1108 #$recref->{dir} ne '' ?
1109 # return "Can't have directory without uid" : ( $recref->{dir}='' );
1110 $recref->{shell} ne '' ?
1111 return "Can't have shell without uid" : ( $recref->{shell}='' );
1114 unless ( $part_svc->part_svc_column('dir')->columnflag eq 'F' ) {
1116 $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
1117 or return "Illegal directory: ". $recref->{dir};
1118 $recref->{dir} = $1;
1119 return "Illegal directory"
1120 if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
1121 return "Illegal directory"
1122 if $recref->{dir} =~ /\&/ && ! $username_ampersand;
1123 unless ( $recref->{dir} ) {
1124 $recref->{dir} = $dir_prefix . '/';
1125 if ( $dirhash > 0 ) {
1126 for my $h ( 1 .. $dirhash ) {
1127 $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
1129 } elsif ( $dirhash < 0 ) {
1130 for my $h ( reverse $dirhash .. -1 ) {
1131 $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
1134 $recref->{dir} .= $recref->{username};
1140 # $error = $self->ut_textn('finger');
1141 # return $error if $error;
1142 if ( $self->getfield('finger') eq '' ) {
1143 my $cust_pkg = $self->svcnum
1144 ? $self->cust_svc->cust_pkg
1145 : qsearchs('cust_pkg', { 'pkgnum' => $self->getfield('pkgnum') } );
1147 my $cust_main = $cust_pkg->cust_main;
1148 $self->setfield('finger', $cust_main->first.' '.$cust_main->get('last') );
1151 $self->getfield('finger') =~
1152 /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
1153 or return "Illegal finger: ". $self->getfield('finger');
1154 $self->setfield('finger', $1);
1156 $recref->{quota} =~ /^(\w*)$/ or return "Illegal quota";
1157 $recref->{quota} = $1;
1159 unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
1160 if ( $recref->{slipip} eq '' ) {
1161 $recref->{slipip} = '';
1162 } elsif ( $recref->{slipip} eq '0e0' ) {
1163 $recref->{slipip} = '0e0';
1165 $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
1166 or return "Illegal slipip: ". $self->slipip;
1167 $recref->{slipip} = $1;
1172 #arbitrary RADIUS stuff; allow ut_textn for now
1173 foreach ( grep /^radius_/, fields('svc_acct') ) {
1174 $self->ut_textn($_);
1177 #generate a password if it is blank
1178 $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
1179 unless ( $recref->{_password} );
1181 #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
1182 if ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
1183 $recref->{_password} = $1.$3;
1184 #uncomment this to encrypt password immediately upon entry, or run
1185 #bin/crypt_pw in cron to give new users a window during which their
1186 #password is available to techs, for faxing, etc. (also be aware of
1188 #$recref->{password} = $1.
1189 # crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
1191 } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* |!!?)?)([\w\.\/\$\;\+]{13,64})$/ ) {
1192 $recref->{_password} = $1.$3;
1193 } elsif ( $recref->{_password} eq '*' ) {
1194 $recref->{_password} = '*';
1195 } elsif ( $recref->{_password} eq '!' ) {
1196 $recref->{_password} = '!';
1197 } elsif ( $recref->{_password} eq '!!' ) {
1198 $recref->{_password} = '!!';
1200 #return "Illegal password";
1201 return gettext('illegal_password'). " $passwordmin-$passwordmax ".
1202 FS::Msgcat::_gettext('illegal_password_characters').
1203 ": ". $recref->{_password};
1206 $self->SUPER::check;
1211 Internal function to check the username against the list of system usernames
1212 from the I<system_usernames> configuration value. Returns true if the username
1213 is listed on the system username list.
1219 scalar( grep { $self->username eq $_ || $self->email eq $_ }
1220 $conf->config('system_usernames')
1224 =item _check_duplicate
1226 Internal function to check for duplicates usernames, username@domain pairs and
1229 If the I<global_unique-username> configuration value is set to B<username> or
1230 B<username@domain>, enforces global username or username@domain uniqueness.
1232 In all cases, check for duplicate uids and usernames or username@domain pairs
1233 per export and with identical I<svcpart> values.
1237 sub _check_duplicate {
1240 my $global_unique = $conf->config('global_unique-username') || 'none';
1241 return '' if $global_unique eq 'disabled';
1243 warn "$me locking svc_acct table for duplicate search" if $DEBUG;
1244 if ( driver_name =~ /^Pg/i ) {
1245 dbh->do("LOCK TABLE svc_acct IN SHARE ROW EXCLUSIVE MODE")
1247 } elsif ( driver_name =~ /^mysql/i ) {
1248 dbh->do("SELECT * FROM duplicate_lock
1249 WHERE lockname = 'svc_acct'
1251 ) or die dbh->errstr;
1253 die "unknown database ". driver_name.
1254 "; don't know how to lock for duplicate search";
1256 warn "$me acquired svc_acct table lock for duplicate search" if $DEBUG;
1258 my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
1259 unless ( $part_svc ) {
1260 return 'unknown svcpart '. $self->svcpart;
1263 my @dup_user = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1264 qsearch( 'svc_acct', { 'username' => $self->username } );
1265 return gettext('username_in_use')
1266 if $global_unique eq 'username' && @dup_user;
1268 my @dup_userdomain = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1269 qsearch( 'svc_acct', { 'username' => $self->username,
1270 'domsvc' => $self->domsvc } );
1271 return gettext('username_in_use')
1272 if $global_unique eq 'username@domain' && @dup_userdomain;
1275 if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
1276 && $self->username !~ /^(toor|(hyla)?fax)$/ ) {
1277 @dup_uid = grep { !$self->svcnum || $_->svcnum != $self->svcnum }
1278 qsearch( 'svc_acct', { 'uid' => $self->uid } );
1283 if ( @dup_user || @dup_userdomain || @dup_uid ) {
1284 my $exports = FS::part_export::export_info('svc_acct');
1285 my %conflict_user_svcpart;
1286 my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
1288 foreach my $part_export ( $part_svc->part_export ) {
1290 #this will catch to the same exact export
1291 my @svcparts = map { $_->svcpart } $part_export->export_svc;
1293 #this will catch to exports w/same exporthost+type ???
1294 #my @other_part_export = qsearch('part_export', {
1295 # 'machine' => $part_export->machine,
1296 # 'exporttype' => $part_export->exporttype,
1298 #foreach my $other_part_export ( @other_part_export ) {
1299 # push @svcparts, map { $_->svcpart }
1300 # qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
1303 #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
1304 #silly kludge to avoid uninitialized value errors
1305 my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
1306 ? $exports->{$part_export->exporttype}{'nodomain'}
1308 if ( $nodomain =~ /^Y/i ) {
1309 $conflict_user_svcpart{$_} = $part_export->exportnum
1312 $conflict_userdomain_svcpart{$_} = $part_export->exportnum
1317 foreach my $dup_user ( @dup_user ) {
1318 my $dup_svcpart = $dup_user->cust_svc->svcpart;
1319 if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
1320 return "duplicate username ". $self->username.
1321 ": conflicts with svcnum ". $dup_user->svcnum.
1322 " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
1326 foreach my $dup_userdomain ( @dup_userdomain ) {
1327 my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
1328 if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1329 return "duplicate username\@domain ". $self->email.
1330 ": conflicts with svcnum ". $dup_userdomain->svcnum.
1331 " via exportnum ". $conflict_userdomain_svcpart{$dup_svcpart};
1335 foreach my $dup_uid ( @dup_uid ) {
1336 my $dup_svcpart = $dup_uid->cust_svc->svcpart;
1337 if ( exists($conflict_user_svcpart{$dup_svcpart})
1338 || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
1339 return "duplicate uid ". $self->uid.
1340 ": conflicts with svcnum ". $dup_uid->svcnum.
1342 ( $conflict_user_svcpart{$dup_svcpart}
1343 || $conflict_userdomain_svcpart{$dup_svcpart} );
1355 Depriciated, use radius_reply instead.
1360 carp "FS::svc_acct::radius depriciated, use radius_reply";
1361 $_[0]->radius_reply;
1366 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1367 reply attributes of this record.
1369 Note that this is now the preferred method for reading RADIUS attributes -
1370 accessing the columns directly is discouraged, as the column names are
1371 expected to change in the future.
1378 return %{ $self->{'radius_reply'} }
1379 if exists $self->{'radius_reply'};
1384 my($column, $attrib) = ($1, $2);
1385 #$attrib =~ s/_/\-/g;
1386 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1387 } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
1389 if ( $self->slipip && $self->slipip ne '0e0' ) {
1390 $reply{$radius_ip} = $self->slipip;
1393 if ( $self->seconds !~ /^$/ ) {
1394 $reply{'Session-Timeout'} = $self->seconds;
1397 if ( $conf->exists('radius-chillispot-max') ) {
1398 #http://dev.coova.org/svn/coova-chilli/doc/dictionary.chillispot
1400 #hmm. just because sqlradius.pm says so?
1407 foreach my $what (qw( input output total )) {
1408 my $is = $whatis{$what}.'bytes';
1409 if ( $self->$is() =~ /\d/ ) {
1410 my $big = new Math::BigInt $self->$is();
1411 my $att = "Chillispot-Max-\u$what";
1412 $reply{"$att-Octets"} = $big->copy->band(0xffffffff)->bstr;
1413 $reply{"$att-Gigawords"} = $big->copy->brsft(32)->bstr;
1424 Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
1425 check attributes of this record.
1427 Note that this is now the preferred method for reading RADIUS attributes -
1428 accessing the columns directly is discouraged, as the column names are
1429 expected to change in the future.
1436 return %{ $self->{'radius_check'} }
1437 if exists $self->{'radius_check'};
1442 my($column, $attrib) = ($1, $2);
1443 #$attrib =~ s/_/\-/g;
1444 ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
1445 } grep { /^rc_/ && $self->getfield($_) } fields( $self->table );
1447 my $password = $self->_password;
1448 my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password'; $check{$pw_attrib} = $password;
1450 my $cust_svc = $self->cust_svc;
1451 die "FATAL: no cust_svc record for svc_acct.svcnum ". $self->svcnum. "\n"
1453 my $cust_pkg = $cust_svc->cust_pkg;
1454 if ( $cust_pkg && $cust_pkg->part_pkg->is_prepaid && $cust_pkg->bill ) {
1455 $check{'Expiration'} = time2str('%B %e %Y %T', $cust_pkg->bill ); #http://lists.cistron.nl/pipermail/freeradius-users/2005-January/040184.html
1464 This method instructs the object to "snapshot" or freeze RADIUS check and
1465 reply attributes to the current values.
1469 #bah, my english is too broken this morning
1470 #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
1471 #the FS::cust_pkg's replace method to trigger the correct export updates when
1472 #package dates change)
1477 $self->{$_} = { $self->$_() }
1478 foreach qw( radius_reply radius_check );
1482 =item forget_snapshot
1484 This methos instructs the object to forget any previously snapshotted
1485 RADIUS check and reply attributes.
1489 sub forget_snapshot {
1493 foreach qw( radius_reply radius_check );
1497 =item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1499 Returns the domain associated with this account.
1501 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1508 die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
1509 my $svc_domain = $self->svc_domain(@_)
1510 or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
1511 $svc_domain->domain;
1516 Returns the FS::svc_domain record for this account's domain (see
1521 # FS::h_svc_acct has a history-aware svc_domain override
1526 ? $self->{'_domsvc'}
1527 : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
1532 Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
1536 #inherited from svc_Common
1538 =item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
1540 Returns an email address associated with the account.
1542 END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
1549 $self->username. '@'. $self->domain(@_);
1554 Returns an array of FS::acct_snarf records associated with the account.
1555 If the acct_snarf table does not exist or there are no associated records,
1556 an empty list is returned
1562 return () unless dbdef->table('acct_snarf');
1563 eval "use FS::acct_snarf;";
1565 qsearch('acct_snarf', { 'svcnum' => $self->svcnum } );
1568 =item decrement_upbytes OCTETS
1570 Decrements the I<upbytes> field of this record by the given amount. If there
1571 is an error, returns the error, otherwise returns false.
1575 sub decrement_upbytes {
1576 shift->_op_usage('-', 'upbytes', @_);
1579 =item increment_upbytes OCTETS
1581 Increments the I<upbytes> field of this record by the given amount. If there
1582 is an error, returns the error, otherwise returns false.
1586 sub increment_upbytes {
1587 shift->_op_usage('+', 'upbytes', @_);
1590 =item decrement_downbytes OCTETS
1592 Decrements the I<downbytes> field of this record by the given amount. If there
1593 is an error, returns the error, otherwise returns false.
1597 sub decrement_downbytes {
1598 shift->_op_usage('-', 'downbytes', @_);
1601 =item increment_downbytes OCTETS
1603 Increments the I<downbytes> field of this record by the given amount. If there
1604 is an error, returns the error, otherwise returns false.
1608 sub increment_downbytes {
1609 shift->_op_usage('+', 'downbytes', @_);
1612 =item decrement_totalbytes OCTETS
1614 Decrements the I<totalbytes> field of this record by the given amount. If there
1615 is an error, returns the error, otherwise returns false.
1619 sub decrement_totalbytes {
1620 shift->_op_usage('-', 'totalbytes', @_);
1623 =item increment_totalbytes OCTETS
1625 Increments the I<totalbytes> field of this record by the given amount. If there
1626 is an error, returns the error, otherwise returns false.
1630 sub increment_totalbytes {
1631 shift->_op_usage('+', 'totalbytes', @_);
1634 =item decrement_seconds SECONDS
1636 Decrements the I<seconds> field of this record by the given amount. If there
1637 is an error, returns the error, otherwise returns false.
1641 sub decrement_seconds {
1642 shift->_op_usage('-', 'seconds', @_);
1645 =item increment_seconds SECONDS
1647 Increments the I<seconds> field of this record by the given amount. If there
1648 is an error, returns the error, otherwise returns false.
1652 sub increment_seconds {
1653 shift->_op_usage('+', 'seconds', @_);
1661 my %op2condition = (
1662 '-' => sub { my($self, $column, $amount) = @_;
1663 $self->$column - $amount <= 0;
1665 '+' => sub { my($self, $column, $amount) = @_;
1666 ($self->$column || 0) + $amount > 0;
1669 my %op2warncondition = (
1670 '-' => sub { my($self, $column, $amount) = @_;
1671 my $threshold = $column . '_threshold';
1672 $self->$column - $amount <= $self->$threshold + 0;
1674 '+' => sub { my($self, $column, $amount) = @_;
1675 ($self->$column || 0) + $amount > 0;
1680 my( $self, $op, $column, $amount ) = @_;
1682 warn "$me _op_usage called for $column on svcnum ". $self->svcnum.
1683 ' ('. $self->email. "): $op $amount\n"
1686 return '' unless $amount;
1688 local $SIG{HUP} = 'IGNORE';
1689 local $SIG{INT} = 'IGNORE';
1690 local $SIG{QUIT} = 'IGNORE';
1691 local $SIG{TERM} = 'IGNORE';
1692 local $SIG{TSTP} = 'IGNORE';
1693 local $SIG{PIPE} = 'IGNORE';
1695 my $oldAutoCommit = $FS::UID::AutoCommit;
1696 local $FS::UID::AutoCommit = 0;
1699 my $sql = "UPDATE svc_acct SET $column = ".
1700 " CASE WHEN $column IS NULL THEN 0 ELSE $column END ". #$column||0
1701 " $op ? WHERE svcnum = ?";
1705 my $sth = $dbh->prepare( $sql )
1706 or die "Error preparing $sql: ". $dbh->errstr;
1707 my $rv = $sth->execute($amount, $self->svcnum);
1708 die "Error executing $sql: ". $sth->errstr
1709 unless defined($rv);
1710 die "Can't update $column for svcnum". $self->svcnum
1713 #$self->snapshot; #not necessary, we retain the old values
1714 #create an object with the updated usage values
1715 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1717 my $error = $new->replace($self);
1719 $dbh->rollback if $oldAutoCommit;
1720 return "Error replacing: $error";
1723 #overlimit_action eq 'cancel' handling
1724 my $cust_pkg = $self->cust_svc->cust_pkg;
1726 && $cust_pkg->part_pkg->option('overlimit_action', 1) eq 'cancel'
1727 && $op eq '-' && &{$op2condition{$op}}($self, $column, $amount)
1731 my $error = $cust_pkg->cancel; #XXX should have a reason
1733 $dbh->rollback if $oldAutoCommit;
1734 return "Error cancelling: $error";
1737 #nothing else is relevant if we're cancelling, so commit & return success
1738 warn "$me update successful; committing\n"
1740 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1745 my $action = $op2action{$op};
1747 if ( &{$op2condition{$op}}($self, $column, $amount) &&
1748 ( $action eq 'suspend' && !$self->overlimit
1749 || $action eq 'unsuspend' && $self->overlimit )
1751 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1752 if ($part_export->option('overlimit_groups')) {
1754 my $other = new FS::svc_acct $self->hashref;
1755 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1756 ($self, $part_export->option('overlimit_groups'));
1757 $other->usergroup( $groups );
1758 if ($action eq 'suspend'){
1759 $new = $other; $old = $self;
1761 $new = $self; $old = $other;
1763 my $error = $part_export->export_replace($new, $old);
1764 $error ||= $self->overlimit($action);
1766 $dbh->rollback if $oldAutoCommit;
1767 return "Error replacing radius groups in export, ${op}: $error";
1773 if ( $conf->exists("svc_acct-usage_$action")
1774 && &{$op2condition{$op}}($self, $column, $amount) ) {
1775 #my $error = $self->$action();
1776 my $error = $self->cust_svc->cust_pkg->$action();
1777 # $error ||= $self->overlimit($action);
1779 $dbh->rollback if $oldAutoCommit;
1780 return "Error ${action}ing: $error";
1784 if ($warning_template && &{$op2warncondition{$op}}($self, $column, $amount)) {
1785 my $wqueue = new FS::queue {
1786 'svcnum' => $self->svcnum,
1787 'job' => 'FS::svc_acct::reached_threshold',
1792 $to = $warning_cc if &{$op2condition{$op}}($self, $column, $amount);
1796 my $error = $wqueue->insert(
1797 'svcnum' => $self->svcnum,
1799 'column' => $column,
1803 $dbh->rollback if $oldAutoCommit;
1804 return "Error queuing threshold activity: $error";
1808 warn "$me update successful; committing\n"
1810 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1816 my( $self, $valueref, %options ) = @_;
1818 warn "$me set_usage called for svcnum ". $self->svcnum.
1819 ' ('. $self->email. "): ".
1820 join(', ', map { "$_ => " . $valueref->{$_}} keys %$valueref) . "\n"
1823 local $SIG{HUP} = 'IGNORE';
1824 local $SIG{INT} = 'IGNORE';
1825 local $SIG{QUIT} = 'IGNORE';
1826 local $SIG{TERM} = 'IGNORE';
1827 local $SIG{TSTP} = 'IGNORE';
1828 local $SIG{PIPE} = 'IGNORE';
1830 local $FS::svc_Common::noexport_hack = 1;
1831 my $oldAutoCommit = $FS::UID::AutoCommit;
1832 local $FS::UID::AutoCommit = 0;
1837 if ( $options{null} ) {
1838 %handyhash = ( map { ( $_ => 'NULL', $_."_threshold" => 'NULL' ) }
1839 qw( seconds upbytes downbytes totalbytes )
1842 foreach my $field (keys %$valueref){
1843 $reset = 1 if $valueref->{$field};
1844 $self->setfield($field, $valueref->{$field});
1845 $self->setfield( $field.'_threshold',
1846 int($self->getfield($field)
1847 * ( $conf->exists('svc_acct-usage_threshold')
1848 ? 1 - $conf->config('svc_acct-usage_threshold')/100
1853 $handyhash{$field} = $self->getfield($field);
1854 $handyhash{$field.'_threshold'} = $self->getfield($field.'_threshold');
1856 #my $error = $self->replace; #NO! we avoid the call to ->check for
1857 #die $error if $error; #services not explicity changed via the UI
1859 my $sql = "UPDATE svc_acct SET " .
1860 join (',', map { "$_ = $handyhash{$_}" } (keys %handyhash) ).
1861 " WHERE svcnum = ". $self->svcnum;
1866 if (scalar(keys %handyhash)) {
1867 my $sth = $dbh->prepare( $sql )
1868 or die "Error preparing $sql: ". $dbh->errstr;
1869 my $rv = $sth->execute();
1870 die "Error executing $sql: ". $sth->errstr
1871 unless defined($rv);
1872 die "Can't update usage for svcnum ". $self->svcnum
1876 #$self->snapshot; #not necessary, we retain the old values
1877 #create an object with the updated usage values
1878 my $new = qsearchs('svc_acct', { 'svcnum' => $self->svcnum });
1880 my $error = $new->replace($self);
1882 $dbh->rollback if $oldAutoCommit;
1883 return "Error replacing: $error";
1889 if ($self->overlimit) {
1890 $error = $self->overlimit('unsuspend');
1891 foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
1892 if ($part_export->option('overlimit_groups')) {
1893 my $old = new FS::svc_acct $self->hashref;
1894 my $groups = &{ $self->_fieldhandlers->{'usergroup'} }
1895 ($self, $part_export->option('overlimit_groups'));
1896 $old->usergroup( $groups );
1897 $error ||= $part_export->export_replace($self, $old);
1902 if ( $conf->exists("svc_acct-usage_unsuspend")) {
1903 $error ||= $self->cust_svc->cust_pkg->unsuspend;
1906 $dbh->rollback if $oldAutoCommit;
1907 return "Error unsuspending: $error";
1911 warn "$me update successful; committing\n"
1913 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1919 =item recharge HASHREF
1921 Increments usage columns by the amount specified in HASHREF as
1922 column=>amount pairs.
1927 my ($self, $vhash) = @_;
1930 warn "[$me] recharge called on $self: ". Dumper($self).
1931 "\nwith vhash: ". Dumper($vhash);
1934 my $oldAutoCommit = $FS::UID::AutoCommit;
1935 local $FS::UID::AutoCommit = 0;
1939 foreach my $column (keys %$vhash){
1940 $error ||= $self->_op_usage('+', $column, $vhash->{$column});
1944 $dbh->rollback if $oldAutoCommit;
1946 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1951 =item is_rechargeable
1953 Returns true if this svc_account can be "recharged" and false otherwise.
1957 sub is_rechargable {
1959 $self->seconds ne ''
1960 || $self->upbytes ne ''
1961 || $self->downbytes ne ''
1962 || $self->totalbytes ne '';
1965 =item seconds_since TIMESTAMP
1967 Returns the number of seconds this account has been online since TIMESTAMP,
1968 according to the session monitor (see L<FS::Session>).
1970 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1971 L<Time::Local> and L<Date::Parse> for conversion functions.
1975 #note: POD here, implementation in FS::cust_svc
1978 $self->cust_svc->seconds_since(@_);
1981 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1983 Returns the numbers of seconds this account has been online between
1984 TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
1985 external SQL radacct table, specified via sqlradius export. Sessions which
1986 started in the specified range but are still open are counted from session
1987 start to the end of the range (unless they are over 1 day old, in which case
1988 they are presumed missing their stop record and not counted). Also, sessions
1989 which end in the range but started earlier are counted from the start of the
1990 range to session end. Finally, sessions which start before the range but end
1991 after are counted for the entire range.
1993 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1994 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1999 #note: POD here, implementation in FS::cust_svc
2000 sub seconds_since_sqlradacct {
2002 $self->cust_svc->seconds_since_sqlradacct(@_);
2005 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
2007 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
2008 in this package for sessions ending between TIMESTAMP_START (inclusive) and
2009 TIMESTAMP_END (exclusive).
2011 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
2012 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
2017 #note: POD here, implementation in FS::cust_svc
2018 sub attribute_since_sqlradacct {
2020 $self->cust_svc->attribute_since_sqlradacct(@_);
2023 =item get_session_history TIMESTAMP_START TIMESTAMP_END
2025 Returns an array of hash references of this customers login history for the
2026 given time range. (document this better)
2030 sub get_session_history {
2032 $self->cust_svc->get_session_history(@_);
2035 =item last_login_text
2037 Returns text describing the time of last login.
2041 sub last_login_text {
2043 $self->last_login ? ctime($self->last_login) : 'unknown';
2046 =item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
2051 my($self, $start, $end, %opt ) = @_;
2053 my $did = $self->username; #yup
2055 my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
2057 my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
2059 #SELECT $for_update * FROM cdr
2060 # WHERE calldate >= $start #need a conversion
2061 # AND calldate < $end #ditto
2062 # AND ( charged_party = "$did"
2063 # OR charged_party = "$prefix$did" #if length($prefix);
2064 # OR ( ( charged_party IS NULL OR charged_party = '' )
2066 # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
2069 # AND ( freesidestatus IS NULL OR freesidestatus = '' )
2072 if ( length($prefix) ) {
2074 " AND ( charged_party = '$did'
2075 OR charged_party = '$prefix$did'
2076 OR ( ( charged_party IS NULL OR charged_party = '' )
2078 ( src = '$did' OR src = '$prefix$did' )
2084 " AND ( charged_party = '$did'
2085 OR ( ( charged_party IS NULL OR charged_party = '' )
2095 'select' => "$for_update *",
2098 #( freesidestatus IS NULL OR freesidestatus = '' )
2099 'freesidestatus' => '',
2101 'extra_sql' => $charged_or_src,
2109 Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
2115 if ( $self->usergroup ) {
2116 confess "explicitly specified usergroup not an arrayref: ". $self->usergroup
2117 unless ref($self->usergroup) eq 'ARRAY';
2118 #when provisioning records, export callback runs in svc_Common.pm before
2119 #radius_usergroup records can be inserted...
2120 @{$self->usergroup};
2122 map { $_->groupname }
2123 qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
2127 =item clone_suspended
2129 Constructor used by FS::part_export::_export_suspend fallback. Document
2134 sub clone_suspended {
2136 my %hash = $self->hash;
2137 $hash{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
2138 new FS::svc_acct \%hash;
2141 =item clone_kludge_unsuspend
2143 Constructor used by FS::part_export::_export_unsuspend fallback. Document
2148 sub clone_kludge_unsuspend {
2150 my %hash = $self->hash;
2151 $hash{_password} = '';
2152 new FS::svc_acct \%hash;
2155 =item check_password
2157 Checks the supplied password against the (possibly encrypted) password in the
2158 database. Returns true for a successful authentication, false for no match.
2160 Currently supported encryptions are: classic DES crypt() and MD5
2164 sub check_password {
2165 my($self, $check_password) = @_;
2167 #remove old-style SUSPENDED kludge, they should be allowed to login to
2168 #self-service and pay up
2169 ( my $password = $self->_password ) =~ s/^\*SUSPENDED\* //;
2171 #eventually should check a "password-encoding" field
2172 if ( $password =~ /^(\*|!!?)$/ ) { #no self-service login
2174 } elsif ( length($password) < 13 ) { #plaintext
2175 $check_password eq $password;
2176 } elsif ( length($password) == 13 ) { #traditional DES crypt
2177 crypt($check_password, $password) eq $password;
2178 } elsif ( $password =~ /^\$1\$/ ) { #MD5 crypt
2179 unix_md5_crypt($check_password, $password) eq $password;
2180 } elsif ( $password =~ /^\$2a?\$/ ) { #Blowfish
2181 warn "Can't check password: Blowfish encryption not yet supported, svcnum".
2182 $self->svcnum. "\n";
2185 warn "Can't check password: Unrecognized encryption for svcnum ".
2186 $self->svcnum. "\n";
2192 =item crypt_password [ DEFAULT_ENCRYPTION_TYPE ]
2194 Returns an encrypted password, either by passing through an encrypted password
2195 in the database or by encrypting a plaintext password from the database.
2197 The optional DEFAULT_ENCRYPTION_TYPE parameter can be set to I<crypt> (classic
2198 UNIX DES crypt), I<md5> (md5 crypt supported by most modern Linux and BSD
2199 distrubtions), or (eventually) I<blowfish> (blowfish hashing supported by
2200 OpenBSD, SuSE, other Linux distibutions with pam_unix2, etc.). The default
2201 encryption type is only used if the password is not already encrypted in the
2206 sub crypt_password {
2208 #eventually should check a "password-encoding" field
2209 if ( length($self->_password) == 13
2210 || $self->_password =~ /^\$(1|2a?)\$/
2211 || $self->_password =~ /^(\*|NP|\*LK\*|!!?)$/
2216 my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2217 if ( $encryption eq 'crypt' ) {
2220 $saltset[int(rand(64))].$saltset[int(rand(64))]
2222 } elsif ( $encryption eq 'md5' ) {
2223 unix_md5_crypt( $self->_password );
2224 } elsif ( $encryption eq 'blowfish' ) {
2225 croak "unknown encryption method $encryption";
2227 croak "unknown encryption method $encryption";
2232 =item ldap_password [ DEFAULT_ENCRYPTION_TYPE ]
2234 Returns an encrypted password in "LDAP" format, with a curly-bracked prefix
2235 describing the format, for example, "{CRYPT}94pAVyK/4oIBk" or
2236 "{PLAIN-MD5}5426824942db4253f87a1009fd5d2d4f".
2238 The optional DEFAULT_ENCRYPTION_TYPE is not yet used, but the idea is for it
2239 to work the same as the B</crypt_password> method.
2245 #eventually should check a "password-encoding" field
2246 if ( length($self->_password) == 13 ) { #crypt
2247 return '{CRYPT}'. $self->_password;
2248 } elsif ( $self->_password =~ /^\$1\$(.*)$/ && length($1) == 31 ) { #passwdMD5
2250 } elsif ( $self->_password =~ /^\$2a?\$(.*)$/ ) { #Blowfish
2251 warn "Blowfish encryption not supported in this context, svcnum ".
2252 $self->svcnum. "\n";
2253 return '{CRYPT}*'; #unsupported, should not auth
2254 } elsif ( $self->_password =~ /^(\w{48})$/ ) { #LDAP SSHA
2255 return '{SSHA}'. $1;
2256 } elsif ( $self->_password =~ /^(\w{64})$/ ) { #LDAP NS-MTA-MD5
2257 return '{NS-MTA-MD5}'. $1;
2259 return '{PLAIN}'. $self->_password;
2260 #my $encryption = ( scalar(@_) && $_[0] ) ? shift : 'crypt';
2261 #if ( $encryption eq 'crypt' ) {
2262 # return '{CRYPT}'. crypt(
2264 # $saltset[int(rand(64))].$saltset[int(rand(64))]
2266 #} elsif ( $encryption eq 'md5' ) {
2267 # unix_md5_crypt( $self->_password );
2268 #} elsif ( $encryption eq 'blowfish' ) {
2269 # croak "unknown encryption method $encryption";
2271 # croak "unknown encryption method $encryption";
2276 =item domain_slash_username
2278 Returns $domain/$username/
2282 sub domain_slash_username {
2284 $self->domain. '/'. $self->username. '/';
2287 =item virtual_maildir
2289 Returns $domain/maildirs/$username/
2293 sub virtual_maildir {
2295 $self->domain. '/maildirs/'. $self->username. '/';
2306 This is the FS::svc_acct job-queue-able version. It still uses
2307 FS::Misc::send_email under-the-hood.
2314 eval "use FS::Misc qw(send_email)";
2317 $opt{mimetype} ||= 'text/plain';
2318 $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2320 my $error = send_email(
2321 'from' => $opt{from},
2323 'subject' => $opt{subject},
2324 'content-type' => $opt{mimetype},
2325 'body' => [ map "$_\n", split("\n", $opt{body}) ],
2327 die $error if $error;
2330 =item check_and_rebuild_fuzzyfiles
2334 sub check_and_rebuild_fuzzyfiles {
2335 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2336 -e "$dir/svc_acct.username"
2337 or &rebuild_fuzzyfiles;
2340 =item rebuild_fuzzyfiles
2344 sub rebuild_fuzzyfiles {
2346 use Fcntl qw(:flock);
2348 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2352 open(USERNAMELOCK,">>$dir/svc_acct.username")
2353 or die "can't open $dir/svc_acct.username: $!";
2354 flock(USERNAMELOCK,LOCK_EX)
2355 or die "can't lock $dir/svc_acct.username: $!";
2357 my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
2359 open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
2360 or die "can't open $dir/svc_acct.username.tmp: $!";
2361 print USERNAMECACHE join("\n", @all_username), "\n";
2362 close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
2364 rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
2374 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2375 open(USERNAMECACHE,"<$dir/svc_acct.username")
2376 or die "can't open $dir/svc_acct.username: $!";
2377 my @array = map { chomp; $_; } <USERNAMECACHE>;
2378 close USERNAMECACHE;
2382 =item append_fuzzyfiles USERNAME
2386 sub append_fuzzyfiles {
2387 my $username = shift;
2389 &check_and_rebuild_fuzzyfiles;
2391 use Fcntl qw(:flock);
2393 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2395 open(USERNAME,">>$dir/svc_acct.username")
2396 or die "can't open $dir/svc_acct.username: $!";
2397 flock(USERNAME,LOCK_EX)
2398 or die "can't lock $dir/svc_acct.username: $!";
2400 print USERNAME "$username\n";
2402 flock(USERNAME,LOCK_UN)
2403 or die "can't unlock $dir/svc_acct.username: $!";
2411 =item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
2415 sub radius_usergroup_selector {
2416 my $sel_groups = shift;
2417 my %sel_groups = map { $_=>1 } @$sel_groups;
2419 my $selectname = shift || 'radius_usergroup';
2422 my $sth = $dbh->prepare(
2423 'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
2424 ) or die $dbh->errstr;
2425 $sth->execute() or die $sth->errstr;
2426 my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
2430 function ${selectname}_doadd(object) {
2431 var myvalue = object.${selectname}_add.value;
2432 var optionName = new Option(myvalue,myvalue,false,true);
2433 var length = object.$selectname.length;
2434 object.$selectname.options[length] = optionName;
2435 object.${selectname}_add.value = "";
2438 <SELECT MULTIPLE NAME="$selectname">
2441 foreach my $group ( @all_groups ) {
2442 $html .= qq(<OPTION VALUE="$group");
2443 if ( $sel_groups{$group} ) {
2444 $html .= ' SELECTED';
2445 $sel_groups{$group} = 0;
2447 $html .= ">$group</OPTION>\n";
2449 foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
2450 $html .= qq(<OPTION VALUE="$group" SELECTED>$group</OPTION>\n);
2452 $html .= '</SELECT>';
2454 $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
2455 qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
2460 =item reached_threshold
2462 Performs some activities when svc_acct thresholds (such as number of seconds
2463 remaining) are reached.
2467 sub reached_threshold {
2470 my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $opt{'svcnum'} } );
2471 die "Cannot find svc_acct with svcnum " . $opt{'svcnum'} unless $svc_acct;
2473 if ( $opt{'op'} eq '+' ){
2474 $svc_acct->setfield( $opt{'column'}.'_threshold',
2475 int($svc_acct->getfield($opt{'column'})
2476 * ( $conf->exists('svc_acct-usage_threshold')
2477 ? $conf->config('svc_acct-usage_threshold')/100
2482 my $error = $svc_acct->replace;
2483 die $error if $error;
2484 }elsif ( $opt{'op'} eq '-' ){
2486 my $threshold = $svc_acct->getfield( $opt{'column'}.'_threshold' );
2487 return '' if ($threshold eq '' );
2489 $svc_acct->setfield( $opt{'column'}.'_threshold', 0 );
2490 my $error = $svc_acct->replace;
2491 die $error if $error; # email next time, i guess
2493 if ( $warning_template ) {
2494 eval "use FS::Misc qw(send_email)";
2497 my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
2498 my $cust_main = $cust_pkg->cust_main;
2500 my $to = join(', ', grep { $_ !~ /^(POST|FAX)$/ }
2501 $cust_main->invoicing_list,
2502 ($opt{'to'} ? $opt{'to'} : ())
2505 my $mimetype = $warning_mimetype;
2506 $mimetype .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
2508 my $body = $warning_template->fill_in( HASH => {
2509 'custnum' => $cust_main->custnum,
2510 'username' => $svc_acct->username,
2511 'password' => $svc_acct->_password,
2512 'first' => $cust_main->first,
2513 'last' => $cust_main->getfield('last'),
2514 'pkg' => $cust_pkg->part_pkg->pkg,
2515 'column' => $opt{'column'},
2516 'amount' => $opt{'column'} =~/bytes/
2517 ? FS::UI::bytecount::display_bytecount($svc_acct->getfield($opt{'column'}))
2518 : $svc_acct->getfield($opt{'column'}),
2519 'threshold' => $opt{'column'} =~/bytes/
2520 ? FS::UI::bytecount::display_bytecount($threshold)
2525 my $error = send_email(
2526 'from' => $warning_from,
2528 'subject' => $warning_subject,
2529 'content-type' => $mimetype,
2530 'body' => [ map "$_\n", split("\n", $body) ],
2532 die $error if $error;
2535 die "unknown op: " . $opt{'op'};
2543 The $recref stuff in sub check should be cleaned up.
2545 The suspend, unsuspend and cancel methods update the database, but not the
2546 current object. This is probably a bug as it's unexpected and
2549 radius_usergroup_selector? putting web ui components in here? they should
2550 probably live somewhere else...
2552 insertion of RADIUS group stuff in insert could be done with child_objects now
2553 (would probably clean up export of them too)
2555 _op_usage and set_usage bypass the history... maybe they shouldn't
2559 L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
2560 export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
2561 L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
2562 L<freeside-queued>), L<FS::svc_acct_pop>,
2563 schema.html from the base documentation.
2567 =item domain_select_hash %OPTIONS
2569 Returns a hash SVCNUM => DOMAIN ... representing the domains this customer
2570 may at present purchase.
2572 Currently available options are: I<pkgnum> I<svcpart>
2576 sub domain_select_hash {
2577 my ($self, %options) = @_;
2583 $part_svc = $self->part_svc;
2584 $cust_pkg = $self->cust_svc->cust_pkg
2588 $part_svc = qsearchs('part_svc', { 'svcpart' => $options{svcpart} })
2589 if $options{'svcpart'};
2591 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $options{pkgnum} })
2592 if $options{'pkgnum'};
2594 if ($part_svc && ( $part_svc->part_svc_column('domsvc')->columnflag eq 'S'
2595 || $part_svc->part_svc_column('domsvc')->columnflag eq 'F')) {
2596 %domains = map { $_->svcnum => $_->domain }
2597 map { qsearchs('svc_domain', { 'svcnum' => $_ }) }
2598 split(',', $part_svc->part_svc_column('domsvc')->columnvalue);
2599 }elsif ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
2600 %domains = map { $_->svcnum => $_->domain }
2601 map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum }) }
2602 map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
2603 qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum });
2605 %domains = map { $_->svcnum => $_->domain } qsearch('svc_domain', {} );
2608 if ($part_svc && $part_svc->part_svc_column('domsvc')->columnflag eq 'D') {
2609 my $svc_domain = qsearchs('svc_domain',
2610 { 'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue } );
2611 if ( $svc_domain ) {
2612 $domains{$svc_domain->svcnum} = $svc_domain->domain;
2614 warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
2615 $part_svc->part_svc_column('domsvc')->columnvalue;