2 use base qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
5 use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
7 #use Scalar::Util qw( blessed );
8 use List::Util qw( max );
10 use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
14 use FS::part_svc_link;
15 use FS::domain_record;
20 #most FS::svc_ classes are autoloaded in svc_x emthod
21 use FS::svc_acct; #this one is used in the cache stuff
29 #ask FS::UID to run this stuff for us later
30 FS::UID->install_callback( sub {
32 $ticket_system = $conf->config('ticket_system')
35 our $cache_enabled = 0;
38 my( $self, $hashref ) = @_;
39 if ( $cache_enabled && $hashref->{'svc'} ) {
40 $self->{'_svcpart'} = FS::part_svc->new($hashref);
46 my ( $hashref, $cache ) = @_;
47 if ( $hashref->{'username'} ) {
48 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
50 if ( $hashref->{'svc'} ) {
51 $self->{'_svcpart'} = FS::part_svc->new($hashref);
57 FS::cust_svc - Object method for cust_svc objects
63 $record = new FS::cust_svc \%hash
64 $record = new FS::cust_svc { 'column' => 'value' };
66 $error = $record->insert;
68 $error = $new_record->replace($old_record);
70 $error = $record->delete;
72 $error = $record->check;
74 ($label, $value) = $record->label;
78 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
79 The following fields are currently supported:
83 =item svcnum - primary key (assigned automatically for new services)
85 =item pkgnum - Package (see L<FS::cust_pkg>)
87 =item svcpart - Service definition (see L<FS::part_svc>)
89 =item agent_svcid - Optional legacy service ID
91 =item overlimit - date the service exceeded its usage limit
101 Creates a new service. To add the refund to the database, see L<"insert">.
102 Services are normally created by creating FS::svc_ objects (see
103 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
107 sub table { 'cust_svc'; }
111 Adds this service to the database. If there is an error, returns the error,
112 otherwise returns false.
119 local $SIG{HUP} = 'IGNORE';
120 local $SIG{INT} = 'IGNORE';
121 local $SIG{QUIT} = 'IGNORE';
122 local $SIG{TERM} = 'IGNORE';
123 local $SIG{TSTP} = 'IGNORE';
124 local $SIG{PIPE} = 'IGNORE';
126 my $oldAutoCommit = $FS::UID::AutoCommit;
127 local $FS::UID::AutoCommit = 0;
130 my $error = $self->SUPER::insert;
132 #check if this releases a hold (see FS::pkg_svc provision_hold)
133 $error ||= $self->_check_provision_hold;
136 $dbh->rollback if $oldAutoCommit;
137 return $error if $error
140 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
147 Deletes this service from the database. If there is an error, returns the
148 error, otherwise returns false. Note that this only removes the cust_svc
149 record - you should probably use the B<cancel> method instead.
158 my $cust_pkg = $self->cust_pkg;
159 my $custnum = $cust_pkg->custnum if $cust_pkg;
161 local $SIG{HUP} = 'IGNORE';
162 local $SIG{INT} = 'IGNORE';
163 local $SIG{QUIT} = 'IGNORE';
164 local $SIG{TERM} = 'IGNORE';
165 local $SIG{TSTP} = 'IGNORE';
166 local $SIG{PIPE} = 'IGNORE';
168 my $oldAutoCommit = $FS::UID::AutoCommit;
169 local $FS::UID::AutoCommit = 0;
172 my $error = $self->SUPER::delete;
174 $dbh->rollback if $oldAutoCommit;
178 foreach my $part_svc_link ( $self->part_svc_link(
179 link_type => 'cust_svc_unprovision_cascade',
182 foreach my $cust_svc ( qsearch( 'cust_svc', {
183 'pkgnum' => $self->pkgnum,
184 'svcpart' => $part_svc_link->dst_svcpart,
187 my $error = $cust_svc->svc_x->delete;
189 $dbh->rollback if $oldAutoCommit;
196 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
198 if ( $ticket_system eq 'RT_Internal' ) {
199 unless ( $rt_session ) {
200 FS::TicketSystem->init;
201 $rt_session = FS::TicketSystem->session;
203 my $links = RT::Links->new($rt_session->{CurrentUser});
204 my $svcnum = $self->svcnum;
205 $links->Limit(FIELD => 'Target',
206 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
207 while ( my $l = $links->Next ) {
210 # re-link to point to the customer instead
212 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
215 ($val, $msg) = $l->Delete;
217 # can't do anything useful on error
218 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
228 Suspends the relevant service by calling the B<suspend> method of the associated
229 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object).
231 If there is an error, returns the error, otherwise returns false.
236 my( $self, %opt ) = @_;
238 $self->part_svc->svcdb =~ /^([\w\-]+)$/ or return 'Illegal part_svc.svcdb';
240 require "FS/$svcdb.pm";
242 my $svc = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
245 my $error = $svc->suspend;
246 return $error if $error;
248 if ( $opt{labels_arryref} ) {
249 my( $label, $value ) = $self->label;
250 push @{ $opt{labels_arrayref} }, "$label: $value";
259 Cancels the relevant service by calling the B<cancel> method of the associated
260 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
261 deleting the FS::svc_XXX record and then deleting this record.
263 If there is an error, returns the error, otherwise returns false.
270 local $SIG{HUP} = 'IGNORE';
271 local $SIG{INT} = 'IGNORE';
272 local $SIG{QUIT} = 'IGNORE';
273 local $SIG{TERM} = 'IGNORE';
274 local $SIG{TSTP} = 'IGNORE';
275 local $SIG{PIPE} = 'IGNORE';
277 my $oldAutoCommit = $FS::UID::AutoCommit;
278 local $FS::UID::AutoCommit = 0;
281 my $part_svc = $self->part_svc;
283 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
284 $dbh->rollback if $oldAutoCommit;
285 return "Illegal svcdb value in part_svc!";
288 require "FS/$svcdb.pm";
290 my $svc = $self->svc_x;
292 if ( %opt && $opt{'date'} ) {
293 my $error = $svc->expire($opt{'date'});
295 $dbh->rollback if $oldAutoCommit;
296 return "Error expiring service: $error";
299 my $error = $svc->cancel;
301 $dbh->rollback if $oldAutoCommit;
302 return "Error canceling service: $error";
304 $error = $svc->delete; #this deletes this cust_svc record as well
306 $dbh->rollback if $oldAutoCommit;
307 return "Error deleting service: $error";
314 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
315 "; deleting cust_svc only\n";
317 my $error = $self->delete;
319 $dbh->rollback if $oldAutoCommit;
320 return "Error deleting cust_svc: $error";
325 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
331 =item overlimit [ ACTION ]
333 Retrieves or sets the overlimit date. If ACTION is absent, return
334 the present value of overlimit. If ACTION is present, it can
335 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
336 is set to the current time if it is not already set. The 'unsuspend' value
337 causes the time to be cleared.
339 If there is an error on setting, returns the error, otherwise returns false.
345 my $action = shift or return $self->getfield('overlimit');
347 local $SIG{HUP} = 'IGNORE';
348 local $SIG{INT} = 'IGNORE';
349 local $SIG{QUIT} = 'IGNORE';
350 local $SIG{TERM} = 'IGNORE';
351 local $SIG{TSTP} = 'IGNORE';
352 local $SIG{PIPE} = 'IGNORE';
354 my $oldAutoCommit = $FS::UID::AutoCommit;
355 local $FS::UID::AutoCommit = 0;
358 if ( $action eq 'suspend' ) {
359 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
360 }elsif ( $action eq 'unsuspend' ) {
361 $self->setfield('overlimit', '');
363 die "unexpected action value: $action";
366 local $ignore_quantity = 1;
367 my $error = $self->replace;
369 $dbh->rollback if $oldAutoCommit;
370 return "Error setting overlimit: $error";
373 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
379 =item replace OLD_RECORD
381 Replaces the OLD_RECORD with this one in the database. If there is an error,
382 returns the error, otherwise returns false.
389 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
391 # : $new->replace_old;
392 my ( $new, $old ) = ( shift, shift );
393 $old = $new->replace_old unless defined($old);
395 local $SIG{HUP} = 'IGNORE';
396 local $SIG{INT} = 'IGNORE';
397 local $SIG{QUIT} = 'IGNORE';
398 local $SIG{TERM} = 'IGNORE';
399 local $SIG{TSTP} = 'IGNORE';
400 local $SIG{PIPE} = 'IGNORE';
402 my $oldAutoCommit = $FS::UID::AutoCommit;
403 local $FS::UID::AutoCommit = 0;
406 if ( $new->svcpart != $old->svcpart ) {
407 my $svc_x = $new->svc_x;
408 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
409 local($FS::Record::nowarn_identical) = 1;
410 my $error = $new_svc_x->replace($svc_x);
412 $dbh->rollback if $oldAutoCommit;
413 return $error if $error;
417 # #trigger a re-export on pkgnum changes?
418 # # (of prepaid packages), for Expiration RADIUS attribute
419 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
420 # my $svc_x = $new->svc_x;
421 # local($FS::Record::nowarn_identical) = 1;
422 # my $error = $svc_x->export('replace');
424 # $dbh->rollback if $oldAutoCommit;
425 # return $error if $error;
429 #trigger a pkg_change export on pkgnum changes
430 if ( $new->pkgnum != $old->pkgnum ) {
431 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
436 $dbh->rollback if $oldAutoCommit;
437 return $error if $error;
439 } # if pkgnum is changing
441 #my $error = $new->SUPER::replace($old, @_);
442 my $error = $new->SUPER::replace($old);
444 #trigger a relocate export on location changes
445 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
446 my $svc_x = $new->svc_x;
447 if ( $svc_x->locationnum ) {
448 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
449 # in this case, set the service location to be the same as the new
451 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
452 # and replace it, which triggers a relocate export so we don't
454 $error ||= $svc_x->replace;
456 # the service already has a different location from its package
460 # the service doesn't have a locationnum (either isn't of a type
461 # that has the locationnum field, or the locationnum is null and
462 # defaults to cust_pkg->locationnum)
463 # so just trigger the export here
464 $error ||= $new->svc_x->export('relocate',
465 $new->cust_pkg->cust_location,
466 $old->cust_pkg->cust_location,
468 } # if ($svc_x->locationnum)
469 } # if this is a location change
471 #check if this releases a hold (see FS::pkg_svc provision_hold)
472 $error ||= $new->_check_provision_hold;
475 $dbh->rollback if $oldAutoCommit;
476 return $error if $error
479 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
486 Checks all fields to make sure this is a valid service. If there is an error,
487 returns the error, otherwise returns false. Called by the insert and
496 $self->ut_numbern('svcnum')
497 || $self->ut_numbern('pkgnum')
498 || $self->ut_number('svcpart')
499 || $self->ut_numbern('agent_svcid')
500 || $self->ut_numbern('overlimit')
502 return $error if $error;
504 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
505 return "Unknown svcpart" unless $part_svc;
507 if ( $self->pkgnum && ! $ignore_quantity ) {
509 #slightly inefficient since ->pkg_svc will also look it up, but fixing
510 # a much larger perf problem and have bigger fish to fry
511 my $cust_pkg = $self->cust_pkg;
513 my $pkg_svc = $self->pkg_svc
514 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
515 'pkgpart' => $cust_pkg->pkgpart,
519 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
520 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
521 my $addon_pkg_svc = qsearchs('pkg_svc', {
522 pkgpart => $part_pkg_link->dst_pkgpart,
523 svcpart => $self->svcpart,
525 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
529 #better error message? UI shouldn't get here
530 return "No svcpart ". $self->svcpart.
531 " services in pkgpart ". $cust_pkg->pkgpart
532 unless $pkg_svc->quantity > 0;
534 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
536 #false laziness w/cust_pkg->part_svc
537 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
541 #better error message? again, UI shouldn't get here
542 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
543 " services for pkgnum ". $self->pkgnum
546 #part_svc_link rules (only make sense in pkgpart context, and
547 # skipping this when ignore_quantity is set DTRT when we're "forcing"
548 # an implicit change here (location change triggered pkgpart change,
549 # ->overlimit, bulk customer service changes)
550 foreach my $part_svc_link ( $self->part_svc_link(
551 link_type => 'cust_svc_provision_restrict',
554 return $part_svc_link->dst_svc. ' must be provisioned before '.
555 $part_svc_link->src_svc
557 'table' => 'cust_svc',
558 'hashref' => { 'pkgnum' => $self->pkgnum,
559 'svcpart' => $part_svc_link->dst_svcpart,
561 'order_by' => 'LIMIT 1',
570 =item check_part_svc_link_unprovision
572 Checks service dependency unprovision rules for this service.
574 If there is an error, returns the error, otherwise returns false.
578 sub check_part_svc_link_unprovision {
581 foreach my $part_svc_link ( $self->part_svc_link(
582 link_type => 'cust_svc_unprovision_restrict',
585 return $part_svc_link->dst_svc. ' must be unprovisioned before '.
586 $part_svc_link->src_svc
588 'table' => 'cust_svc',
589 'hashref' => { 'pkgnum' => $self->pkgnum,
590 'svcpart' => $part_svc_link->dst_svcpart,
592 'order_by' => 'LIMIT 1',
601 Returns the service dependencies (see L<FS::part_svc_link>) for the given
602 search options, taking into account this service definition as source and
603 this customer's agent.
605 Available options are any field in part_svc_link. Typically used options are
612 my $agentnum = $self->pkgnum ? $self->cust_pkg->cust_main->agentnum : '';
613 FS::part_svc_link->by_agentnum($agentnum,
614 src_svcpart=>$self->svcpart,
622 Returns the displayed service number for this service: agent_svcid if it has a
623 value, svcnum otherwise
629 $self->agent_svcid || $self->svcnum;
634 Returns the definition for this service, as a FS::part_svc object (see
641 return $self->{_svcpart} if $self->{_svcpart};
642 cluck 'cust_svc->part_svc called' if $DEBUG;
643 qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
648 Returns the package this service belongs to, as a FS::cust_pkg object (see
653 Returns the pkg_svc record for for this service, if applicable.
659 my $cust_pkg = $self->cust_pkg;
660 return undef unless $cust_pkg;
662 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
663 'pkgpart' => $cust_pkg->pkgpart,
670 Returns the date this service was inserted.
676 $self->h_date('insert');
679 =item pkg_cancel_date
681 Returns the date this service's package was canceled. This normally only
682 exists for a service that's been preserved through cancellation with the
683 part_pkg.preserve flag.
687 sub pkg_cancel_date {
689 my $cust_pkg = $self->cust_pkg or return;
690 return $cust_pkg->getfield('cancel') || '';
695 Returns a list consisting of:
696 - The name of this service (from part_svc)
697 - A meaningful identifier (username, domain, or mail alias)
698 - The table name (i.e. svc_domain) for this service
703 my($label, $value, $svcdb) = $cust_svc->label;
707 Like the B<label> method, except the second item in the list ("meaningful
708 identifier") may be longer - typically, a full name is included.
712 sub label { shift->_label('svc_label', @_); }
713 sub label_long { shift->_label('svc_label_long', @_); }
718 my $svc_x = $self->svc_x
719 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
721 $self->$method($svc_x);
724 sub svc_label { shift->_svc_label('label', @_); }
725 sub svc_label_long { shift->_svc_label('label_long', @_); }
728 my( $self, $method, $svc_x ) = ( shift, shift, shift );
731 $self->part_svc->svc,
733 $self->part_svc->svcdb,
741 Returns a listref of html elements associated with this service's exports.
747 my $svc_x = $self->svc_x
748 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
750 $svc_x->export_links;
753 =item export_getsettings
755 Returns two hashrefs of settings associated with this service's exports.
759 sub export_getsettings {
761 my $svc_x = $self->svc_x
762 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
764 $svc_x->export_getsettings;
770 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
771 FS::svc_domain object, etc.)
777 my $svcdb = $self->part_svc->svcdb;
778 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
779 $self->{'_svc_acct'};
781 require "FS/$svcdb.pm";
782 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
783 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
785 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
789 =item seconds_since TIMESTAMP
791 See L<FS::svc_acct/seconds_since>. Equivalent to
792 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
793 where B<svcdb> is not "svc_acct".
797 #internal session db deprecated (or at least on hold)
798 sub seconds_since { 'internal session db deprecated'; };
799 ##note: implementation here, POD in FS::svc_acct
801 # my($self, $since) = @_;
803 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
806 # AND logout IS NOT NULL'
807 # ) or die $dbh->errstr;
808 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
809 # $sth->fetchrow_arrayref->[0];
812 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
814 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
815 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
816 for records where B<svcdb> is not "svc_acct".
820 #note: implementation here, POD in FS::svc_acct
821 sub seconds_since_sqlradacct {
822 my($self, $start, $end) = @_;
824 my $mes = "$me seconds_since_sqlradacct:";
826 my $svc_x = $self->svc_x;
828 my @part_export = $self->part_svc->part_export_usage;
829 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
830 " service definition"
835 foreach my $part_export ( @part_export ) {
837 next if $part_export->option('ignore_accounting');
839 warn "$mes connecting to sqlradius database\n"
842 my $dbh = DBI->connect( map { $part_export->option($_) }
843 qw(datasrc username password) )
844 or die "can't connect to sqlradius database: ". $DBI::errstr;
846 warn "$mes connected to sqlradius database\n"
849 #select a unix time conversion function based on database type
850 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
851 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
853 my $username = $part_export->export_username($svc_x);
857 warn "$mes finding closed sessions completely within the given range\n"
862 if ($part_export->option('process_single_realm')) {
863 $realm = 'AND Realm = ?';
864 $realmparam = $part_export->option('realm');
867 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
871 AND $str2time AcctStartTime $closing >= ?
872 AND $str2time AcctStopTime $closing < ?
873 AND $str2time AcctStopTime $closing > 0
874 AND AcctStopTime IS NOT NULL"
875 ) or die $dbh->errstr;
876 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
878 my $regular = $sth->fetchrow_arrayref->[0];
880 warn "$mes finding open sessions which start in the range\n"
883 # count session start->range end
884 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
888 AND $str2time AcctStartTime $closing >= ?
889 AND $str2time AcctStartTime $closing < ?
890 AND ( ? - $str2time AcctStartTime $closing ) < 86400
891 AND ( $str2time AcctStopTime $closing = 0
892 OR AcctStopTime IS NULL )";
893 $sth = $dbh->prepare($query) or die $dbh->errstr;
896 ($realm ? $realmparam : ()),
900 or die $sth->errstr. " executing query $query";
901 my $start_during = $sth->fetchrow_arrayref->[0];
903 warn "$mes finding closed sessions which start before the range but stop during\n"
906 #count range start->session end
907 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
911 AND $str2time AcctStartTime $closing < ?
912 AND $str2time AcctStopTime $closing >= ?
913 AND $str2time AcctStopTime $closing < ?
914 AND $str2time AcctStopTime $closing > 0
915 AND AcctStopTime IS NOT NULL"
916 ) or die $dbh->errstr;
917 $sth->execute( $start,
919 ($realm ? $realmparam : ()),
924 my $end_during = $sth->fetchrow_arrayref->[0];
926 warn "$mes finding closed sessions which start before the range but stop after\n"
929 # count range start->range end
930 # don't count open sessions anymore (probably missing stop record)
931 $sth = $dbh->prepare("SELECT COUNT(*)
935 AND $str2time AcctStartTime $closing < ?
936 AND ( $str2time AcctStopTime $closing >= ?
938 # OR AcctStopTime = 0
939 # OR AcctStopTime IS NULL )"
940 ) or die $dbh->errstr;
941 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
943 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
945 $seconds += $regular + $end_during + $start_during + $entire_range;
947 warn "$mes done finding sessions\n"
956 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
958 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
959 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
960 for records where B<svcdb> is not "svc_acct".
964 #note: implementation here, POD in FS::svc_acct
965 #(false laziness w/seconds_since_sqlradacct above)
966 sub attribute_since_sqlradacct {
967 my($self, $start, $end, $attrib) = @_;
969 my $mes = "$me attribute_since_sqlradacct:";
971 my $svc_x = $self->svc_x;
973 my @part_export = $self->part_svc->part_export_usage;
974 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
975 " service definition"
981 foreach my $part_export ( @part_export ) {
983 next if $part_export->option('ignore_accounting');
985 warn "$mes connecting to sqlradius database\n"
988 my $dbh = DBI->connect( map { $part_export->option($_) }
989 qw(datasrc username password) )
990 or die "can't connect to sqlradius database: ". $DBI::errstr;
992 warn "$mes connected to sqlradius database\n"
995 #select a unix time conversion function based on database type
996 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
997 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
999 my $username = $part_export->export_username($svc_x);
1001 warn "$mes SUMing $attrib sessions\n"
1005 my $realmparam = '';
1006 if ($part_export->option('process_single_realm')) {
1007 $realm = 'AND Realm = ?';
1008 $realmparam = $part_export->option('realm');
1011 my $sth = $dbh->prepare("SELECT SUM($attrib)
1015 AND $str2time AcctStopTime $closing >= ?
1016 AND $str2time AcctStopTime $closing < ?
1017 AND AcctStopTime IS NOT NULL"
1018 ) or die $dbh->errstr;
1019 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
1020 or die $sth->errstr;
1022 my $row = $sth->fetchrow_arrayref;
1023 $sum += $row->[0] if defined($row->[0]);
1025 warn "$mes done SUMing sessions\n"
1034 #note: implementation here, POD in FS::svc_acct
1035 # false laziness w/above
1036 sub attribute_last_sqlradacct {
1037 my($self, $attrib) = @_;
1039 my $mes = "$me attribute_last_sqlradacct:";
1041 my $svc_x = $self->svc_x;
1043 my @part_export = $self->part_svc->part_export_usage;
1044 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
1045 " service definition"
1046 unless @part_export;
1050 my $AcctStartTime = 0;
1052 foreach my $part_export ( @part_export ) {
1054 next if $part_export->option('ignore_accounting');
1056 warn "$mes connecting to sqlradius database\n"
1059 my $dbh = DBI->connect( map { $part_export->option($_) }
1060 qw(datasrc username password) )
1061 or die "can't connect to sqlradius database: ". $DBI::errstr;
1063 warn "$mes connected to sqlradius database\n"
1066 #select a unix time conversion function based on database type
1067 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
1068 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
1070 my $username = $part_export->export_username($svc_x);
1072 warn "$mes finding most-recent $attrib\n"
1076 my $realmparam = '';
1077 if ($part_export->option('process_single_realm')) {
1078 $realm = 'AND Realm = ?';
1079 $realmparam = $part_export->option('realm');
1082 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
1086 ORDER BY AcctStartTime DESC LIMIT 1
1087 ") or die $dbh->errstr;
1088 $sth->execute($username, ($realm ? $realmparam : ()) )
1089 or die $sth->errstr;
1091 my $row = $sth->fetchrow_arrayref;
1092 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
1094 $AcctStartTime = $row->[1];
1106 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1108 See L<FS::svc_acct/get_session_history>. Equivalent to
1109 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
1110 records where B<svcdb> is not "svc_acct".
1114 sub get_session_history {
1115 my($self, $start, $end, $attrib) = @_;
1119 my @part_export = $self->part_svc->part_export_usage;
1120 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
1121 " service definition"
1122 unless @part_export;
1127 foreach my $part_export ( @part_export ) {
1129 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
1136 =item tickets [ STATUS ]
1138 Returns an array of hashes representing the tickets linked to this service.
1140 An optional status (or arrayref or hashref of statuses) may be specified.
1146 my $status = ( @_ && $_[0] ) ? shift : '';
1148 my $conf = FS::Conf->new;
1149 my $num = $conf->config('cust_main-max_tickets') || 10;
1152 if ( $conf->config('ticket_system') ) {
1153 unless ( $conf->config('ticket_system-custom_priority_field') ) {
1155 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
1164 foreach my $priority (
1165 $conf->config('ticket_system-custom_priority_field-values'), ''
1167 last if scalar(@tickets) >= $num;
1169 @{ FS::TicketSystem->service_tickets( $self->svcnum,
1170 $num - scalar(@tickets),
1183 my $svc_x = $self->svc_x;
1184 +{ ( map { $_=>$self->$_ } $self->fields ),
1185 ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
1195 =item smart_search OPTION => VALUE ...
1197 Accepts the option I<search>, the string to search for. The string will
1198 be searched for as a username, email address, IP address, MAC address,
1199 phone number, and hardware serial number. Unlike the I<smart_search> on
1200 customers, this always requires an exact match.
1204 # though perhaps it should be fuzzy in some cases?
1207 my %param = __PACKAGE__->smart_search_param(@_);
1211 sub smart_search_param {
1215 my $string = $opt{'search'};
1216 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1219 map { my $table = $_;
1220 my $search_sql = "FS::$table"->search_sql($string);
1221 my $addl_from = "FS::$table"->search_sql_addl_from();
1223 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1224 "FROM $table $addl_from WHERE $search_sql";
1226 FS::part_svc->svc_tables;
1228 if ( $string =~ /^(\d+)$/ ) {
1229 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1232 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1233 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1237 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1238 'null_right' => 'View/link unlinked services'
1240 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1242 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1243 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1244 ' LEFT JOIN part_svc USING ( svcpart )';
1247 'table' => 'cust_svc',
1248 'select' => 'svc_all.svcnum AS svcnum, '.
1249 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1251 'addl_from' => $addl_from,
1253 'extra_sql' => $extra_sql,
1257 # If the associated cust_pkg is 'on hold'
1258 # and the associated pkg_svc has the provision_hold flag
1259 # and there are no more available_part_svcs on the cust_pkg similarly flagged,
1260 # then removes hold from pkg
1261 # returns $error or '' on success,
1262 # does not indicate if pkg status was changed
1263 sub _check_provision_hold {
1266 # check status of cust_pkg
1267 my $cust_pkg = $self->cust_pkg;
1268 return '' unless $cust_pkg && $cust_pkg->status eq 'on hold';
1270 # check flag on this svc
1271 # small false laziness with $self->pkg_svc
1272 # to avoid looking up cust_pkg twice
1273 my $pkg_svc = qsearchs( 'pkg_svc', {
1274 'svcpart' => $self->svcpart,
1275 'pkgpart' => $cust_pkg->pkgpart,
1277 return '' unless $pkg_svc->provision_hold;
1279 # check for any others available with that flag
1280 return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 );
1282 # conditions met, remove hold
1283 return $cust_pkg->unsuspend;
1289 # fix missing (deleted by mistake) svc_x records
1290 warn "searching for missing svc_x records...\n";
1292 'table' => 'cust_svc',
1293 'select' => 'cust_svc.*',
1294 'addl_from' => ' LEFT JOIN ( ' .
1296 map { "SELECT svcnum FROM $_" }
1297 FS::part_svc->svc_tables
1298 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1299 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1301 my @svcs = qsearch(\%search);
1302 warn "found ".scalar(@svcs)."\n";
1304 local $FS::Record::nowarn_classload = 1; # for h_svc_
1305 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1308 'hashref' => { history_action => 'delete' },
1309 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1311 foreach my $cust_svc (@svcs) {
1312 my $svcnum = $cust_svc->svcnum;
1313 my $svcdb = $cust_svc->part_svc->svcdb;
1314 $h_search{'hashref'}{'svcnum'} = $svcnum;
1315 $h_search{'table'} = "h_$svcdb";
1316 my $h_svc_x = qsearchs(\%h_search);
1318 my $class = "FS::$svcdb";
1319 my $new_svc_x = $class->new({ $h_svc_x->hash });
1320 my $error = $new_svc_x->insert;
1321 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1324 # can't be fixed, so remove the dangling cust_svc to avoid breaking
1326 my $error = $cust_svc->delete;
1327 warn "error cleaning up missing svcnum $svcnum ($svcdb):\n$error\n";
1338 Behaviour of changing the svcpart of cust_svc records is undefined and should
1339 possibly be prohibited, and pkg_svc records are not checked.
1341 pkg_svc records are not checked in general (here).
1343 Deleting this record doesn't check or delete the svc_* record associated
1346 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1347 a DBI database handle is not yet implemented.
1351 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1352 schema.html from the base documentation