4 use vars qw( @ISA $DEBUG $me $ignore_quantity $conf $ticket_system );
6 #use Scalar::Util qw( blessed );
7 use List::Util qw( max );
9 use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
14 use FS::domain_record;
19 #most FS::svc_ classes are autoloaded in svc_x emthod
20 use FS::svc_acct; #this one is used in the cache stuff
22 @ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
29 #ask FS::UID to run this stuff for us later
30 FS::UID->install_callback( sub {
32 $ticket_system = $conf->config('ticket_system')
37 my ( $hashref, $cache ) = @_;
38 if ( $hashref->{'username'} ) {
39 $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
41 if ( $hashref->{'svc'} ) {
42 $self->{'_svcpart'} = FS::part_svc->new($hashref);
48 FS::cust_svc - Object method for cust_svc objects
54 $record = new FS::cust_svc \%hash
55 $record = new FS::cust_svc { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ($label, $value) = $record->label;
69 An FS::cust_svc represents a service. FS::cust_svc inherits from FS::Record.
70 The following fields are currently supported:
74 =item svcnum - primary key (assigned automatically for new services)
76 =item pkgnum - Package (see L<FS::cust_pkg>)
78 =item svcpart - Service definition (see L<FS::part_svc>)
80 =item agent_svcid - Optional legacy service ID
82 =item overlimit - date the service exceeded its usage limit
92 Creates a new service. To add the refund to the database, see L<"insert">.
93 Services are normally created by creating FS::svc_ objects (see
94 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
98 sub table { 'cust_svc'; }
102 Adds this service to the database. If there is an error, returns the error,
103 otherwise returns false.
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 $error ||= $new->svc_x->export('relocate',
339 $new->cust_pkg->cust_location,
340 $old->cust_pkg->cust_location,
345 $dbh->rollback if $oldAutoCommit;
346 return $error if $error;
349 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
356 Checks all fields to make sure this is a valid service. If there is an error,
357 returns the error, otherwise returns false. Called by the insert and
366 $self->ut_numbern('svcnum')
367 || $self->ut_numbern('pkgnum')
368 || $self->ut_number('svcpart')
369 || $self->ut_numbern('agent_svcid')
370 || $self->ut_numbern('overlimit')
372 return $error if $error;
374 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
375 return "Unknown svcpart" unless $part_svc;
377 if ( $self->pkgnum && ! $ignore_quantity ) {
379 #slightly inefficient since ->pkg_svc will also look it up, but fixing
380 # a much larger perf problem and have bigger fish to fry
381 my $cust_pkg = $self->cust_pkg;
383 my $pkg_svc = $self->pkg_svc
384 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
385 'pkgpart' => $cust_pkg->pkgpart,
389 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
390 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
391 my $addon_pkg_svc = qsearchs('pkg_svc', {
392 pkgpart => $part_pkg_link->dst_pkgpart,
393 svcpart => $self->svcpart,
395 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
399 #better error message? UI shouldn't get here
400 return "No svcpart ". $self->svcpart.
401 " services in pkgpart ". $cust_pkg->pkgpart
402 unless $pkg_svc->quantity > 0;
404 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
406 #false laziness w/cust_pkg->part_svc
407 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
411 #better error message? again, UI shouldn't get here
412 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
413 " services for pkgnum ". $self->pkgnum
423 Returns the displayed service number for this service: agent_svcid if it has a
424 value, svcnum otherwise
430 $self->agent_svcid || $self->svcnum;
435 Returns the definition for this service, as a FS::part_svc object (see
443 ? $self->{'_svcpart'}
444 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
449 Returns the package this service belongs to, as a FS::cust_pkg object (see
456 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
461 Returns the pkg_svc record for for this service, if applicable.
467 my $cust_pkg = $self->cust_pkg;
468 return undef unless $cust_pkg;
470 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
471 'pkgpart' => $cust_pkg->pkgpart,
478 Returns the date this service was inserted.
484 $self->h_date('insert');
487 =item pkg_cancel_date
489 Returns the date this service's package was canceled. This normally only
490 exists for a service that's been preserved through cancellation with the
491 part_pkg.preserve flag.
495 sub pkg_cancel_date {
497 my $cust_pkg = $self->cust_pkg or return;
498 return $cust_pkg->getfield('cancel') || '';
503 Returns a list consisting of:
504 - The name of this service (from part_svc)
505 - A meaningful identifier (username, domain, or mail alias)
506 - The table name (i.e. svc_domain) for this service
511 my($label, $value, $svcdb) = $cust_svc->label;
515 Like the B<label> method, except the second item in the list ("meaningful
516 identifier") may be longer - typically, a full name is included.
520 sub label { shift->_label('svc_label', @_); }
521 sub label_long { shift->_label('svc_label_long', @_); }
526 my $svc_x = $self->svc_x
527 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
529 $self->$method($svc_x);
532 sub svc_label { shift->_svc_label('label', @_); }
533 sub svc_label_long { shift->_svc_label('label_long', @_); }
536 my( $self, $method, $svc_x ) = ( shift, shift, shift );
539 $self->part_svc->svc,
541 $self->part_svc->svcdb,
549 Returns a listref of html elements associated with this service's exports.
555 my $svc_x = $self->svc_x
556 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
558 $svc_x->export_links;
561 =item export_getsettings
563 Returns two hashrefs of settings associated with this service's exports.
567 sub export_getsettings {
569 my $svc_x = $self->svc_x
570 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
572 $svc_x->export_getsettings;
578 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
579 FS::svc_domain object, etc.)
585 my $svcdb = $self->part_svc->svcdb;
586 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
587 $self->{'_svc_acct'};
589 require "FS/$svcdb.pm";
590 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
591 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
593 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
597 =item seconds_since TIMESTAMP
599 See L<FS::svc_acct/seconds_since>. Equivalent to
600 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
601 where B<svcdb> is not "svc_acct".
605 #internal session db deprecated (or at least on hold)
606 sub seconds_since { 'internal session db deprecated'; };
607 ##note: implementation here, POD in FS::svc_acct
609 # my($self, $since) = @_;
611 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
614 # AND logout IS NOT NULL'
615 # ) or die $dbh->errstr;
616 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
617 # $sth->fetchrow_arrayref->[0];
620 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
622 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
623 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
624 for records where B<svcdb> is not "svc_acct".
628 #note: implementation here, POD in FS::svc_acct
629 sub seconds_since_sqlradacct {
630 my($self, $start, $end) = @_;
632 my $mes = "$me seconds_since_sqlradacct:";
634 my $svc_x = $self->svc_x;
636 my @part_export = $self->part_svc->part_export_usage;
637 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
638 " service definition"
643 foreach my $part_export ( @part_export ) {
645 next if $part_export->option('ignore_accounting');
647 warn "$mes connecting to sqlradius database\n"
650 my $dbh = DBI->connect( map { $part_export->option($_) }
651 qw(datasrc username password) )
652 or die "can't connect to sqlradius database: ". $DBI::errstr;
654 warn "$mes connected to sqlradius database\n"
657 #select a unix time conversion function based on database type
658 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
659 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
661 my $username = $part_export->export_username($svc_x);
665 warn "$mes finding closed sessions completely within the given range\n"
670 if ($part_export->option('process_single_realm')) {
671 $realm = 'AND Realm = ?';
672 $realmparam = $part_export->option('realm');
675 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
679 AND $str2time AcctStartTime $closing >= ?
680 AND $str2time AcctStopTime $closing < ?
681 AND $str2time AcctStopTime $closing > 0
682 AND AcctStopTime IS NOT NULL"
683 ) or die $dbh->errstr;
684 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
686 my $regular = $sth->fetchrow_arrayref->[0];
688 warn "$mes finding open sessions which start in the range\n"
691 # count session start->range end
692 $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
696 AND $str2time AcctStartTime $closing >= ?
697 AND $str2time AcctStartTime $closing < ?
698 AND ( ? - $str2time AcctStartTime $closing ) < 86400
699 AND ( $str2time AcctStopTime $closing = 0
700 OR AcctStopTime IS NULL )";
701 $sth = $dbh->prepare($query) or die $dbh->errstr;
704 ($realm ? $realmparam : ()),
708 or die $sth->errstr. " executing query $query";
709 my $start_during = $sth->fetchrow_arrayref->[0];
711 warn "$mes finding closed sessions which start before the range but stop during\n"
714 #count range start->session end
715 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
719 AND $str2time AcctStartTime $closing < ?
720 AND $str2time AcctStopTime $closing >= ?
721 AND $str2time AcctStopTime $closing < ?
722 AND $str2time AcctStopTime $closing > 0
723 AND AcctStopTime IS NOT NULL"
724 ) or die $dbh->errstr;
725 $sth->execute( $start,
727 ($realm ? $realmparam : ()),
732 my $end_during = $sth->fetchrow_arrayref->[0];
734 warn "$mes finding closed sessions which start before the range but stop after\n"
737 # count range start->range end
738 # don't count open sessions anymore (probably missing stop record)
739 $sth = $dbh->prepare("SELECT COUNT(*)
743 AND $str2time AcctStartTime $closing < ?
744 AND ( $str2time AcctStopTime $closing >= ?
746 # OR AcctStopTime = 0
747 # OR AcctStopTime IS NULL )"
748 ) or die $dbh->errstr;
749 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
751 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
753 $seconds += $regular + $end_during + $start_during + $entire_range;
755 warn "$mes done finding sessions\n"
764 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
766 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
767 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
768 for records where B<svcdb> is not "svc_acct".
772 #note: implementation here, POD in FS::svc_acct
773 #(false laziness w/seconds_since_sqlradacct above)
774 sub attribute_since_sqlradacct {
775 my($self, $start, $end, $attrib) = @_;
777 my $mes = "$me attribute_since_sqlradacct:";
779 my $svc_x = $self->svc_x;
781 my @part_export = $self->part_svc->part_export_usage;
782 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
783 " service definition"
789 foreach my $part_export ( @part_export ) {
791 next if $part_export->option('ignore_accounting');
793 warn "$mes connecting to sqlradius database\n"
796 my $dbh = DBI->connect( map { $part_export->option($_) }
797 qw(datasrc username password) )
798 or die "can't connect to sqlradius database: ". $DBI::errstr;
800 warn "$mes connected to sqlradius database\n"
803 #select a unix time conversion function based on database type
804 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
805 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
807 my $username = $part_export->export_username($svc_x);
809 warn "$mes SUMing $attrib sessions\n"
814 if ($part_export->option('process_single_realm')) {
815 $realm = 'AND Realm = ?';
816 $realmparam = $part_export->option('realm');
819 my $sth = $dbh->prepare("SELECT SUM($attrib)
823 AND $str2time AcctStopTime $closing >= ?
824 AND $str2time AcctStopTime $closing < ?
825 AND AcctStopTime IS NOT NULL"
826 ) or die $dbh->errstr;
827 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
830 my $row = $sth->fetchrow_arrayref;
831 $sum += $row->[0] if defined($row->[0]);
833 warn "$mes done SUMing sessions\n"
842 #note: implementation here, POD in FS::svc_acct
843 # false laziness w/above
844 sub attribute_last_sqlradacct {
845 my($self, $attrib) = @_;
847 my $mes = "$me attribute_last_sqlradacct:";
849 my $svc_x = $self->svc_x;
851 my @part_export = $self->part_svc->part_export_usage;
852 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
853 " service definition"
858 my $AcctStartTime = 0;
860 foreach my $part_export ( @part_export ) {
862 next if $part_export->option('ignore_accounting');
864 warn "$mes connecting to sqlradius database\n"
867 my $dbh = DBI->connect( map { $part_export->option($_) }
868 qw(datasrc username password) )
869 or die "can't connect to sqlradius database: ". $DBI::errstr;
871 warn "$mes connected to sqlradius database\n"
874 #select a unix time conversion function based on database type
875 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
876 my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
878 my $username = $part_export->export_username($svc_x);
880 warn "$mes finding most-recent $attrib\n"
885 if ($part_export->option('process_single_realm')) {
886 $realm = 'AND Realm = ?';
887 $realmparam = $part_export->option('realm');
890 my $sth = $dbh->prepare("SELECT $attrib, $str2time AcctStartTime $closing
894 ORDER BY AcctStartTime DESC LIMIT 1
895 ") or die $dbh->errstr;
896 $sth->execute($username, ($realm ? $realmparam : ()) )
899 my $row = $sth->fetchrow_arrayref;
900 if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
902 $AcctStartTime = $row->[1];
914 =item get_session_history TIMESTAMP_START TIMESTAMP_END
916 See L<FS::svc_acct/get_session_history>. Equivalent to
917 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
918 records where B<svcdb> is not "svc_acct".
922 sub get_session_history {
923 my($self, $start, $end, $attrib) = @_;
927 my @part_export = $self->part_svc->part_export_usage;
928 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
929 " service definition"
935 foreach my $part_export ( @part_export ) {
937 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
944 =item tickets [ STATUS ]
946 Returns an array of hashes representing the tickets linked to this service.
948 An optional status (or arrayref or hashref of statuses) may be specified.
954 my $status = ( @_ && $_[0] ) ? shift : '';
956 my $conf = FS::Conf->new;
957 my $num = $conf->config('cust_main-max_tickets') || 10;
960 if ( $conf->config('ticket_system') ) {
961 unless ( $conf->config('ticket_system-custom_priority_field') ) {
963 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
972 foreach my $priority (
973 $conf->config('ticket_system-custom_priority_field-values'), ''
975 last if scalar(@tickets) >= $num;
977 @{ FS::TicketSystem->service_tickets( $self->svcnum,
978 $num - scalar(@tickets),
996 =item smart_search OPTION => VALUE ...
998 Accepts the option I<search>, the string to search for. The string will
999 be searched for as a username, email address, IP address, MAC address,
1000 phone number, and hardware serial number. Unlike the I<smart_search> on
1001 customers, this always requires an exact match.
1005 # though perhaps it should be fuzzy in some cases?
1008 my %param = __PACKAGE__->smart_search_param(@_);
1012 sub smart_search_param {
1016 my $string = $opt{'search'};
1017 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
1020 map { my $table = $_;
1021 my $search_sql = "FS::$table"->search_sql($string);
1023 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
1024 "FROM $table WHERE $search_sql";
1026 FS::part_svc->svc_tables;
1028 if ( $string =~ /^(\d+)$/ ) {
1029 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
1032 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
1033 " ON (svc_all.svcnum = cust_svc.svcnum) ";
1037 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
1038 'null_right' => 'View/link unlinked services'
1040 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
1042 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
1043 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
1044 ' LEFT JOIN part_svc USING ( svcpart )';
1047 'table' => 'cust_svc',
1048 'select' => 'svc_all.svcnum AS svcnum, '.
1049 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
1051 'addl_from' => $addl_from,
1053 'extra_sql' => $extra_sql,
1060 # fix missing (deleted by mistake) svc_x records
1061 warn "searching for missing svc_x records...\n";
1063 'table' => 'cust_svc',
1064 'select' => 'cust_svc.*',
1065 'addl_from' => ' LEFT JOIN ( ' .
1067 map { "SELECT svcnum FROM $_" }
1068 FS::part_svc->svc_tables
1069 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
1070 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
1072 my @svcs = qsearch(\%search);
1073 warn "found ".scalar(@svcs)."\n";
1075 local $FS::Record::nowarn_classload = 1; # for h_svc_
1076 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
1079 'hashref' => { history_action => 'delete' },
1080 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
1082 foreach my $cust_svc (@svcs) {
1083 my $svcnum = $cust_svc->svcnum;
1084 my $svcdb = $cust_svc->part_svc->svcdb;
1085 $h_search{'hashref'}{'svcnum'} = $svcnum;
1086 $h_search{'table'} = "h_$svcdb";
1087 my $h_svc_x = qsearchs(\%h_search)
1089 my $class = "FS::$svcdb";
1090 my $new_svc_x = $class->new({ $h_svc_x->hash });
1091 my $error = $new_svc_x->insert;
1092 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1103 Behaviour of changing the svcpart of cust_svc records is undefined and should
1104 possibly be prohibited, and pkg_svc records are not checked.
1106 pkg_svc records are not checked in general (here).
1108 Deleting this record doesn't check or delete the svc_* record associated
1111 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1112 a DBI database handle is not yet implemented.
1116 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1117 schema.html from the base documentation