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 $error ||= $new->svc_x->export('relocate',
338 $new->cust_pkg->cust_location,
339 $old->cust_pkg->cust_location,
344 $dbh->rollback if $oldAutoCommit;
345 return $error if $error;
348 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
355 Checks all fields to make sure this is a valid service. If there is an error,
356 returns the error, otherwise returns false. Called by the insert and
365 $self->ut_numbern('svcnum')
366 || $self->ut_numbern('pkgnum')
367 || $self->ut_number('svcpart')
368 || $self->ut_numbern('agent_svcid')
369 || $self->ut_numbern('overlimit')
371 return $error if $error;
373 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
374 return "Unknown svcpart" unless $part_svc;
376 if ( $self->pkgnum && ! $ignore_quantity ) {
378 #slightly inefficient since ->pkg_svc will also look it up, but fixing
379 # a much larger perf problem and have bigger fish to fry
380 my $cust_pkg = $self->cust_pkg;
382 my $pkg_svc = $self->pkg_svc
383 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
384 'pkgpart' => $cust_pkg->pkgpart,
388 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
389 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
390 my $addon_pkg_svc = qsearchs('pkg_svc', {
391 pkgpart => $part_pkg_link->dst_pkgpart,
392 svcpart => $self->svcpart,
394 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
398 #better error message? UI shouldn't get here
399 return "No svcpart ". $self->svcpart.
400 " services in pkgpart ". $cust_pkg->pkgpart
401 unless $pkg_svc->quantity > 0;
403 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
405 #false laziness w/cust_pkg->part_svc
406 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
410 #better error message? again, UI shouldn't get here
411 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
412 " services for pkgnum ". $self->pkgnum
422 Returns the displayed service number for this service: agent_svcid if it has a
423 value, svcnum otherwise
429 $self->agent_svcid || $self->svcnum;
434 Returns the definition for this service, as a FS::part_svc object (see
442 ? $self->{'_svcpart'}
443 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
448 Returns the package this service belongs to, as a FS::cust_pkg object (see
453 Returns the pkg_svc record for for this service, if applicable.
459 my $cust_pkg = $self->cust_pkg;
460 return undef unless $cust_pkg;
462 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
463 'pkgpart' => $cust_pkg->pkgpart,
470 Returns the date this service was inserted.
476 $self->h_date('insert');
479 =item pkg_cancel_date
481 Returns the date this service's package was canceled. This normally only
482 exists for a service that's been preserved through cancellation with the
483 part_pkg.preserve flag.
487 sub pkg_cancel_date {
489 my $cust_pkg = $self->cust_pkg or return;
490 return $cust_pkg->getfield('cancel') || '';
495 Returns a list consisting of:
496 - The name of this service (from part_svc)
497 - A meaningful identifier (username, domain, or mail alias)
498 - The table name (i.e. svc_domain) for this service
503 my($label, $value, $svcdb) = $cust_svc->label;
507 Like the B<label> method, except the second item in the list ("meaningful
508 identifier") may be longer - typically, a full name is included.
512 sub label { shift->_label('svc_label', @_); }
513 sub label_long { shift->_label('svc_label_long', @_); }
518 my $svc_x = $self->svc_x
519 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
521 $self->$method($svc_x);
524 sub svc_label { shift->_svc_label('label', @_); }
525 sub svc_label_long { shift->_svc_label('label_long', @_); }
528 my( $self, $method, $svc_x ) = ( shift, shift, shift );
531 $self->part_svc->svc,
533 $self->part_svc->svcdb,
541 Returns a listref of html elements associated with this service's exports.
547 my $svc_x = $self->svc_x
548 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
550 $svc_x->export_links;
553 =item export_getsettings
555 Returns two hashrefs of settings associated with this service's exports.
559 sub export_getsettings {
561 my $svc_x = $self->svc_x
562 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
564 $svc_x->export_getsettings;
570 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
571 FS::svc_domain object, etc.)
577 my $svcdb = $self->part_svc->svcdb;
578 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
579 $self->{'_svc_acct'};
581 require "FS/$svcdb.pm";
582 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
583 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
585 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
589 =item seconds_since TIMESTAMP
591 See L<FS::svc_acct/seconds_since>. Equivalent to
592 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
593 where B<svcdb> is not "svc_acct".
597 #internal session db deprecated (or at least on hold)
598 sub seconds_since { 'internal session db deprecated'; };
599 ##note: implementation here, POD in FS::svc_acct
601 # my($self, $since) = @_;
603 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
606 # AND logout IS NOT NULL'
607 # ) or die $dbh->errstr;
608 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
609 # $sth->fetchrow_arrayref->[0];
612 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
614 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
615 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
616 for records where B<svcdb> is not "svc_acct".
620 #note: implementation here, POD in FS::svc_acct
621 sub seconds_since_sqlradacct {
622 my($self, $start, $end) = @_;
624 my $mes = "$me seconds_since_sqlradacct:";
626 my $svc_x = $self->svc_x;
628 my @part_export = $self->part_svc->part_export_usage;
629 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
630 " service definition"
635 foreach my $part_export ( @part_export ) {
637 next if $part_export->option('ignore_accounting');
639 warn "$mes connecting to sqlradius database\n"
642 my $dbh = DBI->connect( map { $part_export->option($_) }
643 qw(datasrc username password) )
644 or die "can't connect to sqlradius database: ". $DBI::errstr;
646 warn "$mes connected to sqlradius database\n"
649 #select a unix time conversion function based on database type
650 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
651 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
653 my $username = $part_export->export_username($svc_x);
657 warn "$mes finding closed sessions completely within the given range\n"
662 if ($part_export->option('process_single_realm')) {
663 $realm = 'AND Realm = ?';
664 $realmparam = $part_export->option('realm');
667 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
671 AND $str2time AcctStartTime $closing >= ?
672 AND $str2time AcctStopTime $closing < ?
673 AND $str2time AcctStopTime $closing > 0
674 AND AcctStopTime IS NOT NULL"
675 ) or die $dbh->errstr;
676 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
678 my $regular = $sth->fetchrow_arrayref->[0];
680 warn "$mes finding open sessions which start in the range\n"
683 # count session start->range end
684 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
688 AND $str2time AcctStartTime $closing >= ?
689 AND $str2time AcctStartTime $closing < ?
690 AND ( ? - $str2time AcctStartTime $closing ) < 86400
691 AND ( $str2time AcctStopTime $closing = 0
692 OR AcctStopTime IS NULL )";
693 $sth = $dbh->prepare($query) or die $dbh->errstr;
696 ($realm ? $realmparam : ()),
700 or die $sth->errstr. " executing query $query";
701 my $start_during = $sth->fetchrow_arrayref->[0];
703 warn "$mes finding closed sessions which start before the range but stop during\n"
706 #count range start->session end
707 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
711 AND $str2time AcctStartTime $closing < ?
712 AND $str2time AcctStopTime $closing >= ?
713 AND $str2time AcctStopTime $closing < ?
714 AND $str2time AcctStopTime $closing > 0
715 AND AcctStopTime IS NOT NULL"
716 ) or die $dbh->errstr;
717 $sth->execute( $start,
719 ($realm ? $realmparam : ()),
724 my $end_during = $sth->fetchrow_arrayref->[0];
726 warn "$mes finding closed sessions which start before the range but stop after\n"
729 # count range start->range end
730 # don't count open sessions anymore (probably missing stop record)
731 $sth = $dbh->prepare("SELECT COUNT(*)
735 AND $str2time AcctStartTime $closing < ?
736 AND ( $str2time AcctStopTime $closing >= ?
738 # OR AcctStopTime = 0
739 # OR AcctStopTime IS NULL )"
740 ) or die $dbh->errstr;
741 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
743 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
745 $seconds += $regular + $end_during + $start_during + $entire_range;
747 warn "$mes done finding sessions\n"
756 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
758 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
759 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
760 for records where B<svcdb> is not "svc_acct".
764 #note: implementation here, POD in FS::svc_acct
765 #(false laziness w/seconds_since_sqlradacct above)
766 sub attribute_since_sqlradacct {
767 my($self, $start, $end, $attrib) = @_;
769 my $mes = "$me attribute_since_sqlradacct:";
771 my $svc_x = $self->svc_x;
773 my @part_export = $self->part_svc->part_export_usage;
774 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
775 " service definition"
781 foreach my $part_export ( @part_export ) {
783 next if $part_export->option('ignore_accounting');
785 warn "$mes connecting to sqlradius database\n"
788 my $dbh = DBI->connect( map { $part_export->option($_) }
789 qw(datasrc username password) )
790 or die "can't connect to sqlradius database: ". $DBI::errstr;
792 warn "$mes connected to sqlradius database\n"
795 #select a unix time conversion function based on database type
796 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
797 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
799 my $username = $part_export->export_username($svc_x);
801 warn "$mes SUMing $attrib sessions\n"
806 if ($part_export->option('process_single_realm')) {
807 $realm = 'AND Realm = ?';
808 $realmparam = $part_export->option('realm');
811 my $sth = $dbh->prepare("SELECT SUM($attrib)
815 AND $str2time AcctStopTime $closing >= ?
816 AND $str2time AcctStopTime $closing < ?
817 AND AcctStopTime IS NOT NULL"
818 ) or die $dbh->errstr;
819 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
822 my $row = $sth->fetchrow_arrayref;
823 $sum += $row->[0] if defined($row->[0]);
825 warn "$mes done SUMing sessions\n"
834 #note: implementation here, POD in FS::svc_acct
835 # false laziness w/above
836 sub attribute_last_sqlradacct {
837 my($self, $attrib) = @_;
839 my $mes = "$me attribute_last_sqlradacct:";
841 my $svc_x = $self->svc_x;
843 my @part_export = $self->part_svc->part_export_usage;
844 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
845 " service definition"
850 my $AcctStartTime = 0;
852 foreach my $part_export ( @part_export ) {
854 next if $part_export->option('ignore_accounting');
856 warn "$mes connecting to sqlradius database\n"
859 my $dbh = DBI->connect( map { $part_export->option($_) }
860 qw(datasrc username password) )
861 or die "can't connect to sqlradius database: ". $DBI::errstr;
863 warn "$mes connected to sqlradius database\n"
866 #select a unix time conversion function based on database type
867 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
868 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
870 my $username = $part_export->export_username($svc_x);
872 warn "$mes finding most-recent $attrib\n"
877 if ($part_export->option('process_single_realm')) {
878 $realm = 'AND Realm = ?';
879 $realmparam = $part_export->option('realm');
882 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
886 ORDER BY AcctStartTime DESC LIMIT 1
887 ") or die $dbh->errstr;
888 $sth->execute($username, ($realm ? $realmparam : ()) )
891 my $row = $sth->fetchrow_arrayref;
892 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
894 $AcctStartTime = $row->[1];
906 =item get_session_history TIMESTAMP_START TIMESTAMP_END
908 See L<FS::svc_acct/get_session_history>. Equivalent to
909 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
910 records where B<svcdb> is not "svc_acct".
914 sub get_session_history {
915 my($self, $start, $end, $attrib) = @_;
919 my @part_export = $self->part_svc->part_export_usage;
920 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
921 " service definition"
927 foreach my $part_export ( @part_export ) {
929 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
936 =item tickets [ STATUS ]
938 Returns an array of hashes representing the tickets linked to this service.
940 An optional status (or arrayref or hashref of statuses) may be specified.
946 my $status = ( @_ && $_[0] ) ? shift : '';
948 my $conf = FS::Conf->new;
949 my $num = $conf->config('cust_main-max_tickets') || 10;
952 if ( $conf->config('ticket_system') ) {
953 unless ( $conf->config('ticket_system-custom_priority_field') ) {
955 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
964 foreach my $priority (
965 $conf->config('ticket_system-custom_priority_field-values'), ''
967 last if scalar(@tickets) >= $num;
969 @{ FS::TicketSystem->service_tickets( $self->svcnum,
970 $num - scalar(@tickets),
983 my $svc_x = $self->svc_x;
984 +{ ( map { $_=>$self->$_ } $self->fields ),
985 ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
995 =item smart_search OPTION => VALUE ...
997 Accepts the option I<search>, the string to search for. The string will
998 be searched for as a username, email address, IP address, MAC address,
999 phone number, and hardware serial number. Unlike the I<smart_search> on
1000 customers, this always requires an exact match.
1004 # though perhaps it should be fuzzy in some cases?
1007 my %param = __PACKAGE__->smart_search_param(@_);
1011 sub smart_search_param {
1015 my $string = $opt{'search'};
1016 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1019 map { my $table = $_;
1020 my $search_sql = "FS::$table"->search_sql($string);
1022 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1023 "FROM $table WHERE $search_sql";
1025 FS::part_svc->svc_tables;
1027 if ( $string =~ /^(\d+)$/ ) {
1028 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1031 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1032 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1036 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1037 'null_right' => 'View/link unlinked services'
1039 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1041 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1042 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1043 ' LEFT JOIN part_svc USING ( svcpart )';
1046 'table' => 'cust_svc',
1047 'select' => 'svc_all.svcnum AS svcnum, '.
1048 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1050 'addl_from' => $addl_from,
1052 'extra_sql' => $extra_sql,
1059 # fix missing (deleted by mistake) svc_x records
1060 warn "searching for missing svc_x records...\n";
1062 'table' => 'cust_svc',
1063 'select' => 'cust_svc.*',
1064 'addl_from' => ' LEFT JOIN ( ' .
1066 map { "SELECT svcnum FROM $_" }
1067 FS::part_svc->svc_tables
1068 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1069 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1071 my @svcs = qsearch(\%search);
1072 warn "found ".scalar(@svcs)."\n";
1074 local $FS::Record::nowarn_classload = 1; # for h_svc_
1075 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1078 'hashref' => { history_action => 'delete' },
1079 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1081 foreach my $cust_svc (@svcs) {
1082 my $svcnum = $cust_svc->svcnum;
1083 my $svcdb = $cust_svc->part_svc->svcdb;
1084 $h_search{'hashref'}{'svcnum'} = $svcnum;
1085 $h_search{'table'} = "h_$svcdb";
1086 my $h_svc_x = qsearchs(\%h_search)
1088 my $class = "FS::$svcdb";
1089 my $new_svc_x = $class->new({ $h_svc_x->hash });
1090 my $error = $new_svc_x->insert;
1091 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1102 Behaviour of changing the svcpart of cust_svc records is undefined and should
1103 possibly be prohibited, and pkg_svc records are not checked.
1105 pkg_svc records are not checked in general (here).
1107 Deleting this record doesn't check or delete the svc_* record associated
1110 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1111 a DBI database handle is not yet implemented.
1115 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1116 schema.html from the base documentation