4 use vars qw( @ISA $DEBUG $me $ignore_quantity $conf $ticket_system );
6 #use Scalar::Util qw( blessed );
7 use List::Util qw( max );
9 use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
14 use FS::domain_record;
19 #most FS::svc_ classes are autoloaded in svc_x emthod
20 use FS::svc_acct; #this one is used in the cache stuff
22 @ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
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 my $error = $self->SUPER::delete;
162 return $error if $error;
164 if ( $ticket_system eq 'RT_Internal' ) {
165 unless ( $rt_session ) {
166 FS::TicketSystem->init;
167 $rt_session = FS::TicketSystem->session;
169 my $links = RT::Links->new($rt_session->{CurrentUser});
170 my $svcnum = $self->svcnum;
171 $links->Limit(FIELD => 'Target',
172 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
173 while ( my $l = $links->Next ) {
176 # re-link to point to the customer instead
178 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
181 ($val, $msg) = $l->Delete;
183 # can't do anything useful on error
184 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
191 Cancels the relevant service by calling the B<cancel> method of the associated
192 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
193 deleting the FS::svc_XXX record and then deleting this record.
195 If there is an error, returns the error, otherwise returns false.
202 local $SIG{HUP} = 'IGNORE';
203 local $SIG{INT} = 'IGNORE';
204 local $SIG{QUIT} = 'IGNORE';
205 local $SIG{TERM} = 'IGNORE';
206 local $SIG{TSTP} = 'IGNORE';
207 local $SIG{PIPE} = 'IGNORE';
209 my $oldAutoCommit = $FS::UID::AutoCommit;
210 local $FS::UID::AutoCommit = 0;
213 my $part_svc = $self->part_svc;
215 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
216 $dbh->rollback if $oldAutoCommit;
217 return "Illegal svcdb value in part_svc!";
220 require "FS/$svcdb.pm";
222 my $svc = $self->svc_x;
224 if ( %opt && $opt{'date'} ) {
225 my $error = $svc->expire($opt{'date'});
227 $dbh->rollback if $oldAutoCommit;
228 return "Error expiring service: $error";
231 my $error = $svc->cancel;
233 $dbh->rollback if $oldAutoCommit;
234 return "Error canceling service: $error";
236 $error = $svc->delete; #this deletes this cust_svc record as well
238 $dbh->rollback if $oldAutoCommit;
239 return "Error deleting service: $error";
246 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
247 "; deleting cust_svc only\n";
249 my $error = $self->delete;
251 $dbh->rollback if $oldAutoCommit;
252 return "Error deleting cust_svc: $error";
257 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
263 =item overlimit [ ACTION ]
265 Retrieves or sets the overlimit date. If ACTION is absent, return
266 the present value of overlimit. If ACTION is present, it can
267 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
268 is set to the current time if it is not already set. The 'unsuspend' value
269 causes the time to be cleared.
271 If there is an error on setting, returns the error, otherwise returns false.
277 my $action = shift or return $self->getfield('overlimit');
279 local $SIG{HUP} = 'IGNORE';
280 local $SIG{INT} = 'IGNORE';
281 local $SIG{QUIT} = 'IGNORE';
282 local $SIG{TERM} = 'IGNORE';
283 local $SIG{TSTP} = 'IGNORE';
284 local $SIG{PIPE} = 'IGNORE';
286 my $oldAutoCommit = $FS::UID::AutoCommit;
287 local $FS::UID::AutoCommit = 0;
290 if ( $action eq 'suspend' ) {
291 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
292 }elsif ( $action eq 'unsuspend' ) {
293 $self->setfield('overlimit', '');
295 die "unexpected action value: $action";
298 local $ignore_quantity = 1;
299 my $error = $self->replace;
301 $dbh->rollback if $oldAutoCommit;
302 return "Error setting overlimit: $error";
305 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
311 =item replace OLD_RECORD
313 Replaces the OLD_RECORD with this one in the database. If there is an error,
314 returns the error, otherwise returns false.
321 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
323 # : $new->replace_old;
324 my ( $new, $old ) = ( shift, shift );
325 $old = $new->replace_old unless defined($old);
327 local $SIG{HUP} = 'IGNORE';
328 local $SIG{INT} = 'IGNORE';
329 local $SIG{QUIT} = 'IGNORE';
330 local $SIG{TERM} = 'IGNORE';
331 local $SIG{TSTP} = 'IGNORE';
332 local $SIG{PIPE} = 'IGNORE';
334 my $oldAutoCommit = $FS::UID::AutoCommit;
335 local $FS::UID::AutoCommit = 0;
338 if ( $new->svcpart != $old->svcpart ) {
339 my $svc_x = $new->svc_x;
340 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
341 local($FS::Record::nowarn_identical) = 1;
342 my $error = $new_svc_x->replace($svc_x);
344 $dbh->rollback if $oldAutoCommit;
345 return $error if $error;
349 # #trigger a re-export on pkgnum changes?
350 # # (of prepaid packages), for Expiration RADIUS attribute
351 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
352 # my $svc_x = $new->svc_x;
353 # local($FS::Record::nowarn_identical) = 1;
354 # my $error = $svc_x->export('replace');
356 # $dbh->rollback if $oldAutoCommit;
357 # return $error if $error;
361 #trigger a pkg_change export on pkgnum changes
362 if ( $new->pkgnum != $old->pkgnum ) {
363 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
368 $dbh->rollback if $oldAutoCommit;
369 return $error if $error;
371 } # if pkgnum is changing
373 #my $error = $new->SUPER::replace($old, @_);
374 my $error = $new->SUPER::replace($old);
376 #trigger a relocate export on location changes
377 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
378 my $svc_x = $new->svc_x;
379 if ( $svc_x->locationnum ) {
380 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
381 # in this case, set the service location to be the same as the new
383 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
384 # and replace it, which triggers a relocate export so we don't
386 $error ||= $svc_x->replace;
388 # the service already has a different location from its package
392 # the service doesn't have a locationnum (either isn't of a type
393 # that has the locationnum field, or the locationnum is null and
394 # defaults to cust_pkg->locationnum)
395 # so just trigger the export here
396 $error ||= $new->svc_x->export('relocate',
397 $new->cust_pkg->cust_location,
398 $old->cust_pkg->cust_location,
400 } # if ($svc_x->locationnum)
401 } # if this is a location change
403 #check if this releases a hold (see FS::pkg_svc provision_hold)
404 $error ||= $new->_check_provision_hold;
407 $dbh->rollback if $oldAutoCommit;
408 return $error if $error
411 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
418 Checks all fields to make sure this is a valid service. If there is an error,
419 returns the error, otherwise returns false. Called by the insert and
428 $self->ut_numbern('svcnum')
429 || $self->ut_numbern('pkgnum')
430 || $self->ut_number('svcpart')
431 || $self->ut_numbern('agent_svcid')
432 || $self->ut_numbern('overlimit')
434 return $error if $error;
436 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
437 return "Unknown svcpart" unless $part_svc;
439 if ( $self->pkgnum && ! $ignore_quantity ) {
441 #slightly inefficient since ->pkg_svc will also look it up, but fixing
442 # a much larger perf problem and have bigger fish to fry
443 my $cust_pkg = $self->cust_pkg;
445 my $pkg_svc = $self->pkg_svc
446 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
447 'pkgpart' => $cust_pkg->pkgpart,
451 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
452 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
453 my $addon_pkg_svc = qsearchs('pkg_svc', {
454 pkgpart => $part_pkg_link->dst_pkgpart,
455 svcpart => $self->svcpart,
457 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
461 #better error message? UI shouldn't get here
462 return "No svcpart ". $self->svcpart.
463 " services in pkgpart ". $cust_pkg->pkgpart
464 unless $pkg_svc->quantity > 0;
466 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
468 #false laziness w/cust_pkg->part_svc
469 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
473 #better error message? again, UI shouldn't get here
474 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
475 " services for pkgnum ". $self->pkgnum
485 Returns the displayed service number for this service: agent_svcid if it has a
486 value, svcnum otherwise
492 $self->agent_svcid || $self->svcnum;
497 Returns the definition for this service, as a FS::part_svc object (see
504 return $self->{_svcpart} if $self->{_svcpart};
505 cluck 'cust_svc->part_svc called' if $DEBUG;
506 qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
511 Returns the package this service belongs to, as a FS::cust_pkg object (see
518 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
523 Returns the pkg_svc record for for this service, if applicable.
529 my $cust_pkg = $self->cust_pkg;
530 return undef unless $cust_pkg;
532 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
533 'pkgpart' => $cust_pkg->pkgpart,
540 Returns the date this service was inserted.
546 $self->h_date('insert');
549 =item pkg_cancel_date
551 Returns the date this service's package was canceled. This normally only
552 exists for a service that's been preserved through cancellation with the
553 part_pkg.preserve flag.
557 sub pkg_cancel_date {
559 my $cust_pkg = $self->cust_pkg or return;
560 return $cust_pkg->getfield('cancel') || '';
565 Returns a list consisting of:
566 - The name of this service (from part_svc)
567 - A meaningful identifier (username, domain, or mail alias)
568 - The table name (i.e. svc_domain) for this service
573 my($label, $value, $svcdb) = $cust_svc->label;
577 Like the B<label> method, except the second item in the list ("meaningful
578 identifier") may be longer - typically, a full name is included.
582 sub label { shift->_label('svc_label', @_); }
583 sub label_long { shift->_label('svc_label_long', @_); }
588 my $svc_x = $self->svc_x
589 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
591 $self->$method($svc_x);
594 sub svc_label { shift->_svc_label('label', @_); }
595 sub svc_label_long { shift->_svc_label('label_long', @_); }
598 my( $self, $method, $svc_x ) = ( shift, shift, shift );
601 $self->part_svc->svc,
603 $self->part_svc->svcdb,
611 Returns a listref of html elements associated with this service's exports.
617 my $svc_x = $self->svc_x
618 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
620 $svc_x->export_links;
623 =item export_getsettings
625 Returns two hashrefs of settings associated with this service's exports.
629 sub export_getsettings {
631 my $svc_x = $self->svc_x
632 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
634 $svc_x->export_getsettings;
640 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
641 FS::svc_domain object, etc.)
647 my $svcdb = $self->part_svc->svcdb;
648 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
649 $self->{'_svc_acct'};
651 require "FS/$svcdb.pm";
652 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
653 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
655 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
659 =item seconds_since TIMESTAMP
661 See L<FS::svc_acct/seconds_since>. Equivalent to
662 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
663 where B<svcdb> is not "svc_acct".
667 #internal session db deprecated (or at least on hold)
668 sub seconds_since { 'internal session db deprecated'; };
669 ##note: implementation here, POD in FS::svc_acct
671 # my($self, $since) = @_;
673 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
676 # AND logout IS NOT NULL'
677 # ) or die $dbh->errstr;
678 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
679 # $sth->fetchrow_arrayref->[0];
682 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
684 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
685 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
686 for records where B<svcdb> is not "svc_acct".
690 #note: implementation here, POD in FS::svc_acct
691 sub seconds_since_sqlradacct {
692 my($self, $start, $end) = @_;
694 my $mes = "$me seconds_since_sqlradacct:";
696 my $svc_x = $self->svc_x;
698 my @part_export = $self->part_svc->part_export_usage;
699 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
700 " service definition"
705 foreach my $part_export ( @part_export ) {
707 next if $part_export->option('ignore_accounting');
709 warn "$mes connecting to sqlradius database\n"
712 my $dbh = DBI->connect( map { $part_export->option($_) }
713 qw(datasrc username password) )
714 or die "can't connect to sqlradius database: ". $DBI::errstr;
716 warn "$mes connected to sqlradius database\n"
719 #select a unix time conversion function based on database type
720 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
721 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
723 my $username = $part_export->export_username($svc_x);
727 warn "$mes finding closed sessions completely within the given range\n"
732 if ($part_export->option('process_single_realm')) {
733 $realm = 'AND Realm = ?';
734 $realmparam = $part_export->option('realm');
737 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
741 AND $str2time AcctStartTime $closing >= ?
742 AND $str2time AcctStopTime $closing < ?
743 AND $str2time AcctStopTime $closing > 0
744 AND AcctStopTime IS NOT NULL"
745 ) or die $dbh->errstr;
746 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
748 my $regular = $sth->fetchrow_arrayref->[0];
750 warn "$mes finding open sessions which start in the range\n"
753 # count session start->range end
754 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
758 AND $str2time AcctStartTime $closing >= ?
759 AND $str2time AcctStartTime $closing < ?
760 AND ( ? - $str2time AcctStartTime $closing ) < 86400
761 AND ( $str2time AcctStopTime $closing = 0
762 OR AcctStopTime IS NULL )";
763 $sth = $dbh->prepare($query) or die $dbh->errstr;
766 ($realm ? $realmparam : ()),
770 or die $sth->errstr. " executing query $query";
771 my $start_during = $sth->fetchrow_arrayref->[0];
773 warn "$mes finding closed sessions which start before the range but stop during\n"
776 #count range start->session end
777 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
781 AND $str2time AcctStartTime $closing < ?
782 AND $str2time AcctStopTime $closing >= ?
783 AND $str2time AcctStopTime $closing < ?
784 AND $str2time AcctStopTime $closing > 0
785 AND AcctStopTime IS NOT NULL"
786 ) or die $dbh->errstr;
787 $sth->execute( $start,
789 ($realm ? $realmparam : ()),
794 my $end_during = $sth->fetchrow_arrayref->[0];
796 warn "$mes finding closed sessions which start before the range but stop after\n"
799 # count range start->range end
800 # don't count open sessions anymore (probably missing stop record)
801 $sth = $dbh->prepare("SELECT COUNT(*)
805 AND $str2time AcctStartTime $closing < ?
806 AND ( $str2time AcctStopTime $closing >= ?
808 # OR AcctStopTime = 0
809 # OR AcctStopTime IS NULL )"
810 ) or die $dbh->errstr;
811 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
813 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
815 $seconds += $regular + $end_during + $start_during + $entire_range;
817 warn "$mes done finding sessions\n"
826 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
828 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
829 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
830 for records where B<svcdb> is not "svc_acct".
834 #note: implementation here, POD in FS::svc_acct
835 #(false laziness w/seconds_since_sqlradacct above)
836 sub attribute_since_sqlradacct {
837 my($self, $start, $end, $attrib) = @_;
839 my $mes = "$me attribute_since_sqlradacct:";
841 my $svc_x = $self->svc_x;
843 my @part_export = $self->part_svc->part_export_usage;
844 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
845 " service definition"
851 foreach my $part_export ( @part_export ) {
853 next if $part_export->option('ignore_accounting');
855 warn "$mes connecting to sqlradius database\n"
858 my $dbh = DBI->connect( map { $part_export->option($_) }
859 qw(datasrc username password) )
860 or die "can't connect to sqlradius database: ". $DBI::errstr;
862 warn "$mes connected to sqlradius database\n"
865 #select a unix time conversion function based on database type
866 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
867 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
869 my $username = $part_export->export_username($svc_x);
871 warn "$mes SUMing $attrib sessions\n"
876 if ($part_export->option('process_single_realm')) {
877 $realm = 'AND Realm = ?';
878 $realmparam = $part_export->option('realm');
881 my $sth = $dbh->prepare("SELECT SUM($attrib)
885 AND $str2time AcctStopTime $closing >= ?
886 AND $str2time AcctStopTime $closing < ?
887 AND AcctStopTime IS NOT NULL"
888 ) or die $dbh->errstr;
889 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
892 my $row = $sth->fetchrow_arrayref;
893 $sum += $row->[0] if defined($row->[0]);
895 warn "$mes done SUMing sessions\n"
904 #note: implementation here, POD in FS::svc_acct
905 # false laziness w/above
906 sub attribute_last_sqlradacct {
907 my($self, $attrib) = @_;
909 my $mes = "$me attribute_last_sqlradacct:";
911 my $svc_x = $self->svc_x;
913 my @part_export = $self->part_svc->part_export_usage;
914 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
915 " service definition"
920 my $AcctStartTime = 0;
922 foreach my $part_export ( @part_export ) {
924 next if $part_export->option('ignore_accounting');
926 warn "$mes connecting to sqlradius database\n"
929 my $dbh = DBI->connect( map { $part_export->option($_) }
930 qw(datasrc username password) )
931 or die "can't connect to sqlradius database: ". $DBI::errstr;
933 warn "$mes connected to sqlradius database\n"
936 #select a unix time conversion function based on database type
937 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
938 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
940 my $username = $part_export->export_username($svc_x);
942 warn "$mes finding most-recent $attrib\n"
947 if ($part_export->option('process_single_realm')) {
948 $realm = 'AND Realm = ?';
949 $realmparam = $part_export->option('realm');
952 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
956 ORDER BY AcctStartTime DESC LIMIT 1
957 ") or die $dbh->errstr;
958 $sth->execute($username, ($realm ? $realmparam : ()) )
961 my $row = $sth->fetchrow_arrayref;
962 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
964 $AcctStartTime = $row->[1];
976 =item get_session_history TIMESTAMP_START TIMESTAMP_END
978 See L<FS::svc_acct/get_session_history>. Equivalent to
979 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
980 records where B<svcdb> is not "svc_acct".
984 sub get_session_history {
985 my($self, $start, $end, $attrib) = @_;
989 my @part_export = $self->part_svc->part_export_usage;
990 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
991 " service definition"
997 foreach my $part_export ( @part_export ) {
999 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
1006 =item tickets [ STATUS ]
1008 Returns an array of hashes representing the tickets linked to this service.
1010 An optional status (or arrayref or hashref of statuses) may be specified.
1016 my $status = ( @_ && $_[0] ) ? shift : '';
1018 my $conf = FS::Conf->new;
1019 my $num = $conf->config('cust_main-max_tickets') || 10;
1022 if ( $conf->config('ticket_system') ) {
1023 unless ( $conf->config('ticket_system-custom_priority_field') ) {
1025 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
1034 foreach my $priority (
1035 $conf->config('ticket_system-custom_priority_field-values'), ''
1037 last if scalar(@tickets) >= $num;
1039 @{ FS::TicketSystem->service_tickets( $self->svcnum,
1040 $num - scalar(@tickets),
1058 =item smart_search OPTION => VALUE ...
1060 Accepts the option I<search>, the string to search for. The string will
1061 be searched for as a username, email address, IP address, MAC address,
1062 phone number, and hardware serial number. Unlike the I<smart_search> on
1063 customers, this always requires an exact match.
1067 # though perhaps it should be fuzzy in some cases?
1070 my %param = __PACKAGE__->smart_search_param(@_);
1074 sub smart_search_param {
1078 my $string = $opt{'search'};
1079 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1082 map { my $table = $_;
1083 my $search_sql = "FS::$table"->search_sql($string);
1084 my $addl_from = "FS::$table"->search_sql_addl_from();
1086 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1087 "FROM $table $addl_from WHERE $search_sql";
1089 FS::part_svc->svc_tables;
1091 if ( $string =~ /^(\d+)$/ ) {
1092 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1095 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1096 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1100 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1101 'null_right' => 'View/link unlinked services'
1103 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1105 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1106 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1107 ' LEFT JOIN part_svc USING ( svcpart )';
1110 'table' => 'cust_svc',
1111 'select' => 'svc_all.svcnum AS svcnum, '.
1112 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1114 'addl_from' => $addl_from,
1116 'extra_sql' => $extra_sql,
1120 # If the associated cust_pkg is 'on hold'
1121 # and the associated pkg_svc has the provision_hold flag
1122 # and there are no more available_part_svcs on the cust_pkg similarly flagged,
1123 # then removes hold from pkg
1124 # returns $error or '' on success,
1125 # does not indicate if pkg status was changed
1126 sub _check_provision_hold {
1129 # check status of cust_pkg
1130 my $cust_pkg = $self->cust_pkg;
1131 return '' unless $cust_pkg && $cust_pkg->status eq 'on hold';
1133 # check flag on this svc
1134 # small false laziness with $self->pkg_svc
1135 # to avoid looking up cust_pkg twice
1136 my $pkg_svc = qsearchs( 'pkg_svc', {
1137 'svcpart' => $self->svcpart,
1138 'pkgpart' => $cust_pkg->pkgpart,
1140 return '' unless $pkg_svc->provision_hold;
1142 # check for any others available with that flag
1143 return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 );
1145 # conditions met, remove hold
1146 return $cust_pkg->unsuspend;
1152 # fix missing (deleted by mistake) svc_x records
1153 warn "searching for missing svc_x records...\n";
1155 'table' => 'cust_svc',
1156 'select' => 'cust_svc.*',
1157 'addl_from' => ' LEFT JOIN ( ' .
1159 map { "SELECT svcnum FROM $_" }
1160 FS::part_svc->svc_tables
1161 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1162 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1164 my @svcs = qsearch(\%search);
1165 warn "found ".scalar(@svcs)."\n";
1167 local $FS::Record::nowarn_classload = 1; # for h_svc_
1168 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1171 'hashref' => { history_action => 'delete' },
1172 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1174 foreach my $cust_svc (@svcs) {
1175 my $svcnum = $cust_svc->svcnum;
1176 my $svcdb = $cust_svc->part_svc->svcdb;
1177 $h_search{'hashref'}{'svcnum'} = $svcnum;
1178 $h_search{'table'} = "h_$svcdb";
1179 my $h_svc_x = qsearchs(\%h_search)
1181 my $class = "FS::$svcdb";
1182 my $new_svc_x = $class->new({ $h_svc_x->hash });
1183 my $error = $new_svc_x->insert;
1184 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1195 Behaviour of changing the svcpart of cust_svc records is undefined and should
1196 possibly be prohibited, and pkg_svc records are not checked.
1198 pkg_svc records are not checked in general (here).
1200 Deleting this record doesn't check or delete the svc_* record associated
1203 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1204 a DBI database handle is not yet implemented.
1208 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1209 schema.html from the base documentation