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 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 # delete associated export_cust_svc
173 foreach my $export_cust_svc (
174 qsearch('export_cust_svc',{ 'svcnum' => $self->svcnum })
176 my $error = $export_cust_svc->delete;
178 $dbh->rollback if $oldAutoCommit;
183 my $error = $self->SUPER::delete;
185 $dbh->rollback if $oldAutoCommit;
189 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
191 if ( $ticket_system eq 'RT_Internal' ) {
192 unless ( $rt_session ) {
193 FS::TicketSystem->init;
194 $rt_session = FS::TicketSystem->session;
196 my $links = RT::Links->new($rt_session->{CurrentUser});
197 my $svcnum = $self->svcnum;
198 $links->Limit(FIELD => 'Target',
199 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
200 while ( my $l = $links->Next ) {
203 # re-link to point to the customer instead
205 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
208 ($val, $msg) = $l->Delete;
210 # can't do anything useful on error
211 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
218 Cancels the relevant service by calling the B<cancel> method of the associated
219 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
220 deleting the FS::svc_XXX record and then deleting this record.
222 If there is an error, returns the error, otherwise returns false.
229 local $SIG{HUP} = 'IGNORE';
230 local $SIG{INT} = 'IGNORE';
231 local $SIG{QUIT} = 'IGNORE';
232 local $SIG{TERM} = 'IGNORE';
233 local $SIG{TSTP} = 'IGNORE';
234 local $SIG{PIPE} = 'IGNORE';
236 my $oldAutoCommit = $FS::UID::AutoCommit;
237 local $FS::UID::AutoCommit = 0;
240 my $part_svc = $self->part_svc;
242 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
243 $dbh->rollback if $oldAutoCommit;
244 return "Illegal svcdb value in part_svc!";
247 require "FS/$svcdb.pm";
249 my $svc = $self->svc_x;
251 if ( %opt && $opt{'date'} ) {
252 my $error = $svc->expire($opt{'date'});
254 $dbh->rollback if $oldAutoCommit;
255 return "Error expiring service: $error";
258 my $error = $svc->cancel;
260 $dbh->rollback if $oldAutoCommit;
261 return "Error canceling service: $error";
263 $error = $svc->delete; #this deletes this cust_svc record as well
265 $dbh->rollback if $oldAutoCommit;
266 return "Error deleting service: $error";
273 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
274 "; deleting cust_svc only\n";
276 my $error = $self->delete;
278 $dbh->rollback if $oldAutoCommit;
279 return "Error deleting cust_svc: $error";
284 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
290 =item overlimit [ ACTION ]
292 Retrieves or sets the overlimit date. If ACTION is absent, return
293 the present value of overlimit. If ACTION is present, it can
294 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
295 is set to the current time if it is not already set. The 'unsuspend' value
296 causes the time to be cleared.
298 If there is an error on setting, returns the error, otherwise returns false.
304 my $action = shift or return $self->getfield('overlimit');
306 local $SIG{HUP} = 'IGNORE';
307 local $SIG{INT} = 'IGNORE';
308 local $SIG{QUIT} = 'IGNORE';
309 local $SIG{TERM} = 'IGNORE';
310 local $SIG{TSTP} = 'IGNORE';
311 local $SIG{PIPE} = 'IGNORE';
313 my $oldAutoCommit = $FS::UID::AutoCommit;
314 local $FS::UID::AutoCommit = 0;
317 if ( $action eq 'suspend' ) {
318 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
319 }elsif ( $action eq 'unsuspend' ) {
320 $self->setfield('overlimit', '');
322 die "unexpected action value: $action";
325 local $ignore_quantity = 1;
326 my $error = $self->replace;
328 $dbh->rollback if $oldAutoCommit;
329 return "Error setting overlimit: $error";
332 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
338 =item replace OLD_RECORD
340 Replaces the OLD_RECORD with this one in the database. If there is an error,
341 returns the error, otherwise returns false.
348 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
350 # : $new->replace_old;
351 my ( $new, $old ) = ( shift, shift );
352 $old = $new->replace_old unless defined($old);
354 local $SIG{HUP} = 'IGNORE';
355 local $SIG{INT} = 'IGNORE';
356 local $SIG{QUIT} = 'IGNORE';
357 local $SIG{TERM} = 'IGNORE';
358 local $SIG{TSTP} = 'IGNORE';
359 local $SIG{PIPE} = 'IGNORE';
361 my $oldAutoCommit = $FS::UID::AutoCommit;
362 local $FS::UID::AutoCommit = 0;
365 if ( $new->svcpart != $old->svcpart ) {
366 my $svc_x = $new->svc_x;
367 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
368 local($FS::Record::nowarn_identical) = 1;
369 my $error = $new_svc_x->replace($svc_x);
371 $dbh->rollback if $oldAutoCommit;
372 return $error if $error;
376 # #trigger a re-export on pkgnum changes?
377 # # (of prepaid packages), for Expiration RADIUS attribute
378 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
379 # my $svc_x = $new->svc_x;
380 # local($FS::Record::nowarn_identical) = 1;
381 # my $error = $svc_x->export('replace');
383 # $dbh->rollback if $oldAutoCommit;
384 # return $error if $error;
388 #trigger a pkg_change export on pkgnum changes
389 if ( $new->pkgnum != $old->pkgnum ) {
390 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
395 $dbh->rollback if $oldAutoCommit;
396 return $error if $error;
398 } # if pkgnum is changing
400 #my $error = $new->SUPER::replace($old, @_);
401 my $error = $new->SUPER::replace($old);
403 #trigger a relocate export on location changes
404 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
405 my $svc_x = $new->svc_x;
406 if ( $svc_x->locationnum ) {
407 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
408 # in this case, set the service location to be the same as the new
410 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
411 # and replace it, which triggers a relocate export so we don't
413 $error ||= $svc_x->replace;
415 # the service already has a different location from its package
419 # the service doesn't have a locationnum (either isn't of a type
420 # that has the locationnum field, or the locationnum is null and
421 # defaults to cust_pkg->locationnum)
422 # so just trigger the export here
423 $error ||= $new->svc_x->export('relocate',
424 $new->cust_pkg->cust_location,
425 $old->cust_pkg->cust_location,
427 } # if ($svc_x->locationnum)
428 } # if this is a location change
430 #check if this releases a hold (see FS::pkg_svc provision_hold)
431 $error ||= $new->_check_provision_hold;
434 $dbh->rollback if $oldAutoCommit;
435 return $error if $error
438 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
445 Checks all fields to make sure this is a valid service. If there is an error,
446 returns the error, otherwise returns false. Called by the insert and
455 $self->ut_numbern('svcnum')
456 || $self->ut_numbern('pkgnum')
457 || $self->ut_number('svcpart')
458 || $self->ut_numbern('agent_svcid')
459 || $self->ut_numbern('overlimit')
461 return $error if $error;
463 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
464 return "Unknown svcpart" unless $part_svc;
466 if ( $self->pkgnum && ! $ignore_quantity ) {
468 #slightly inefficient since ->pkg_svc will also look it up, but fixing
469 # a much larger perf problem and have bigger fish to fry
470 my $cust_pkg = $self->cust_pkg;
472 my $pkg_svc = $self->pkg_svc
473 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
474 'pkgpart' => $cust_pkg->pkgpart,
478 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
479 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
480 my $addon_pkg_svc = qsearchs('pkg_svc', {
481 pkgpart => $part_pkg_link->dst_pkgpart,
482 svcpart => $self->svcpart,
484 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
488 #better error message? UI shouldn't get here
489 return "No svcpart ". $self->svcpart.
490 " services in pkgpart ". $cust_pkg->pkgpart
491 unless $pkg_svc->quantity > 0;
493 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
495 #false laziness w/cust_pkg->part_svc
496 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
500 #better error message? again, UI shouldn't get here
501 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
502 " services for pkgnum ". $self->pkgnum
512 Returns the displayed service number for this service: agent_svcid if it has a
513 value, svcnum otherwise
519 $self->agent_svcid || $self->svcnum;
524 Returns the definition for this service, as a FS::part_svc object (see
531 return $self->{_svcpart} if $self->{_svcpart};
532 cluck 'cust_svc->part_svc called' if $DEBUG;
533 qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
538 Returns the package this service belongs to, as a FS::cust_pkg object (see
545 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
550 Returns the pkg_svc record for for this service, if applicable.
556 my $cust_pkg = $self->cust_pkg;
557 return undef unless $cust_pkg;
559 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
560 'pkgpart' => $cust_pkg->pkgpart,
567 Returns the date this service was inserted.
573 $self->h_date('insert');
576 =item pkg_cancel_date
578 Returns the date this service's package was canceled. This normally only
579 exists for a service that's been preserved through cancellation with the
580 part_pkg.preserve flag.
584 sub pkg_cancel_date {
586 my $cust_pkg = $self->cust_pkg or return;
587 return $cust_pkg->getfield('cancel') || '';
592 Returns a list consisting of:
593 - The name of this service (from part_svc)
594 - A meaningful identifier (username, domain, or mail alias)
595 - The table name (i.e. svc_domain) for this service
600 my($label, $value, $svcdb) = $cust_svc->label;
604 Like the B<label> method, except the second item in the list ("meaningful
605 identifier") may be longer - typically, a full name is included.
609 sub label { shift->_label('svc_label', @_); }
610 sub label_long { shift->_label('svc_label_long', @_); }
615 my $svc_x = $self->svc_x
616 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
618 $self->$method($svc_x);
621 sub svc_label { shift->_svc_label('label', @_); }
622 sub svc_label_long { shift->_svc_label('label_long', @_); }
625 my( $self, $method, $svc_x ) = ( shift, shift, shift );
628 $self->part_svc->svc,
630 $self->part_svc->svcdb,
638 Returns a listref of html elements associated with this service's exports.
644 my $svc_x = $self->svc_x
645 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
647 $svc_x->export_links;
650 =item export_getsettings
652 Returns two hashrefs of settings associated with this service's exports.
656 sub export_getsettings {
658 my $svc_x = $self->svc_x
659 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
661 $svc_x->export_getsettings;
667 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
668 FS::svc_domain object, etc.)
674 my $svcdb = $self->part_svc->svcdb;
675 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
676 $self->{'_svc_acct'};
678 require "FS/$svcdb.pm";
679 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
680 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
682 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
686 =item seconds_since TIMESTAMP
688 See L<FS::svc_acct/seconds_since>. Equivalent to
689 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
690 where B<svcdb> is not "svc_acct".
694 #internal session db deprecated (or at least on hold)
695 sub seconds_since { 'internal session db deprecated'; };
696 ##note: implementation here, POD in FS::svc_acct
698 # my($self, $since) = @_;
700 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
703 # AND logout IS NOT NULL'
704 # ) or die $dbh->errstr;
705 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
706 # $sth->fetchrow_arrayref->[0];
709 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
711 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
712 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
713 for records where B<svcdb> is not "svc_acct".
717 #note: implementation here, POD in FS::svc_acct
718 sub seconds_since_sqlradacct {
719 my($self, $start, $end) = @_;
721 my $mes = "$me seconds_since_sqlradacct:";
723 my $svc_x = $self->svc_x;
725 my @part_export = $self->part_svc->part_export_usage;
726 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
727 " service definition"
732 foreach my $part_export ( @part_export ) {
734 next if $part_export->option('ignore_accounting');
736 warn "$mes connecting to sqlradius database\n"
739 my $dbh = DBI->connect( map { $part_export->option($_) }
740 qw(datasrc username password) )
741 or die "can't connect to sqlradius database: ". $DBI::errstr;
743 warn "$mes connected to sqlradius database\n"
746 #select a unix time conversion function based on database type
747 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
748 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
750 my $username = $part_export->export_username($svc_x);
754 warn "$mes finding closed sessions completely within the given range\n"
759 if ($part_export->option('process_single_realm')) {
760 $realm = 'AND Realm = ?';
761 $realmparam = $part_export->option('realm');
764 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
768 AND $str2time AcctStartTime $closing >= ?
769 AND $str2time AcctStopTime $closing < ?
770 AND $str2time AcctStopTime $closing > 0
771 AND AcctStopTime IS NOT NULL"
772 ) or die $dbh->errstr;
773 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
775 my $regular = $sth->fetchrow_arrayref->[0];
777 warn "$mes finding open sessions which start in the range\n"
780 # count session start->range end
781 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
785 AND $str2time AcctStartTime $closing >= ?
786 AND $str2time AcctStartTime $closing < ?
787 AND ( ? - $str2time AcctStartTime $closing ) < 86400
788 AND ( $str2time AcctStopTime $closing = 0
789 OR AcctStopTime IS NULL )";
790 $sth = $dbh->prepare($query) or die $dbh->errstr;
793 ($realm ? $realmparam : ()),
797 or die $sth->errstr. " executing query $query";
798 my $start_during = $sth->fetchrow_arrayref->[0];
800 warn "$mes finding closed sessions which start before the range but stop during\n"
803 #count range start->session end
804 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
808 AND $str2time AcctStartTime $closing < ?
809 AND $str2time AcctStopTime $closing >= ?
810 AND $str2time AcctStopTime $closing < ?
811 AND $str2time AcctStopTime $closing > 0
812 AND AcctStopTime IS NOT NULL"
813 ) or die $dbh->errstr;
814 $sth->execute( $start,
816 ($realm ? $realmparam : ()),
821 my $end_during = $sth->fetchrow_arrayref->[0];
823 warn "$mes finding closed sessions which start before the range but stop after\n"
826 # count range start->range end
827 # don't count open sessions anymore (probably missing stop record)
828 $sth = $dbh->prepare("SELECT COUNT(*)
832 AND $str2time AcctStartTime $closing < ?
833 AND ( $str2time AcctStopTime $closing >= ?
835 # OR AcctStopTime = 0
836 # OR AcctStopTime IS NULL )"
837 ) or die $dbh->errstr;
838 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
840 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
842 $seconds += $regular + $end_during + $start_during + $entire_range;
844 warn "$mes done finding sessions\n"
853 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
855 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
856 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
857 for records where B<svcdb> is not "svc_acct".
861 #note: implementation here, POD in FS::svc_acct
862 #(false laziness w/seconds_since_sqlradacct above)
863 sub attribute_since_sqlradacct {
864 my($self, $start, $end, $attrib) = @_;
866 my $mes = "$me attribute_since_sqlradacct:";
868 my $svc_x = $self->svc_x;
870 my @part_export = $self->part_svc->part_export_usage;
871 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
872 " service definition"
878 foreach my $part_export ( @part_export ) {
880 next if $part_export->option('ignore_accounting');
882 warn "$mes connecting to sqlradius database\n"
885 my $dbh = DBI->connect( map { $part_export->option($_) }
886 qw(datasrc username password) )
887 or die "can't connect to sqlradius database: ". $DBI::errstr;
889 warn "$mes connected to sqlradius database\n"
892 #select a unix time conversion function based on database type
893 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
894 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
896 my $username = $part_export->export_username($svc_x);
898 warn "$mes SUMing $attrib sessions\n"
903 if ($part_export->option('process_single_realm')) {
904 $realm = 'AND Realm = ?';
905 $realmparam = $part_export->option('realm');
908 my $sth = $dbh->prepare("SELECT SUM($attrib)
912 AND $str2time AcctStopTime $closing >= ?
913 AND $str2time AcctStopTime $closing < ?
914 AND AcctStopTime IS NOT NULL"
915 ) or die $dbh->errstr;
916 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
919 my $row = $sth->fetchrow_arrayref;
920 $sum += $row->[0] if defined($row->[0]);
922 warn "$mes done SUMing sessions\n"
931 #note: implementation here, POD in FS::svc_acct
932 # false laziness w/above
933 sub attribute_last_sqlradacct {
934 my($self, $attrib) = @_;
936 my $mes = "$me attribute_last_sqlradacct:";
938 my $svc_x = $self->svc_x;
940 my @part_export = $self->part_svc->part_export_usage;
941 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
942 " service definition"
947 my $AcctStartTime = 0;
949 foreach my $part_export ( @part_export ) {
951 next if $part_export->option('ignore_accounting');
953 warn "$mes connecting to sqlradius database\n"
956 my $dbh = DBI->connect( map { $part_export->option($_) }
957 qw(datasrc username password) )
958 or die "can't connect to sqlradius database: ". $DBI::errstr;
960 warn "$mes connected to sqlradius database\n"
963 #select a unix time conversion function based on database type
964 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
965 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
967 my $username = $part_export->export_username($svc_x);
969 warn "$mes finding most-recent $attrib\n"
974 if ($part_export->option('process_single_realm')) {
975 $realm = 'AND Realm = ?';
976 $realmparam = $part_export->option('realm');
979 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
983 ORDER BY AcctStartTime DESC LIMIT 1
984 ") or die $dbh->errstr;
985 $sth->execute($username, ($realm ? $realmparam : ()) )
988 my $row = $sth->fetchrow_arrayref;
989 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
991 $AcctStartTime = $row->[1];
1003 =item get_session_history TIMESTAMP_START TIMESTAMP_END
1005 See L<FS::svc_acct/get_session_history>. Equivalent to
1006 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
1007 records where B<svcdb> is not "svc_acct".
1011 sub get_session_history {
1012 my($self, $start, $end, $attrib) = @_;
1016 my @part_export = $self->part_svc->part_export_usage;
1017 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
1018 " service definition"
1019 unless @part_export;
1024 foreach my $part_export ( @part_export ) {
1026 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
1033 =item tickets [ STATUS ]
1035 Returns an array of hashes representing the tickets linked to this service.
1037 An optional status (or arrayref or hashref of statuses) may be specified.
1043 my $status = ( @_ && $_[0] ) ? shift : '';
1045 my $conf = FS::Conf->new;
1046 my $num = $conf->config('cust_main-max_tickets') || 10;
1049 if ( $conf->config('ticket_system') ) {
1050 unless ( $conf->config('ticket_system-custom_priority_field') ) {
1052 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
1061 foreach my $priority (
1062 $conf->config('ticket_system-custom_priority_field-values'), ''
1064 last if scalar(@tickets) >= $num;
1066 @{ FS::TicketSystem->service_tickets( $self->svcnum,
1067 $num - scalar(@tickets),
1085 =item smart_search OPTION => VALUE ...
1087 Accepts the option I<search>, the string to search for. The string will
1088 be searched for as a username, email address, IP address, MAC address,
1089 phone number, and hardware serial number. Unlike the I<smart_search> on
1090 customers, this always requires an exact match.
1094 # though perhaps it should be fuzzy in some cases?
1097 my %param = __PACKAGE__->smart_search_param(@_);
1101 sub smart_search_param {
1105 my $string = $opt{'search'};
1106 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1109 map { my $table = $_;
1110 my $search_sql = "FS::$table"->search_sql($string);
1111 my $addl_from = "FS::$table"->search_sql_addl_from();
1113 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1114 "FROM $table $addl_from WHERE $search_sql";
1116 FS::part_svc->svc_tables;
1118 if ( $string =~ /^(\d+)$/ ) {
1119 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1122 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1123 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1127 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1128 'null_right' => 'View/link unlinked services'
1130 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1132 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1133 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1134 ' LEFT JOIN part_svc USING ( svcpart )';
1137 'table' => 'cust_svc',
1138 'select' => 'svc_all.svcnum AS svcnum, '.
1139 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1141 'addl_from' => $addl_from,
1143 'extra_sql' => $extra_sql,
1147 # If the associated cust_pkg is 'on hold'
1148 # and the associated pkg_svc has the provision_hold flag
1149 # and there are no more available_part_svcs on the cust_pkg similarly flagged,
1150 # then removes hold from pkg
1151 # returns $error or '' on success,
1152 # does not indicate if pkg status was changed
1153 sub _check_provision_hold {
1156 # check status of cust_pkg
1157 my $cust_pkg = $self->cust_pkg;
1158 return '' unless $cust_pkg && $cust_pkg->status eq 'on hold';
1160 # check flag on this svc
1161 # small false laziness with $self->pkg_svc
1162 # to avoid looking up cust_pkg twice
1163 my $pkg_svc = qsearchs( 'pkg_svc', {
1164 'svcpart' => $self->svcpart,
1165 'pkgpart' => $cust_pkg->pkgpart,
1167 return '' unless $pkg_svc->provision_hold;
1169 # check for any others available with that flag
1170 return '' if $cust_pkg->available_part_svc( 'provision_hold' => 1 );
1172 # conditions met, remove hold
1173 return $cust_pkg->unsuspend;
1179 # fix missing (deleted by mistake) svc_x records
1180 warn "searching for missing svc_x records...\n";
1182 'table' => 'cust_svc',
1183 'select' => 'cust_svc.*',
1184 'addl_from' => ' LEFT JOIN ( ' .
1186 map { "SELECT svcnum FROM $_" }
1187 FS::part_svc->svc_tables
1188 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1189 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1191 my @svcs = qsearch(\%search);
1192 warn "found ".scalar(@svcs)."\n";
1194 local $FS::Record::nowarn_classload = 1; # for h_svc_
1195 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1198 'hashref' => { history_action => 'delete' },
1199 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1201 foreach my $cust_svc (@svcs) {
1202 my $svcnum = $cust_svc->svcnum;
1203 my $svcdb = $cust_svc->part_svc->svcdb;
1204 $h_search{'hashref'}{'svcnum'} = $svcnum;
1205 $h_search{'table'} = "h_$svcdb";
1206 my $h_svc_x = qsearchs(\%h_search)
1208 my $class = "FS::$svcdb";
1209 my $new_svc_x = $class->new({ $h_svc_x->hash });
1210 my $error = $new_svc_x->insert;
1211 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1222 Behaviour of changing the svcpart of cust_svc records is undefined and should
1223 possibly be prohibited, and pkg_svc records are not checked.
1225 pkg_svc records are not checked in general (here).
1227 Deleting this record doesn't check or delete the svc_* record associated
1230 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1231 a DBI database handle is not yet implemented.
1235 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1236 schema.html from the base documentation