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')
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.
107 Deletes this service from the database. If there is an error, returns the
108 error, otherwise returns false. Note that this only removes the cust_svc
109 record - you should probably use the B<cancel> method instead.
118 my $cust_pkg = $self->cust_pkg;
119 my $custnum = $cust_pkg->custnum if $cust_pkg;
121 my $error = $self->SUPER::delete;
122 return $error if $error;
124 if ( $ticket_system eq 'RT_Internal' ) {
125 unless ( $rt_session ) {
126 FS::TicketSystem->init;
127 $rt_session = FS::TicketSystem->session;
129 my $links = RT::Links->new($rt_session->{CurrentUser});
130 my $svcnum = $self->svcnum;
131 $links->Limit(FIELD => 'Target',
132 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
133 while ( my $l = $links->Next ) {
136 # re-link to point to the customer instead
138 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
141 ($val, $msg) = $l->Delete;
143 # can't do anything useful on error
144 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
151 Cancels the relevant service by calling the B<cancel> method of the associated
152 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
153 deleting the FS::svc_XXX record and then deleting this record.
155 If there is an error, returns the error, otherwise returns false.
162 local $SIG{HUP} = 'IGNORE';
163 local $SIG{INT} = 'IGNORE';
164 local $SIG{QUIT} = 'IGNORE';
165 local $SIG{TERM} = 'IGNORE';
166 local $SIG{TSTP} = 'IGNORE';
167 local $SIG{PIPE} = 'IGNORE';
169 my $oldAutoCommit = $FS::UID::AutoCommit;
170 local $FS::UID::AutoCommit = 0;
173 my $part_svc = $self->part_svc;
175 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
176 $dbh->rollback if $oldAutoCommit;
177 return "Illegal svcdb value in part_svc!";
180 require "FS/$svcdb.pm";
182 my $svc = $self->svc_x;
184 if ( %opt && $opt{'date'} ) {
185 my $error = $svc->expire($opt{'date'});
187 $dbh->rollback if $oldAutoCommit;
188 return "Error expiring service: $error";
191 my $error = $svc->cancel;
193 $dbh->rollback if $oldAutoCommit;
194 return "Error canceling service: $error";
196 $error = $svc->delete; #this deletes this cust_svc record as well
198 $dbh->rollback if $oldAutoCommit;
199 return "Error deleting service: $error";
206 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
207 "; deleting cust_svc only\n";
209 my $error = $self->delete;
211 $dbh->rollback if $oldAutoCommit;
212 return "Error deleting cust_svc: $error";
217 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
223 =item overlimit [ ACTION ]
225 Retrieves or sets the overlimit date. If ACTION is absent, return
226 the present value of overlimit. If ACTION is present, it can
227 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
228 is set to the current time if it is not already set. The 'unsuspend' value
229 causes the time to be cleared.
231 If there is an error on setting, returns the error, otherwise returns false.
237 my $action = shift or return $self->getfield('overlimit');
239 local $SIG{HUP} = 'IGNORE';
240 local $SIG{INT} = 'IGNORE';
241 local $SIG{QUIT} = 'IGNORE';
242 local $SIG{TERM} = 'IGNORE';
243 local $SIG{TSTP} = 'IGNORE';
244 local $SIG{PIPE} = 'IGNORE';
246 my $oldAutoCommit = $FS::UID::AutoCommit;
247 local $FS::UID::AutoCommit = 0;
250 if ( $action eq 'suspend' ) {
251 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
252 }elsif ( $action eq 'unsuspend' ) {
253 $self->setfield('overlimit', '');
255 die "unexpected action value: $action";
258 local $ignore_quantity = 1;
259 my $error = $self->replace;
261 $dbh->rollback if $oldAutoCommit;
262 return "Error setting overlimit: $error";
265 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
271 =item replace OLD_RECORD
273 Replaces the OLD_RECORD with this one in the database. If there is an error,
274 returns the error, otherwise returns false.
281 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
283 # : $new->replace_old;
284 my ( $new, $old ) = ( shift, shift );
285 $old = $new->replace_old unless defined($old);
287 local $SIG{HUP} = 'IGNORE';
288 local $SIG{INT} = 'IGNORE';
289 local $SIG{QUIT} = 'IGNORE';
290 local $SIG{TERM} = 'IGNORE';
291 local $SIG{TSTP} = 'IGNORE';
292 local $SIG{PIPE} = 'IGNORE';
294 my $oldAutoCommit = $FS::UID::AutoCommit;
295 local $FS::UID::AutoCommit = 0;
298 if ( $new->svcpart != $old->svcpart ) {
299 my $svc_x = $new->svc_x;
300 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
301 local($FS::Record::nowarn_identical) = 1;
302 my $error = $new_svc_x->replace($svc_x);
304 $dbh->rollback if $oldAutoCommit;
305 return $error if $error;
309 # #trigger a re-export on pkgnum changes?
310 # # (of prepaid packages), for Expiration RADIUS attribute
311 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
312 # my $svc_x = $new->svc_x;
313 # local($FS::Record::nowarn_identical) = 1;
314 # my $error = $svc_x->export('replace');
316 # $dbh->rollback if $oldAutoCommit;
317 # return $error if $error;
321 #trigger a pkg_change export on pkgnum changes
322 if ( $new->pkgnum != $old->pkgnum ) {
323 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
328 $dbh->rollback if $oldAutoCommit;
329 return $error if $error;
331 } # if pkgnum is changing
333 #my $error = $new->SUPER::replace($old, @_);
334 my $error = $new->SUPER::replace($old);
336 #trigger a relocate export on location changes
337 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
338 my $svc_x = $new->svc_x;
339 if ( $svc_x->locationnum ) {
340 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
341 # in this case, set the service location to be the same as the new
343 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
344 # and replace it, which triggers a relocate export so we don't
346 $error ||= $svc_x->replace;
348 # the service already has a different location from its package
352 # the service doesn't have a locationnum (either isn't of a type
353 # that has the locationnum field, or the locationnum is null and
354 # defaults to cust_pkg->locationnum)
355 # so just trigger the export here
356 $error ||= $new->svc_x->export('relocate',
357 $new->cust_pkg->cust_location,
358 $old->cust_pkg->cust_location,
360 } # if ($svc_x->locationnum)
361 } # if this is a location change
364 $dbh->rollback if $oldAutoCommit;
365 return $error if $error
368 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
375 Checks all fields to make sure this is a valid service. If there is an error,
376 returns the error, otherwise returns false. Called by the insert and
385 $self->ut_numbern('svcnum')
386 || $self->ut_numbern('pkgnum')
387 || $self->ut_number('svcpart')
388 || $self->ut_numbern('agent_svcid')
389 || $self->ut_numbern('overlimit')
391 return $error if $error;
393 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
394 return "Unknown svcpart" unless $part_svc;
396 if ( $self->pkgnum && ! $ignore_quantity ) {
398 #slightly inefficient since ->pkg_svc will also look it up, but fixing
399 # a much larger perf problem and have bigger fish to fry
400 my $cust_pkg = $self->cust_pkg;
402 my $pkg_svc = $self->pkg_svc
403 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
404 'pkgpart' => $cust_pkg->pkgpart,
408 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
409 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
410 my $addon_pkg_svc = qsearchs('pkg_svc', {
411 pkgpart => $part_pkg_link->dst_pkgpart,
412 svcpart => $self->svcpart,
414 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
418 #better error message? UI shouldn't get here
419 return "No svcpart ". $self->svcpart.
420 " services in pkgpart ". $cust_pkg->pkgpart
421 unless $pkg_svc->quantity > 0;
423 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
425 #false laziness w/cust_pkg->part_svc
426 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
430 #better error message? again, UI shouldn't get here
431 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
432 " services for pkgnum ". $self->pkgnum
435 #part_svc_link rules (only make sense in pkgpart context, and
436 # skipping this when ignore_quantity is set DTRT when we're "forcing"
437 # an implicit change here (location change triggered pkgpart change,
438 # ->overlimit, bulk customer service changes)
439 foreach my $part_svc_link ( $self->part_svc_link(
440 link_type => 'cust_svc_provision_restrict',
443 return $part_svc_link->dst_svc. ' must be provisioned before '.
444 $part_svc_link->src_svc
446 'table' => 'cust_svc',
447 'hashref' => { 'pkgnum' => $self->pkgnum,
448 'svcpart' => $part_svc_link->dst_svcpart,
450 'order_by' => 'LIMIT 1',
459 =item check_part_svc_link_unprovision
461 Checks service dependency unprovision rules for this service.
463 If there is an error, returns the error, otherwise returns false.
467 sub check_part_svc_link_unprovision {
470 foreach my $part_svc_link ( $self->part_svc_link(
471 link_type => 'cust_svc_unprovision_restrict',
474 return $part_svc_link->dst_svc. ' must be unprovisioned before '.
475 $part_svc_link->src_svc
477 'table' => 'cust_svc',
478 'hashref' => { 'pkgnum' => $self->pkgnum,
479 'svcpart' => $part_svc_link->dst_svcpart,
481 'order_by' => 'LIMIT 1',
490 Returns the service dependencies (see L<FS::part_svc_link>) for the given
491 search options, taking into account this service definition as source and
492 this customer's agent.
494 Available options are any field in part_svc_link. Typically used options are
501 my $agentnum = $self->pkgnum ? $self->cust_pkg->cust_main->agentnum : '';
502 FS::part_svc_link->by_agentnum($agentnum, src_svcpart=>$self->svcpart, @_);
507 Returns the displayed service number for this service: agent_svcid if it has a
508 value, svcnum otherwise
514 $self->agent_svcid || $self->svcnum;
519 Returns the definition for this service, as a FS::part_svc object (see
527 ? $self->{'_svcpart'}
528 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
533 Returns the package this service belongs to, as a FS::cust_pkg object (see
538 Returns the pkg_svc record for for this service, if applicable.
544 my $cust_pkg = $self->cust_pkg;
545 return undef unless $cust_pkg;
547 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
548 'pkgpart' => $cust_pkg->pkgpart,
555 Returns the date this service was inserted.
561 $self->h_date('insert');
564 =item pkg_cancel_date
566 Returns the date this service's package was canceled. This normally only
567 exists for a service that's been preserved through cancellation with the
568 part_pkg.preserve flag.
572 sub pkg_cancel_date {
574 my $cust_pkg = $self->cust_pkg or return;
575 return $cust_pkg->getfield('cancel') || '';
580 Returns a list consisting of:
581 - The name of this service (from part_svc)
582 - A meaningful identifier (username, domain, or mail alias)
583 - The table name (i.e. svc_domain) for this service
588 my($label, $value, $svcdb) = $cust_svc->label;
592 Like the B<label> method, except the second item in the list ("meaningful
593 identifier") may be longer - typically, a full name is included.
597 sub label { shift->_label('svc_label', @_); }
598 sub label_long { shift->_label('svc_label_long', @_); }
603 my $svc_x = $self->svc_x
604 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
606 $self->$method($svc_x);
609 sub svc_label { shift->_svc_label('label', @_); }
610 sub svc_label_long { shift->_svc_label('label_long', @_); }
613 my( $self, $method, $svc_x ) = ( shift, shift, shift );
616 $self->part_svc->svc,
618 $self->part_svc->svcdb,
626 Returns a listref of html elements associated with this service's exports.
632 my $svc_x = $self->svc_x
633 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
635 $svc_x->export_links;
638 =item export_getsettings
640 Returns two hashrefs of settings associated with this service's exports.
644 sub export_getsettings {
646 my $svc_x = $self->svc_x
647 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
649 $svc_x->export_getsettings;
655 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
656 FS::svc_domain object, etc.)
662 my $svcdb = $self->part_svc->svcdb;
663 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
664 $self->{'_svc_acct'};
666 require "FS/$svcdb.pm";
667 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
668 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
670 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
674 =item seconds_since TIMESTAMP
676 See L<FS::svc_acct/seconds_since>. Equivalent to
677 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
678 where B<svcdb> is not "svc_acct".
682 #internal session db deprecated (or at least on hold)
683 sub seconds_since { 'internal session db deprecated'; };
684 ##note: implementation here, POD in FS::svc_acct
686 # my($self, $since) = @_;
688 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
691 # AND logout IS NOT NULL'
692 # ) or die $dbh->errstr;
693 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
694 # $sth->fetchrow_arrayref->[0];
697 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
699 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
700 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
701 for records where B<svcdb> is not "svc_acct".
705 #note: implementation here, POD in FS::svc_acct
706 sub seconds_since_sqlradacct {
707 my($self, $start, $end) = @_;
709 my $mes = "$me seconds_since_sqlradacct:";
711 my $svc_x = $self->svc_x;
713 my @part_export = $self->part_svc->part_export_usage;
714 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
715 " service definition"
720 foreach my $part_export ( @part_export ) {
722 next if $part_export->option('ignore_accounting');
724 warn "$mes connecting to sqlradius database\n"
727 my $dbh = DBI->connect( map { $part_export->option($_) }
728 qw(datasrc username password) )
729 or die "can't connect to sqlradius database: ". $DBI::errstr;
731 warn "$mes connected to sqlradius database\n"
734 #select a unix time conversion function based on database type
735 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
736 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
738 my $username = $part_export->export_username($svc_x);
742 warn "$mes finding closed sessions completely within the given range\n"
747 if ($part_export->option('process_single_realm')) {
748 $realm = 'AND Realm = ?';
749 $realmparam = $part_export->option('realm');
752 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
756 AND $str2time AcctStartTime $closing >= ?
757 AND $str2time AcctStopTime $closing < ?
758 AND $str2time AcctStopTime $closing > 0
759 AND AcctStopTime IS NOT NULL"
760 ) or die $dbh->errstr;
761 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
763 my $regular = $sth->fetchrow_arrayref->[0];
765 warn "$mes finding open sessions which start in the range\n"
768 # count session start->range end
769 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
773 AND $str2time AcctStartTime $closing >= ?
774 AND $str2time AcctStartTime $closing < ?
775 AND ( ? - $str2time AcctStartTime $closing ) < 86400
776 AND ( $str2time AcctStopTime $closing = 0
777 OR AcctStopTime IS NULL )";
778 $sth = $dbh->prepare($query) or die $dbh->errstr;
781 ($realm ? $realmparam : ()),
785 or die $sth->errstr. " executing query $query";
786 my $start_during = $sth->fetchrow_arrayref->[0];
788 warn "$mes finding closed sessions which start before the range but stop during\n"
791 #count range start->session end
792 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
796 AND $str2time AcctStartTime $closing < ?
797 AND $str2time AcctStopTime $closing >= ?
798 AND $str2time AcctStopTime $closing < ?
799 AND $str2time AcctStopTime $closing > 0
800 AND AcctStopTime IS NOT NULL"
801 ) or die $dbh->errstr;
802 $sth->execute( $start,
804 ($realm ? $realmparam : ()),
809 my $end_during = $sth->fetchrow_arrayref->[0];
811 warn "$mes finding closed sessions which start before the range but stop after\n"
814 # count range start->range end
815 # don't count open sessions anymore (probably missing stop record)
816 $sth = $dbh->prepare("SELECT COUNT(*)
820 AND $str2time AcctStartTime $closing < ?
821 AND ( $str2time AcctStopTime $closing >= ?
823 # OR AcctStopTime = 0
824 # OR AcctStopTime IS NULL )"
825 ) or die $dbh->errstr;
826 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
828 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
830 $seconds += $regular + $end_during + $start_during + $entire_range;
832 warn "$mes done finding sessions\n"
841 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
843 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
844 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
845 for records where B<svcdb> is not "svc_acct".
849 #note: implementation here, POD in FS::svc_acct
850 #(false laziness w/seconds_since_sqlradacct above)
851 sub attribute_since_sqlradacct {
852 my($self, $start, $end, $attrib) = @_;
854 my $mes = "$me attribute_since_sqlradacct:";
856 my $svc_x = $self->svc_x;
858 my @part_export = $self->part_svc->part_export_usage;
859 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
860 " service definition"
866 foreach my $part_export ( @part_export ) {
868 next if $part_export->option('ignore_accounting');
870 warn "$mes connecting to sqlradius database\n"
873 my $dbh = DBI->connect( map { $part_export->option($_) }
874 qw(datasrc username password) )
875 or die "can't connect to sqlradius database: ". $DBI::errstr;
877 warn "$mes connected to sqlradius database\n"
880 #select a unix time conversion function based on database type
881 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
882 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
884 my $username = $part_export->export_username($svc_x);
886 warn "$mes SUMing $attrib sessions\n"
891 if ($part_export->option('process_single_realm')) {
892 $realm = 'AND Realm = ?';
893 $realmparam = $part_export->option('realm');
896 my $sth = $dbh->prepare("SELECT SUM($attrib)
900 AND $str2time AcctStopTime $closing >= ?
901 AND $str2time AcctStopTime $closing < ?
902 AND AcctStopTime IS NOT NULL"
903 ) or die $dbh->errstr;
904 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
907 my $row = $sth->fetchrow_arrayref;
908 $sum += $row->[0] if defined($row->[0]);
910 warn "$mes done SUMing sessions\n"
919 #note: implementation here, POD in FS::svc_acct
920 # false laziness w/above
921 sub attribute_last_sqlradacct {
922 my($self, $attrib) = @_;
924 my $mes = "$me attribute_last_sqlradacct:";
926 my $svc_x = $self->svc_x;
928 my @part_export = $self->part_svc->part_export_usage;
929 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
930 " service definition"
935 my $AcctStartTime = 0;
937 foreach my $part_export ( @part_export ) {
939 next if $part_export->option('ignore_accounting');
941 warn "$mes connecting to sqlradius database\n"
944 my $dbh = DBI->connect( map { $part_export->option($_) }
945 qw(datasrc username password) )
946 or die "can't connect to sqlradius database: ". $DBI::errstr;
948 warn "$mes connected to sqlradius database\n"
951 #select a unix time conversion function based on database type
952 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
953 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
955 my $username = $part_export->export_username($svc_x);
957 warn "$mes finding most-recent $attrib\n"
962 if ($part_export->option('process_single_realm')) {
963 $realm = 'AND Realm = ?';
964 $realmparam = $part_export->option('realm');
967 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
971 ORDER BY AcctStartTime DESC LIMIT 1
972 ") or die $dbh->errstr;
973 $sth->execute($username, ($realm ? $realmparam : ()) )
976 my $row = $sth->fetchrow_arrayref;
977 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
979 $AcctStartTime = $row->[1];
991 =item get_session_history TIMESTAMP_START TIMESTAMP_END
993 See L<FS::svc_acct/get_session_history>. Equivalent to
994 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
995 records where B<svcdb> is not "svc_acct".
999 sub get_session_history {
1000 my($self, $start, $end, $attrib) = @_;
1004 my @part_export = $self->part_svc->part_export_usage;
1005 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
1006 " service definition"
1007 unless @part_export;
1012 foreach my $part_export ( @part_export ) {
1014 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
1021 =item tickets [ STATUS ]
1023 Returns an array of hashes representing the tickets linked to this service.
1025 An optional status (or arrayref or hashref of statuses) may be specified.
1031 my $status = ( @_ && $_[0] ) ? shift : '';
1033 my $conf = FS::Conf->new;
1034 my $num = $conf->config('cust_main-max_tickets') || 10;
1037 if ( $conf->config('ticket_system') ) {
1038 unless ( $conf->config('ticket_system-custom_priority_field') ) {
1040 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
1049 foreach my $priority (
1050 $conf->config('ticket_system-custom_priority_field-values'), ''
1052 last if scalar(@tickets) >= $num;
1054 @{ FS::TicketSystem->service_tickets( $self->svcnum,
1055 $num - scalar(@tickets),
1068 my $svc_x = $self->svc_x;
1069 +{ ( map { $_=>$self->$_ } $self->fields ),
1070 ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
1080 =item smart_search OPTION => VALUE ...
1082 Accepts the option I<search>, the string to search for. The string will
1083 be searched for as a username, email address, IP address, MAC address,
1084 phone number, and hardware serial number. Unlike the I<smart_search> on
1085 customers, this always requires an exact match.
1089 # though perhaps it should be fuzzy in some cases?
1092 my %param = __PACKAGE__->smart_search_param(@_);
1096 sub smart_search_param {
1100 my $string = $opt{'search'};
1101 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1104 map { my $table = $_;
1105 my $search_sql = "FS::$table"->search_sql($string);
1107 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1108 "FROM $table WHERE $search_sql";
1110 FS::part_svc->svc_tables;
1112 if ( $string =~ /^(\d+)$/ ) {
1113 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1116 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1117 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1121 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1122 'null_right' => 'View/link unlinked services'
1124 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1126 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1127 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1128 ' LEFT JOIN part_svc USING ( svcpart )';
1131 'table' => 'cust_svc',
1132 'select' => 'svc_all.svcnum AS svcnum, '.
1133 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1135 'addl_from' => $addl_from,
1137 'extra_sql' => $extra_sql,
1144 # fix missing (deleted by mistake) svc_x records
1145 warn "searching for missing svc_x records...\n";
1147 'table' => 'cust_svc',
1148 'select' => 'cust_svc.*',
1149 'addl_from' => ' LEFT JOIN ( ' .
1151 map { "SELECT svcnum FROM $_" }
1152 FS::part_svc->svc_tables
1153 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1154 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1156 my @svcs = qsearch(\%search);
1157 warn "found ".scalar(@svcs)."\n";
1159 local $FS::Record::nowarn_classload = 1; # for h_svc_
1160 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1163 'hashref' => { history_action => 'delete' },
1164 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1166 foreach my $cust_svc (@svcs) {
1167 my $svcnum = $cust_svc->svcnum;
1168 my $svcdb = $cust_svc->part_svc->svcdb;
1169 $h_search{'hashref'}{'svcnum'} = $svcnum;
1170 $h_search{'table'} = "h_$svcdb";
1171 my $h_svc_x = qsearchs(\%h_search)
1173 my $class = "FS::$svcdb";
1174 my $new_svc_x = $class->new({ $h_svc_x->hash });
1175 my $error = $new_svc_x->insert;
1176 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1187 Behaviour of changing the svcpart of cust_svc records is undefined and should
1188 possibly be prohibited, and pkg_svc records are not checked.
1190 pkg_svc records are not checked in general (here).
1192 Deleting this record doesn't check or delete the svc_* record associated
1195 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1196 a DBI database handle is not yet implemented.
1200 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1201 schema.html from the base documentation