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')
37 my ( $hashref, $cache ) = @_;
38 if ( $hashref->{'username'} ) {
39 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
41 if ( $hashref->{'svc'} ) {
42 $self->{'_svcpart'} = FS::part_svc->new($hashref);
48 FS::cust_svc - Object method for cust_svc objects
54 $record = new FS::cust_svc \%hash
55 $record = new FS::cust_svc { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ($label, $value) = $record->label;
69 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
70 The following fields are currently supported:
74 =item svcnum - primary key (assigned automatically for new services)
76 =item pkgnum - Package (see L<FS::cust_pkg>)
78 =item svcpart - Service definition (see L<FS::part_svc>)
80 =item agent_svcid - Optional legacy service ID
82 =item overlimit - date the service exceeded its usage limit
92 Creates a new service. To add the refund to the database, see L<"insert">.
93 Services are normally created by creating FS::svc_ objects (see
94 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
98 sub table { 'cust_svc'; }
102 Adds this service to the database. If there is an error, returns the error,
103 otherwise returns false.
110 local $SIG{HUP} = 'IGNORE';
111 local $SIG{INT} = 'IGNORE';
112 local $SIG{QUIT} = 'IGNORE';
113 local $SIG{TERM} = 'IGNORE';
114 local $SIG{TSTP} = 'IGNORE';
115 local $SIG{PIPE} = 'IGNORE';
117 my $oldAutoCommit = $FS::UID::AutoCommit;
118 local $FS::UID::AutoCommit = 0;
121 my $error = $self->SUPER::insert;
123 #check if this releases a hold (see FS::pkg_svc provision_hold)
124 $error ||= $self->_check_provision_hold;
127 $dbh->rollback if $oldAutoCommit;
128 return $error if $error
131 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
138 Deletes this service from the database. If there is an error, returns the
139 error, otherwise returns false. Note that this only removes the cust_svc
140 record - you should probably use the B<cancel> method instead.
149 my $cust_pkg = $self->cust_pkg;
150 my $custnum = $cust_pkg->custnum if $cust_pkg;
152 my $error = $self->SUPER::delete;
153 return $error if $error;
155 if ( $ticket_system eq 'RT_Internal' ) {
156 unless ( $rt_session ) {
157 FS::TicketSystem->init;
158 $rt_session = FS::TicketSystem->session;
160 my $links = RT::Links->new($rt_session->{CurrentUser});
161 my $svcnum = $self->svcnum;
162 $links->Limit(FIELD => 'Target',
163 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
164 while ( my $l = $links->Next ) {
167 # re-link to point to the customer instead
169 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
172 ($val, $msg) = $l->Delete;
174 # can't do anything useful on error
175 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
182 Cancels the relevant service by calling the B<cancel> method of the associated
183 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
184 deleting the FS::svc_XXX record and then deleting this record.
186 If there is an error, returns the error, otherwise returns false.
193 local $SIG{HUP} = 'IGNORE';
194 local $SIG{INT} = 'IGNORE';
195 local $SIG{QUIT} = 'IGNORE';
196 local $SIG{TERM} = 'IGNORE';
197 local $SIG{TSTP} = 'IGNORE';
198 local $SIG{PIPE} = 'IGNORE';
200 my $oldAutoCommit = $FS::UID::AutoCommit;
201 local $FS::UID::AutoCommit = 0;
204 my $part_svc = $self->part_svc;
206 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
207 $dbh->rollback if $oldAutoCommit;
208 return "Illegal svcdb value in part_svc!";
211 require "FS/$svcdb.pm";
213 my $svc = $self->svc_x;
215 if ( %opt && $opt{'date'} ) {
216 my $error = $svc->expire($opt{'date'});
218 $dbh->rollback if $oldAutoCommit;
219 return "Error expiring service: $error";
222 my $error = $svc->cancel;
224 $dbh->rollback if $oldAutoCommit;
225 return "Error canceling service: $error";
227 $error = $svc->delete; #this deletes this cust_svc record as well
229 $dbh->rollback if $oldAutoCommit;
230 return "Error deleting service: $error";
237 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
238 "; deleting cust_svc only\n";
240 my $error = $self->delete;
242 $dbh->rollback if $oldAutoCommit;
243 return "Error deleting cust_svc: $error";
248 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
254 =item overlimit [ ACTION ]
256 Retrieves or sets the overlimit date. If ACTION is absent, return
257 the present value of overlimit. If ACTION is present, it can
258 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
259 is set to the current time if it is not already set. The 'unsuspend' value
260 causes the time to be cleared.
262 If there is an error on setting, returns the error, otherwise returns false.
268 my $action = shift or return $self->getfield('overlimit');
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 if ( $action eq 'suspend' ) {
282 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
283 }elsif ( $action eq 'unsuspend' ) {
284 $self->setfield('overlimit', '');
286 die "unexpected action value: $action";
289 local $ignore_quantity = 1;
290 my $error = $self->replace;
292 $dbh->rollback if $oldAutoCommit;
293 return "Error setting overlimit: $error";
296 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
302 =item replace OLD_RECORD
304 Replaces the OLD_RECORD with this one in the database. If there is an error,
305 returns the error, otherwise returns false.
312 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
314 # : $new->replace_old;
315 my ( $new, $old ) = ( shift, shift );
316 $old = $new->replace_old unless defined($old);
318 local $SIG{HUP} = 'IGNORE';
319 local $SIG{INT} = 'IGNORE';
320 local $SIG{QUIT} = 'IGNORE';
321 local $SIG{TERM} = 'IGNORE';
322 local $SIG{TSTP} = 'IGNORE';
323 local $SIG{PIPE} = 'IGNORE';
325 my $oldAutoCommit = $FS::UID::AutoCommit;
326 local $FS::UID::AutoCommit = 0;
329 if ( $new->svcpart != $old->svcpart ) {
330 my $svc_x = $new->svc_x;
331 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
332 local($FS::Record::nowarn_identical) = 1;
333 my $error = $new_svc_x->replace($svc_x);
335 $dbh->rollback if $oldAutoCommit;
336 return $error if $error;
340 # #trigger a re-export on pkgnum changes?
341 # # (of prepaid packages), for Expiration RADIUS attribute
342 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
343 # my $svc_x = $new->svc_x;
344 # local($FS::Record::nowarn_identical) = 1;
345 # my $error = $svc_x->export('replace');
347 # $dbh->rollback if $oldAutoCommit;
348 # return $error if $error;
352 #trigger a pkg_change export on pkgnum changes
353 if ( $new->pkgnum != $old->pkgnum ) {
354 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
359 $dbh->rollback if $oldAutoCommit;
360 return $error if $error;
362 } # if pkgnum is changing
364 #my $error = $new->SUPER::replace($old, @_);
365 my $error = $new->SUPER::replace($old);
367 #trigger a relocate export on location changes
368 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
369 my $svc_x = $new->svc_x;
370 if ( $svc_x->locationnum ) {
371 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
372 # in this case, set the service location to be the same as the new
374 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
375 # and replace it, which triggers a relocate export so we don't
377 $error ||= $svc_x->replace;
379 # the service already has a different location from its package
383 # the service doesn't have a locationnum (either isn't of a type
384 # that has the locationnum field, or the locationnum is null and
385 # defaults to cust_pkg->locationnum)
386 # so just trigger the export here
387 $error ||= $new->svc_x->export('relocate',
388 $new->cust_pkg->cust_location,
389 $old->cust_pkg->cust_location,
391 } # if ($svc_x->locationnum)
392 } # if this is a location change
394 #check if this releases a hold (see FS::pkg_svc provision_hold)
395 $error ||= $new->_check_provision_hold;
398 $dbh->rollback if $oldAutoCommit;
399 return $error if $error
402 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
409 Checks all fields to make sure this is a valid service. If there is an error,
410 returns the error, otherwise returns false. Called by the insert and
419 $self->ut_numbern('svcnum')
420 || $self->ut_numbern('pkgnum')
421 || $self->ut_number('svcpart')
422 || $self->ut_numbern('agent_svcid')
423 || $self->ut_numbern('overlimit')
425 return $error if $error;
427 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
428 return "Unknown svcpart" unless $part_svc;
430 if ( $self->pkgnum && ! $ignore_quantity ) {
432 #slightly inefficient since ->pkg_svc will also look it up, but fixing
433 # a much larger perf problem and have bigger fish to fry
434 my $cust_pkg = $self->cust_pkg;
436 my $pkg_svc = $self->pkg_svc
437 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
438 'pkgpart' => $cust_pkg->pkgpart,
442 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
443 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
444 my $addon_pkg_svc = qsearchs('pkg_svc', {
445 pkgpart => $part_pkg_link->dst_pkgpart,
446 svcpart => $self->svcpart,
448 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
452 #better error message? UI shouldn't get here
453 return "No svcpart ". $self->svcpart.
454 " services in pkgpart ". $cust_pkg->pkgpart
455 unless $pkg_svc->quantity > 0;
457 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
459 #false laziness w/cust_pkg->part_svc
460 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
464 #better error message? again, UI shouldn't get here
465 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
466 " services for pkgnum ". $self->pkgnum
476 Returns the displayed service number for this service: agent_svcid if it has a
477 value, svcnum otherwise
483 $self->agent_svcid || $self->svcnum;
488 Returns the definition for this service, as a FS::part_svc object (see
496 ? $self->{'_svcpart'}
497 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
502 Returns the package this service belongs to, as a FS::cust_pkg object (see
509 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
514 Returns the pkg_svc record for for this service, if applicable.
520 my $cust_pkg = $self->cust_pkg;
521 return undef unless $cust_pkg;
523 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
524 'pkgpart' => $cust_pkg->pkgpart,
531 Returns the date this service was inserted.
537 $self->h_date('insert');
540 =item pkg_cancel_date
542 Returns the date this service's package was canceled. This normally only
543 exists for a service that's been preserved through cancellation with the
544 part_pkg.preserve flag.
548 sub pkg_cancel_date {
550 my $cust_pkg = $self->cust_pkg or return;
551 return $cust_pkg->getfield('cancel') || '';
556 Returns a list consisting of:
557 - The name of this service (from part_svc)
558 - A meaningful identifier (username, domain, or mail alias)
559 - The table name (i.e. svc_domain) for this service
564 my($label, $value, $svcdb) = $cust_svc->label;
568 Like the B<label> method, except the second item in the list ("meaningful
569 identifier") may be longer - typically, a full name is included.
573 sub label { shift->_label('svc_label', @_); }
574 sub label_long { shift->_label('svc_label_long', @_); }
579 my $svc_x = $self->svc_x
580 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
582 $self->$method($svc_x);
585 sub svc_label { shift->_svc_label('label', @_); }
586 sub svc_label_long { shift->_svc_label('label_long', @_); }
589 my( $self, $method, $svc_x ) = ( shift, shift, shift );
592 $self->part_svc->svc,
594 $self->part_svc->svcdb,
602 Returns a listref of html elements associated with this service's exports.
608 my $svc_x = $self->svc_x
609 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
611 $svc_x->export_links;
614 =item export_getsettings
616 Returns two hashrefs of settings associated with this service's exports.
620 sub export_getsettings {
622 my $svc_x = $self->svc_x
623 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
625 $svc_x->export_getsettings;
631 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
632 FS::svc_domain object, etc.)
638 my $svcdb = $self->part_svc->svcdb;
639 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
640 $self->{'_svc_acct'};
642 require "FS/$svcdb.pm";
643 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
644 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
646 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
650 =item seconds_since TIMESTAMP
652 See L<FS::svc_acct/seconds_since>. Equivalent to
653 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
654 where B<svcdb> is not "svc_acct".
658 #internal session db deprecated (or at least on hold)
659 sub seconds_since { 'internal session db deprecated'; };
660 ##note: implementation here, POD in FS::svc_acct
662 # my($self, $since) = @_;
664 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
667 # AND logout IS NOT NULL'
668 # ) or die $dbh->errstr;
669 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
670 # $sth->fetchrow_arrayref->[0];
673 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
675 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
676 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
677 for records where B<svcdb> is not "svc_acct".
681 #note: implementation here, POD in FS::svc_acct
682 sub seconds_since_sqlradacct {
683 my($self, $start, $end) = @_;
685 my $mes = "$me seconds_since_sqlradacct:";
687 my $svc_x = $self->svc_x;
689 my @part_export = $self->part_svc->part_export_usage;
690 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
691 " service definition"
696 foreach my $part_export ( @part_export ) {
698 next if $part_export->option('ignore_accounting');
700 warn "$mes connecting to sqlradius database\n"
703 my $dbh = DBI->connect( map { $part_export->option($_) }
704 qw(datasrc username password) )
705 or die "can't connect to sqlradius database: ". $DBI::errstr;
707 warn "$mes connected to sqlradius database\n"
710 #select a unix time conversion function based on database type
711 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
712 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
714 my $username = $part_export->export_username($svc_x);
718 warn "$mes finding closed sessions completely within the given range\n"
723 if ($part_export->option('process_single_realm')) {
724 $realm = 'AND Realm = ?';
725 $realmparam = $part_export->option('realm');
728 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
732 AND $str2time AcctStartTime $closing >= ?
733 AND $str2time AcctStopTime $closing < ?
734 AND $str2time AcctStopTime $closing > 0
735 AND AcctStopTime IS NOT NULL"
736 ) or die $dbh->errstr;
737 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
739 my $regular = $sth->fetchrow_arrayref->[0];
741 warn "$mes finding open sessions which start in the range\n"
744 # count session start->range end
745 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
749 AND $str2time AcctStartTime $closing >= ?
750 AND $str2time AcctStartTime $closing < ?
751 AND ( ? - $str2time AcctStartTime $closing ) < 86400
752 AND ( $str2time AcctStopTime $closing = 0
753 OR AcctStopTime IS NULL )";
754 $sth = $dbh->prepare($query) or die $dbh->errstr;
757 ($realm ? $realmparam : ()),
761 or die $sth->errstr. " executing query $query";
762 my $start_during = $sth->fetchrow_arrayref->[0];
764 warn "$mes finding closed sessions which start before the range but stop during\n"
767 #count range start->session end
768 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
772 AND $str2time AcctStartTime $closing < ?
773 AND $str2time AcctStopTime $closing >= ?
774 AND $str2time AcctStopTime $closing < ?
775 AND $str2time AcctStopTime $closing > 0
776 AND AcctStopTime IS NOT NULL"
777 ) or die $dbh->errstr;
778 $sth->execute( $start,
780 ($realm ? $realmparam : ()),
785 my $end_during = $sth->fetchrow_arrayref->[0];
787 warn "$mes finding closed sessions which start before the range but stop after\n"
790 # count range start->range end
791 # don't count open sessions anymore (probably missing stop record)
792 $sth = $dbh->prepare("SELECT COUNT(*)
796 AND $str2time AcctStartTime $closing < ?
797 AND ( $str2time AcctStopTime $closing >= ?
799 # OR AcctStopTime = 0
800 # OR AcctStopTime IS NULL )"
801 ) or die $dbh->errstr;
802 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
804 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
806 $seconds += $regular + $end_during + $start_during + $entire_range;
808 warn "$mes done finding sessions\n"
817 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
819 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
820 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
821 for records where B<svcdb> is not "svc_acct".
825 #note: implementation here, POD in FS::svc_acct
826 #(false laziness w/seconds_since_sqlradacct above)
827 sub attribute_since_sqlradacct {
828 my($self, $start, $end, $attrib) = @_;
830 my $mes = "$me attribute_since_sqlradacct:";
832 my $svc_x = $self->svc_x;
834 my @part_export = $self->part_svc->part_export_usage;
835 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
836 " service definition"
842 foreach my $part_export ( @part_export ) {
844 next if $part_export->option('ignore_accounting');
846 warn "$mes connecting to sqlradius database\n"
849 my $dbh = DBI->connect( map { $part_export->option($_) }
850 qw(datasrc username password) )
851 or die "can't connect to sqlradius database: ". $DBI::errstr;
853 warn "$mes connected to sqlradius database\n"
856 #select a unix time conversion function based on database type
857 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
858 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
860 my $username = $part_export->export_username($svc_x);
862 warn "$mes SUMing $attrib sessions\n"
867 if ($part_export->option('process_single_realm')) {
868 $realm = 'AND Realm = ?';
869 $realmparam = $part_export->option('realm');
872 my $sth = $dbh->prepare("SELECT SUM($attrib)
876 AND $str2time AcctStopTime $closing >= ?
877 AND $str2time AcctStopTime $closing < ?
878 AND AcctStopTime IS NOT NULL"
879 ) or die $dbh->errstr;
880 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
883 my $row = $sth->fetchrow_arrayref;
884 $sum += $row->[0] if defined($row->[0]);
886 warn "$mes done SUMing sessions\n"
895 #note: implementation here, POD in FS::svc_acct
896 # false laziness w/above
897 sub attribute_last_sqlradacct {
898 my($self, $attrib) = @_;
900 my $mes = "$me attribute_last_sqlradacct:";
902 my $svc_x = $self->svc_x;
904 my @part_export = $self->part_svc->part_export_usage;
905 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
906 " service definition"
911 my $AcctStartTime = 0;
913 foreach my $part_export ( @part_export ) {
915 next if $part_export->option('ignore_accounting');
917 warn "$mes connecting to sqlradius database\n"
920 my $dbh = DBI->connect( map { $part_export->option($_) }
921 qw(datasrc username password) )
922 or die "can't connect to sqlradius database: ". $DBI::errstr;
924 warn "$mes connected to sqlradius database\n"
927 #select a unix time conversion function based on database type
928 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
929 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
931 my $username = $part_export->export_username($svc_x);
933 warn "$mes finding most-recent $attrib\n"
938 if ($part_export->option('process_single_realm')) {
939 $realm = 'AND Realm = ?';
940 $realmparam = $part_export->option('realm');
943 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
947 ORDER BY AcctStartTime DESC LIMIT 1
948 ") or die $dbh->errstr;
949 $sth->execute($username, ($realm ? $realmparam : ()) )
952 my $row = $sth->fetchrow_arrayref;
953 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
955 $AcctStartTime = $row->[1];
967 =item get_session_history TIMESTAMP_START TIMESTAMP_END
969 See L<FS::svc_acct/get_session_history>. Equivalent to
970 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
971 records where B<svcdb> is not "svc_acct".
975 sub get_session_history {
976 my($self, $start, $end, $attrib) = @_;
980 my @part_export = $self->part_svc->part_export_usage;
981 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
982 " service definition"
988 foreach my $part_export ( @part_export ) {
990 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
997 =item tickets [ STATUS ]
999 Returns an array of hashes representing the tickets linked to this service.
1001 An optional status (or arrayref or hashref of statuses) may be specified.
1007 my $status = ( @_ && $_[0] ) ? shift : '';
1009 my $conf = FS::Conf->new;
1010 my $num = $conf->config('cust_main-max_tickets') || 10;
1013 if ( $conf->config('ticket_system') ) {
1014 unless ( $conf->config('ticket_system-custom_priority_field') ) {
1016 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
1025 foreach my $priority (
1026 $conf->config('ticket_system-custom_priority_field-values'), ''
1028 last if scalar(@tickets) >= $num;
1030 @{ FS::TicketSystem->service_tickets( $self->svcnum,
1031 $num - scalar(@tickets),
1049 =item smart_search OPTION => VALUE ...
1051 Accepts the option I<search>, the string to search for. The string will
1052 be searched for as a username, email address, IP address, MAC address,
1053 phone number, and hardware serial number. Unlike the I<smart_search> on
1054 customers, this always requires an exact match.
1058 # though perhaps it should be fuzzy in some cases?
1061 my %param = __PACKAGE__->smart_search_param(@_);
1065 sub smart_search_param {
1069 my $string = $opt{'search'};
1070 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1073 map { my $table = $_;
1074 my $search_sql = "FS::$table"->search_sql($string);
1076 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1077 "FROM $table WHERE $search_sql";
1079 FS::part_svc->svc_tables;
1081 if ( $string =~ /^(\d+)$/ ) {
1082 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1085 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1086 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1090 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1091 'null_right' => 'View/link unlinked services'
1093 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1095 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1096 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1097 ' LEFT JOIN part_svc USING ( svcpart )';
1100 'table' => 'cust_svc',
1101 'select' => 'svc_all.svcnum AS svcnum, '.
1102 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1104 'addl_from' => $addl_from,
1106 'extra_sql' => $extra_sql,
1110 # If the associated cust_pkg is 'on hold'
1111 # and the associated pkg_svc has the provision_hold flag
1112 # and there are no more available_part_svcs on the cust_pkg similarly flagged,
1113 # then removes hold from pkg
1114 # returns $error or '' on success,
1115 # does not indicate if pkg status was changed
1116 sub _check_provision_hold {
1119 # check status of cust_pkg
1120 my $cust_pkg = $self->cust_pkg;
1121 return '' unless $cust_pkg->status eq 'on hold';
1123 # check flag on this svc
1124 # small false laziness with $self->pkg_svc
1125 # to avoid looking up cust_pkg twice
1126 my $pkg_svc = qsearchs( 'pkg_svc', {
1127 'svcpart' => $self->svcpart,
1128 'pkgpart' => $cust_pkg->pkgpart,
1130 return '' unless $pkg_svc->provision_hold;
1132 # check for any others available with that flag
1133 return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 );
1135 # conditions met, remove hold
1136 return $cust_pkg->unsuspend;
1142 # fix missing (deleted by mistake) svc_x records
1143 warn "searching for missing svc_x records...\n";
1145 'table' => 'cust_svc',
1146 'select' => 'cust_svc.*',
1147 'addl_from' => ' LEFT JOIN ( ' .
1149 map { "SELECT svcnum FROM $_" }
1150 FS::part_svc->svc_tables
1151 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1152 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1154 my @svcs = qsearch(\%search);
1155 warn "found ".scalar(@svcs)."\n";
1157 local $FS::Record::nowarn_classload = 1; # for h_svc_
1158 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1161 'hashref' => { history_action => 'delete' },
1162 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1164 foreach my $cust_svc (@svcs) {
1165 my $svcnum = $cust_svc->svcnum;
1166 my $svcdb = $cust_svc->part_svc->svcdb;
1167 $h_search{'hashref'}{'svcnum'} = $svcnum;
1168 $h_search{'table'} = "h_$svcdb";
1169 my $h_svc_x = qsearchs(\%h_search)
1171 my $class = "FS::$svcdb";
1172 my $new_svc_x = $class->new({ $h_svc_x->hash });
1173 my $error = $new_svc_x->insert;
1174 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1185 Behaviour of changing the svcpart of cust_svc records is undefined and should
1186 possibly be prohibited, and pkg_svc records are not checked.
1188 pkg_svc records are not checked in general (here).
1190 Deleting this record doesn't check or delete the svc_* record associated
1193 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1194 a DBI database handle is not yet implemented.
1198 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1199 schema.html from the base documentation