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::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
28 #ask FS::UID to run this stuff for us later
29 FS::UID->install_callback( sub {
31 $ticket_system = $conf->config('ticket_system')
36 my ( $hashref, $cache ) = @_;
37 if ( $hashref->{'username'} ) {
38 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
40 if ( $hashref->{'svc'} ) {
41 $self->{'_svcpart'} = FS::part_svc->new($hashref);
47 FS::cust_svc - Object method for cust_svc objects
53 $record = new FS::cust_svc \%hash
54 $record = new FS::cust_svc { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ($label, $value) = $record->label;
68 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
69 The following fields are currently supported:
73 =item svcnum - primary key (assigned automatically for new services)
75 =item pkgnum - Package (see L<FS::cust_pkg>)
77 =item svcpart - Service definition (see L<FS::part_svc>)
79 =item agent_svcid - Optional legacy service ID
81 =item overlimit - date the service exceeded its usage limit
91 Creates a new service. To add the refund to the database, see L<"insert">.
92 Services are normally created by creating FS::svc_ objects (see
93 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
97 sub table { 'cust_svc'; }
101 Adds this service to the database. If there is an error, returns the error,
102 otherwise returns false.
106 Deletes this service from the database. If there is an error, returns the
107 error, otherwise returns false. Note that this only removes the cust_svc
108 record - you should probably use the B<cancel> method instead.
117 my $cust_pkg = $self->cust_pkg;
118 my $custnum = $cust_pkg->custnum if $cust_pkg;
120 my $error = $self->SUPER::delete;
121 return $error if $error;
123 if ( $ticket_system eq 'RT_Internal' ) {
124 unless ( $rt_session ) {
125 FS::TicketSystem->init;
126 $rt_session = FS::TicketSystem->session;
128 my $links = RT::Links->new($rt_session->{CurrentUser});
129 my $svcnum = $self->svcnum;
130 $links->Limit(FIELD => 'Target',
131 VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
132 while ( my $l = $links->Next ) {
135 # re-link to point to the customer instead
137 $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
140 ($val, $msg) = $l->Delete;
142 # can't do anything useful on error
143 warn "error unlinking ticket $svcnum: $msg\n" if !$val;
150 Cancels the relevant service by calling the B<cancel> method of the associated
151 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
152 deleting the FS::svc_XXX record and then deleting this record.
154 If there is an error, returns the error, otherwise returns false.
161 local $SIG{HUP} = 'IGNORE';
162 local $SIG{INT} = 'IGNORE';
163 local $SIG{QUIT} = 'IGNORE';
164 local $SIG{TERM} = 'IGNORE';
165 local $SIG{TSTP} = 'IGNORE';
166 local $SIG{PIPE} = 'IGNORE';
168 my $oldAutoCommit = $FS::UID::AutoCommit;
169 local $FS::UID::AutoCommit = 0;
172 my $part_svc = $self->part_svc;
174 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
175 $dbh->rollback if $oldAutoCommit;
176 return "Illegal svcdb value in part_svc!";
179 require "FS/$svcdb.pm";
181 my $svc = $self->svc_x;
183 if ( %opt && $opt{'date'} ) {
184 my $error = $svc->expire($opt{'date'});
186 $dbh->rollback if $oldAutoCommit;
187 return "Error expiring service: $error";
190 my $error = $svc->cancel;
192 $dbh->rollback if $oldAutoCommit;
193 return "Error canceling service: $error";
195 $error = $svc->delete; #this deletes this cust_svc record as well
197 $dbh->rollback if $oldAutoCommit;
198 return "Error deleting service: $error";
205 warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
206 "; deleting cust_svc only\n";
208 my $error = $self->delete;
210 $dbh->rollback if $oldAutoCommit;
211 return "Error deleting cust_svc: $error";
216 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
222 =item overlimit [ ACTION ]
224 Retrieves or sets the overlimit date. If ACTION is absent, return
225 the present value of overlimit. If ACTION is present, it can
226 have the value 'suspend' or 'unsuspend'. In the case of 'suspend' overlimit
227 is set to the current time if it is not already set. The 'unsuspend' value
228 causes the time to be cleared.
230 If there is an error on setting, returns the error, otherwise returns false.
236 my $action = shift or return $self->getfield('overlimit');
238 local $SIG{HUP} = 'IGNORE';
239 local $SIG{INT} = 'IGNORE';
240 local $SIG{QUIT} = 'IGNORE';
241 local $SIG{TERM} = 'IGNORE';
242 local $SIG{TSTP} = 'IGNORE';
243 local $SIG{PIPE} = 'IGNORE';
245 my $oldAutoCommit = $FS::UID::AutoCommit;
246 local $FS::UID::AutoCommit = 0;
249 if ( $action eq 'suspend' ) {
250 $self->setfield('overlimit', time) unless $self->getfield('overlimit');
251 }elsif ( $action eq 'unsuspend' ) {
252 $self->setfield('overlimit', '');
254 die "unexpected action value: $action";
257 local $ignore_quantity = 1;
258 my $error = $self->replace;
260 $dbh->rollback if $oldAutoCommit;
261 return "Error setting overlimit: $error";
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 =item replace OLD_RECORD
272 Replaces the OLD_RECORD with this one in the database. If there is an error,
273 returns the error, otherwise returns false.
280 # my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
282 # : $new->replace_old;
283 my ( $new, $old ) = ( shift, shift );
284 $old = $new->replace_old unless defined($old);
286 local $SIG{HUP} = 'IGNORE';
287 local $SIG{INT} = 'IGNORE';
288 local $SIG{QUIT} = 'IGNORE';
289 local $SIG{TERM} = 'IGNORE';
290 local $SIG{TSTP} = 'IGNORE';
291 local $SIG{PIPE} = 'IGNORE';
293 my $oldAutoCommit = $FS::UID::AutoCommit;
294 local $FS::UID::AutoCommit = 0;
297 if ( $new->svcpart != $old->svcpart ) {
298 my $svc_x = $new->svc_x;
299 my $new_svc_x = ref($svc_x)->new({$svc_x->hash, svcpart=>$new->svcpart });
300 local($FS::Record::nowarn_identical) = 1;
301 my $error = $new_svc_x->replace($svc_x);
303 $dbh->rollback if $oldAutoCommit;
304 return $error if $error;
308 # #trigger a re-export on pkgnum changes?
309 # # (of prepaid packages), for Expiration RADIUS attribute
310 # if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
311 # my $svc_x = $new->svc_x;
312 # local($FS::Record::nowarn_identical) = 1;
313 # my $error = $svc_x->export('replace');
315 # $dbh->rollback if $oldAutoCommit;
316 # return $error if $error;
320 #trigger a pkg_change export on pkgnum changes
321 if ( $new->pkgnum != $old->pkgnum ) {
322 my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
327 $dbh->rollback if $oldAutoCommit;
328 return $error if $error;
330 } # if pkgnum is changing
332 #my $error = $new->SUPER::replace($old, @_);
333 my $error = $new->SUPER::replace($old);
335 #trigger a relocate export on location changes
336 if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
337 my $svc_x = $new->svc_x;
338 if ( $svc_x->locationnum ) {
339 if ( $svc_x->locationnum == $old->cust_pkg->locationnum ) {
340 # in this case, set the service location to be the same as the new
342 $svc_x->set('locationnum', $new->cust_pkg->locationnum);
343 # and replace it, which triggers a relocate export so we don't
345 $error ||= $svc_x->replace;
347 # the service already has a different location from its package
351 # the service doesn't have a locationnum (either isn't of a type
352 # that has the locationnum field, or the locationnum is null and
353 # defaults to cust_pkg->locationnum)
354 # so just trigger the export here
355 $error ||= $new->svc_x->export('relocate',
356 $new->cust_pkg->cust_location,
357 $old->cust_pkg->cust_location,
359 } # if ($svc_x->locationnum)
360 } # if this is a location change
363 $dbh->rollback if $oldAutoCommit;
364 return $error if $error
367 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
374 Checks all fields to make sure this is a valid service. If there is an error,
375 returns the error, otherwise returns false. Called by the insert and
384 $self->ut_numbern('svcnum')
385 || $self->ut_numbern('pkgnum')
386 || $self->ut_number('svcpart')
387 || $self->ut_numbern('agent_svcid')
388 || $self->ut_numbern('overlimit')
390 return $error if $error;
392 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
393 return "Unknown svcpart" unless $part_svc;
395 if ( $self->pkgnum && ! $ignore_quantity ) {
397 #slightly inefficient since ->pkg_svc will also look it up, but fixing
398 # a much larger perf problem and have bigger fish to fry
399 my $cust_pkg = $self->cust_pkg;
401 my $pkg_svc = $self->pkg_svc
402 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
403 'pkgpart' => $cust_pkg->pkgpart,
407 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
408 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
409 my $addon_pkg_svc = qsearchs('pkg_svc', {
410 pkgpart => $part_pkg_link->dst_pkgpart,
411 svcpart => $self->svcpart,
413 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
417 #better error message? UI shouldn't get here
418 return "No svcpart ". $self->svcpart.
419 " services in pkgpart ". $cust_pkg->pkgpart
420 unless $pkg_svc->quantity > 0;
422 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
424 #false laziness w/cust_pkg->part_svc
425 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
429 #better error message? again, UI shouldn't get here
430 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
431 " services for pkgnum ". $self->pkgnum
441 Returns the displayed service number for this service: agent_svcid if it has a
442 value, svcnum otherwise
448 $self->agent_svcid || $self->svcnum;
453 Returns the definition for this service, as a FS::part_svc object (see
461 ? $self->{'_svcpart'}
462 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
467 Returns the package this service belongs to, as a FS::cust_pkg object (see
472 Returns the pkg_svc record for for this service, if applicable.
478 my $cust_pkg = $self->cust_pkg;
479 return undef unless $cust_pkg;
481 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
482 'pkgpart' => $cust_pkg->pkgpart,
489 Returns the date this service was inserted.
495 $self->h_date('insert');
498 =item pkg_cancel_date
500 Returns the date this service's package was canceled. This normally only
501 exists for a service that's been preserved through cancellation with the
502 part_pkg.preserve flag.
506 sub pkg_cancel_date {
508 my $cust_pkg = $self->cust_pkg or return;
509 return $cust_pkg->getfield('cancel') || '';
514 Returns a list consisting of:
515 - The name of this service (from part_svc)
516 - A meaningful identifier (username, domain, or mail alias)
517 - The table name (i.e. svc_domain) for this service
522 my($label, $value, $svcdb) = $cust_svc->label;
526 Like the B<label> method, except the second item in the list ("meaningful
527 identifier") may be longer - typically, a full name is included.
531 sub label { shift->_label('svc_label', @_); }
532 sub label_long { shift->_label('svc_label_long', @_); }
537 my $svc_x = $self->svc_x
538 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
540 $self->$method($svc_x);
543 sub svc_label { shift->_svc_label('label', @_); }
544 sub svc_label_long { shift->_svc_label('label_long', @_); }
547 my( $self, $method, $svc_x ) = ( shift, shift, shift );
550 $self->part_svc->svc,
552 $self->part_svc->svcdb,
560 Returns a listref of html elements associated with this service's exports.
566 my $svc_x = $self->svc_x
567 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
569 $svc_x->export_links;
572 =item export_getsettings
574 Returns two hashrefs of settings associated with this service's exports.
578 sub export_getsettings {
580 my $svc_x = $self->svc_x
581 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
583 $svc_x->export_getsettings;
589 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
590 FS::svc_domain object, etc.)
596 my $svcdb = $self->part_svc->svcdb;
597 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
598 $self->{'_svc_acct'};
600 require "FS/$svcdb.pm";
601 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
602 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
604 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
608 =item seconds_since TIMESTAMP
610 See L<FS::svc_acct/seconds_since>. Equivalent to
611 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
612 where B<svcdb> is not "svc_acct".
616 #internal session db deprecated (or at least on hold)
617 sub seconds_since { 'internal session db deprecated'; };
618 ##note: implementation here, POD in FS::svc_acct
620 # my($self, $since) = @_;
622 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
625 # AND logout IS NOT NULL'
626 # ) or die $dbh->errstr;
627 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
628 # $sth->fetchrow_arrayref->[0];
631 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
633 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
634 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
635 for records where B<svcdb> is not "svc_acct".
639 #note: implementation here, POD in FS::svc_acct
640 sub seconds_since_sqlradacct {
641 my($self, $start, $end) = @_;
643 my $mes = "$me seconds_since_sqlradacct:";
645 my $svc_x = $self->svc_x;
647 my @part_export = $self->part_svc->part_export_usage;
648 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
649 " service definition"
654 foreach my $part_export ( @part_export ) {
656 next if $part_export->option('ignore_accounting');
658 warn "$mes connecting to sqlradius database\n"
661 my $dbh = DBI->connect( map { $part_export->option($_) }
662 qw(datasrc username password) )
663 or die "can't connect to sqlradius database: ". $DBI::errstr;
665 warn "$mes connected to sqlradius database\n"
668 #select a unix time conversion function based on database type
669 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
670 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
672 my $username = $part_export->export_username($svc_x);
676 warn "$mes finding closed sessions completely within the given range\n"
681 if ($part_export->option('process_single_realm')) {
682 $realm = 'AND Realm = ?';
683 $realmparam = $part_export->option('realm');
686 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
690 AND $str2time AcctStartTime $closing >= ?
691 AND $str2time AcctStopTime $closing < ?
692 AND $str2time AcctStopTime $closing > 0
693 AND AcctStopTime IS NOT NULL"
694 ) or die $dbh->errstr;
695 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
697 my $regular = $sth->fetchrow_arrayref->[0];
699 warn "$mes finding open sessions which start in the range\n"
702 # count session start->range end
703 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
707 AND $str2time AcctStartTime $closing >= ?
708 AND $str2time AcctStartTime $closing < ?
709 AND ( ? - $str2time AcctStartTime $closing ) < 86400
710 AND ( $str2time AcctStopTime $closing = 0
711 OR AcctStopTime IS NULL )";
712 $sth = $dbh->prepare($query) or die $dbh->errstr;
715 ($realm ? $realmparam : ()),
719 or die $sth->errstr. " executing query $query";
720 my $start_during = $sth->fetchrow_arrayref->[0];
722 warn "$mes finding closed sessions which start before the range but stop during\n"
725 #count range start->session end
726 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
730 AND $str2time AcctStartTime $closing < ?
731 AND $str2time AcctStopTime $closing >= ?
732 AND $str2time AcctStopTime $closing < ?
733 AND $str2time AcctStopTime $closing > 0
734 AND AcctStopTime IS NOT NULL"
735 ) or die $dbh->errstr;
736 $sth->execute( $start,
738 ($realm ? $realmparam : ()),
743 my $end_during = $sth->fetchrow_arrayref->[0];
745 warn "$mes finding closed sessions which start before the range but stop after\n"
748 # count range start->range end
749 # don't count open sessions anymore (probably missing stop record)
750 $sth = $dbh->prepare("SELECT COUNT(*)
754 AND $str2time AcctStartTime $closing < ?
755 AND ( $str2time AcctStopTime $closing >= ?
757 # OR AcctStopTime = 0
758 # OR AcctStopTime IS NULL )"
759 ) or die $dbh->errstr;
760 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
762 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
764 $seconds += $regular + $end_during + $start_during + $entire_range;
766 warn "$mes done finding sessions\n"
775 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
777 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
778 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
779 for records where B<svcdb> is not "svc_acct".
783 #note: implementation here, POD in FS::svc_acct
784 #(false laziness w/seconds_since_sqlradacct above)
785 sub attribute_since_sqlradacct {
786 my($self, $start, $end, $attrib) = @_;
788 my $mes = "$me attribute_since_sqlradacct:";
790 my $svc_x = $self->svc_x;
792 my @part_export = $self->part_svc->part_export_usage;
793 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
794 " service definition"
800 foreach my $part_export ( @part_export ) {
802 next if $part_export->option('ignore_accounting');
804 warn "$mes connecting to sqlradius database\n"
807 my $dbh = DBI->connect( map { $part_export->option($_) }
808 qw(datasrc username password) )
809 or die "can't connect to sqlradius database: ". $DBI::errstr;
811 warn "$mes connected to sqlradius database\n"
814 #select a unix time conversion function based on database type
815 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
816 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
818 my $username = $part_export->export_username($svc_x);
820 warn "$mes SUMing $attrib sessions\n"
825 if ($part_export->option('process_single_realm')) {
826 $realm = 'AND Realm = ?';
827 $realmparam = $part_export->option('realm');
830 my $sth = $dbh->prepare("SELECT SUM($attrib)
834 AND $str2time AcctStopTime $closing >= ?
835 AND $str2time AcctStopTime $closing < ?
836 AND AcctStopTime IS NOT NULL"
837 ) or die $dbh->errstr;
838 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
841 my $row = $sth->fetchrow_arrayref;
842 $sum += $row->[0] if defined($row->[0]);
844 warn "$mes done SUMing sessions\n"
853 #note: implementation here, POD in FS::svc_acct
854 # false laziness w/above
855 sub attribute_last_sqlradacct {
856 my($self, $attrib) = @_;
858 my $mes = "$me attribute_last_sqlradacct:";
860 my $svc_x = $self->svc_x;
862 my @part_export = $self->part_svc->part_export_usage;
863 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
864 " service definition"
869 my $AcctStartTime = 0;
871 foreach my $part_export ( @part_export ) {
873 next if $part_export->option('ignore_accounting');
875 warn "$mes connecting to sqlradius database\n"
878 my $dbh = DBI->connect( map { $part_export->option($_) }
879 qw(datasrc username password) )
880 or die "can't connect to sqlradius database: ". $DBI::errstr;
882 warn "$mes connected to sqlradius database\n"
885 #select a unix time conversion function based on database type
886 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
887 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
889 my $username = $part_export->export_username($svc_x);
891 warn "$mes finding most-recent $attrib\n"
896 if ($part_export->option('process_single_realm')) {
897 $realm = 'AND Realm = ?';
898 $realmparam = $part_export->option('realm');
901 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
905 ORDER BY AcctStartTime DESC LIMIT 1
906 ") or die $dbh->errstr;
907 $sth->execute($username, ($realm ? $realmparam : ()) )
910 my $row = $sth->fetchrow_arrayref;
911 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
913 $AcctStartTime = $row->[1];
925 =item get_session_history TIMESTAMP_START TIMESTAMP_END
927 See L<FS::svc_acct/get_session_history>. Equivalent to
928 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
929 records where B<svcdb> is not "svc_acct".
933 sub get_session_history {
934 my($self, $start, $end, $attrib) = @_;
938 my @part_export = $self->part_svc->part_export_usage;
939 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
940 " service definition"
946 foreach my $part_export ( @part_export ) {
948 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
955 =item tickets [ STATUS ]
957 Returns an array of hashes representing the tickets linked to this service.
959 An optional status (or arrayref or hashref of statuses) may be specified.
965 my $status = ( @_ && $_[0] ) ? shift : '';
967 my $conf = FS::Conf->new;
968 my $num = $conf->config('cust_main-max_tickets') || 10;
971 if ( $conf->config('ticket_system') ) {
972 unless ( $conf->config('ticket_system-custom_priority_field') ) {
974 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
983 foreach my $priority (
984 $conf->config('ticket_system-custom_priority_field-values'), ''
986 last if scalar(@tickets) >= $num;
988 @{ FS::TicketSystem->service_tickets( $self->svcnum,
989 $num - scalar(@tickets),
1002 my $svc_x = $self->svc_x;
1003 +{ ( map { $_=>$self->$_ } $self->fields ),
1004 ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
1014 =item smart_search OPTION => VALUE ...
1016 Accepts the option I<search>, the string to search for. The string will
1017 be searched for as a username, email address, IP address, MAC address,
1018 phone number, and hardware serial number. Unlike the I<smart_search> on
1019 customers, this always requires an exact match.
1023 # though perhaps it should be fuzzy in some cases?
1026 my %param = __PACKAGE__->smart_search_param(@_);
1030 sub smart_search_param {
1034 my $string = $opt{'search'};
1035 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1038 map { my $table = $_;
1039 my $search_sql = "FS::$table"->search_sql($string);
1041 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1042 "FROM $table WHERE $search_sql";
1044 FS::part_svc->svc_tables;
1046 if ( $string =~ /^(\d+)$/ ) {
1047 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1050 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1051 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1055 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1056 'null_right' => 'View/link unlinked services'
1058 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1060 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1061 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1062 ' LEFT JOIN part_svc USING ( svcpart )';
1065 'table' => 'cust_svc',
1066 'select' => 'svc_all.svcnum AS svcnum, '.
1067 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1069 'addl_from' => $addl_from,
1071 'extra_sql' => $extra_sql,
1078 # fix missing (deleted by mistake) svc_x records
1079 warn "searching for missing svc_x records...\n";
1081 'table' => 'cust_svc',
1082 'select' => 'cust_svc.*',
1083 'addl_from' => ' LEFT JOIN ( ' .
1085 map { "SELECT svcnum FROM $_" }
1086 FS::part_svc->svc_tables
1087 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1088 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1090 my @svcs = qsearch(\%search);
1091 warn "found ".scalar(@svcs)."\n";
1093 local $FS::Record::nowarn_classload = 1; # for h_svc_
1094 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1097 'hashref' => { history_action => 'delete' },
1098 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1100 foreach my $cust_svc (@svcs) {
1101 my $svcnum = $cust_svc->svcnum;
1102 my $svcdb = $cust_svc->part_svc->svcdb;
1103 $h_search{'hashref'}{'svcnum'} = $svcnum;
1104 $h_search{'table'} = "h_$svcdb";
1105 my $h_svc_x = qsearchs(\%h_search)
1107 my $class = "FS::$svcdb";
1108 my $new_svc_x = $class->new({ $h_svc_x->hash });
1109 my $error = $new_svc_x->insert;
1110 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1121 Behaviour of changing the svcpart of cust_svc records is undefined and should
1122 possibly be prohibited, and pkg_svc records are not checked.
1124 pkg_svc records are not checked in general (here).
1126 Deleting this record doesn't check or delete the svc_* record associated
1129 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1130 a DBI database handle is not yet implemented.
1134 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1135 schema.html from the base documentation