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 );
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,
327 $dbh->rollback if $oldAutoCommit;
328 return $error if $error;
332 #my $error = $new->SUPER::replace($old, @_);
333 my $error = $new->SUPER::replace($old);
335 $dbh->rollback if $oldAutoCommit;
336 return $error if $error;
339 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
346 Checks all fields to make sure this is a valid service. If there is an error,
347 returns the error, otherwise returns false. Called by the insert and
356 $self->ut_numbern('svcnum')
357 || $self->ut_numbern('pkgnum')
358 || $self->ut_number('svcpart')
359 || $self->ut_numbern('agent_svcid')
360 || $self->ut_numbern('overlimit')
362 return $error if $error;
364 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
365 return "Unknown svcpart" unless $part_svc;
367 if ( $self->pkgnum && ! $ignore_quantity ) {
369 #slightly inefficient since ->pkg_svc will also look it up, but fixing
370 # a much larger perf problem and have bigger fish to fry
371 my $cust_pkg = $self->cust_pkg;
373 my $pkg_svc = $self->pkg_svc
374 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
375 'pkgpart' => $cust_pkg->pkgpart,
379 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
380 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
381 my $addon_pkg_svc = qsearchs('pkg_svc', {
382 pkgpart => $part_pkg_link->dst_pkgpart,
383 svcpart => $self->svcpart,
385 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
389 #better error message? UI shouldn't get here
390 return "No svcpart ". $self->svcpart.
391 " services in pkgpart ". $cust_pkg->pkgpart
392 unless $pkg_svc->quantity > 0;
394 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
396 #false laziness w/cust_pkg->part_svc
397 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
401 #better error message? again, UI shouldn't get here
402 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
403 " services for pkgnum ". $self->pkgnum
413 Returns the displayed service number for this service: agent_svcid if it has a
414 value, svcnum otherwise
420 $self->agent_svcid || $self->svcnum;
425 Returns the definition for this service, as a FS::part_svc object (see
433 ? $self->{'_svcpart'}
434 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
439 Returns the package this service belongs to, as a FS::cust_pkg object (see
446 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
451 Returns the pkg_svc record for for this service, if applicable.
457 my $cust_pkg = $self->cust_pkg;
458 return undef unless $cust_pkg;
460 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
461 'pkgpart' => $cust_pkg->pkgpart,
468 Returns the date this service was inserted.
474 $self->h_date('insert');
477 =item pkg_cancel_date
479 Returns the date this service's package was canceled. This normally only
480 exists for a service that's been preserved through cancellation with the
481 part_pkg.preserve flag.
485 sub pkg_cancel_date {
487 my $cust_pkg = $self->cust_pkg or return;
488 return $cust_pkg->getfield('cancel') || '';
493 Returns a list consisting of:
494 - The name of this service (from part_svc)
495 - A meaningful identifier (username, domain, or mail alias)
496 - The table name (i.e. svc_domain) for this service
501 my($label, $value, $svcdb) = $cust_svc->label;
505 Like the B<label> method, except the second item in the list ("meaningful
506 identifier") may be longer - typically, a full name is included.
510 sub label { shift->_label('svc_label', @_); }
511 sub label_long { shift->_label('svc_label_long', @_); }
516 my $svc_x = $self->svc_x
517 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
519 $self->$method($svc_x);
522 sub svc_label { shift->_svc_label('label', @_); }
523 sub svc_label_long { shift->_svc_label('label_long', @_); }
526 my( $self, $method, $svc_x ) = ( shift, shift, shift );
529 $self->part_svc->svc,
531 $self->part_svc->svcdb,
539 Returns a listref of html elements associated with this service's exports.
545 my $svc_x = $self->svc_x
546 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
548 $svc_x->export_links;
551 =item export_getsettings
553 Returns two hashrefs of settings associated with this service's exports.
557 sub export_getsettings {
559 my $svc_x = $self->svc_x
560 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
562 $svc_x->export_getsettings;
568 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
569 FS::svc_domain object, etc.)
575 my $svcdb = $self->part_svc->svcdb;
576 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
577 $self->{'_svc_acct'};
579 require "FS/$svcdb.pm";
580 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
581 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
583 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
587 =item seconds_since TIMESTAMP
589 See L<FS::svc_acct/seconds_since>. Equivalent to
590 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
591 where B<svcdb> is not "svc_acct".
595 #internal session db deprecated (or at least on hold)
596 sub seconds_since { 'internal session db deprecated'; };
597 ##note: implementation here, POD in FS::svc_acct
599 # my($self, $since) = @_;
601 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
604 # AND logout IS NOT NULL'
605 # ) or die $dbh->errstr;
606 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
607 # $sth->fetchrow_arrayref->[0];
610 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
612 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
613 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
614 for records where B<svcdb> is not "svc_acct".
618 #note: implementation here, POD in FS::svc_acct
619 sub seconds_since_sqlradacct {
620 my($self, $start, $end) = @_;
622 my $mes = "$me seconds_since_sqlradacct:";
624 my $svc_x = $self->svc_x;
626 my @part_export = $self->part_svc->part_export_usage;
627 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
628 " service definition"
633 foreach my $part_export ( @part_export ) {
635 next if $part_export->option('ignore_accounting');
637 warn "$mes connecting to sqlradius database\n"
640 my $dbh = DBI->connect( map { $part_export->option($_) }
641 qw(datasrc username password) )
642 or die "can't connect to sqlradius database: ". $DBI::errstr;
644 warn "$mes connected to sqlradius database\n"
647 #select a unix time conversion function based on database type
648 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
650 my $username = $part_export->export_username($svc_x);
654 warn "$mes finding closed sessions completely within the given range\n"
659 if ($part_export->option('process_single_realm')) {
660 $realm = 'AND Realm = ?';
661 $realmparam = $part_export->option('realm');
664 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
668 AND $str2time AcctStartTime) >= ?
669 AND $str2time AcctStopTime ) < ?
670 AND $str2time AcctStopTime ) > 0
671 AND AcctStopTime IS NOT NULL"
672 ) or die $dbh->errstr;
673 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
675 my $regular = $sth->fetchrow_arrayref->[0];
677 warn "$mes finding open sessions which start in the range\n"
680 # count session start->range end
681 $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
685 AND $str2time AcctStartTime ) >= ?
686 AND $str2time AcctStartTime ) < ?
687 AND ( ? - $str2time AcctStartTime ) ) < 86400
688 AND ( $str2time AcctStopTime ) = 0
689 OR AcctStopTime IS NULL )";
690 $sth = $dbh->prepare($query) or die $dbh->errstr;
693 ($realm ? $realmparam : ()),
697 or die $sth->errstr. " executing query $query";
698 my $start_during = $sth->fetchrow_arrayref->[0];
700 warn "$mes finding closed sessions which start before the range but stop during\n"
703 #count range start->session end
704 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
708 AND $str2time AcctStartTime ) < ?
709 AND $str2time AcctStopTime ) >= ?
710 AND $str2time AcctStopTime ) < ?
711 AND $str2time AcctStopTime ) > 0
712 AND AcctStopTime IS NOT NULL"
713 ) or die $dbh->errstr;
714 $sth->execute( $start,
716 ($realm ? $realmparam : ()),
721 my $end_during = $sth->fetchrow_arrayref->[0];
723 warn "$mes finding closed sessions which start before the range but stop after\n"
726 # count range start->range end
727 # don't count open sessions anymore (probably missing stop record)
728 $sth = $dbh->prepare("SELECT COUNT(*)
732 AND $str2time AcctStartTime ) < ?
733 AND ( $str2time AcctStopTime ) >= ?
735 # OR AcctStopTime = 0
736 # OR AcctStopTime IS NULL )"
737 ) or die $dbh->errstr;
738 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
740 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
742 $seconds += $regular + $end_during + $start_during + $entire_range;
744 warn "$mes done finding sessions\n"
753 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
755 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
756 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
757 for records where B<svcdb> is not "svc_acct".
761 #note: implementation here, POD in FS::svc_acct
762 #(false laziness w/seconds_since_sqlradacct above)
763 sub attribute_since_sqlradacct {
764 my($self, $start, $end, $attrib) = @_;
766 my $mes = "$me attribute_since_sqlradacct:";
768 my $svc_x = $self->svc_x;
770 my @part_export = $self->part_svc->part_export_usage;
771 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
772 " service definition"
778 foreach my $part_export ( @part_export ) {
780 next if $part_export->option('ignore_accounting');
782 warn "$mes connecting to sqlradius database\n"
785 my $dbh = DBI->connect( map { $part_export->option($_) }
786 qw(datasrc username password) )
787 or die "can't connect to sqlradius database: ". $DBI::errstr;
789 warn "$mes connected to sqlradius database\n"
792 #select a unix time conversion function based on database type
793 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
795 my $username = $part_export->export_username($svc_x);
797 warn "$mes SUMing $attrib sessions\n"
802 if ($part_export->option('process_single_realm')) {
803 $realm = 'AND Realm = ?';
804 $realmparam = $part_export->option('realm');
807 my $sth = $dbh->prepare("SELECT SUM($attrib)
811 AND $str2time AcctStopTime ) >= ?
812 AND $str2time AcctStopTime ) < ?
813 AND AcctStopTime IS NOT NULL"
814 ) or die $dbh->errstr;
815 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
818 my $row = $sth->fetchrow_arrayref;
819 $sum += $row->[0] if defined($row->[0]);
821 warn "$mes done SUMing sessions\n"
830 =item get_session_history TIMESTAMP_START TIMESTAMP_END
832 See L<FS::svc_acct/get_session_history>. Equivalent to
833 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
834 records where B<svcdb> is not "svc_acct".
838 sub get_session_history {
839 my($self, $start, $end, $attrib) = @_;
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"
851 foreach my $part_export ( @part_export ) {
853 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
860 =item tickets [ STATUS ]
862 Returns an array of hashes representing the tickets linked to this service.
864 An optional status (or arrayref or hashref of statuses) may be specified.
870 my $status = ( @_ && $_[0] ) ? shift : '';
872 my $conf = FS::Conf->new;
873 my $num = $conf->config('cust_main-max_tickets') || 10;
876 if ( $conf->config('ticket_system') ) {
877 unless ( $conf->config('ticket_system-custom_priority_field') ) {
879 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
888 foreach my $priority (
889 $conf->config('ticket_system-custom_priority_field-values'), ''
891 last if scalar(@tickets) >= $num;
893 @{ FS::TicketSystem->service_tickets( $self->svcnum,
894 $num - scalar(@tickets),
912 =item smart_search OPTION => VALUE ...
914 Accepts the option I<search>, the string to search for. The string will
915 be searched for as a username, email address, IP address, MAC address,
916 phone number, and hardware serial number. Unlike the I<smart_search> on
917 customers, this always requires an exact match.
921 # though perhaps it should be fuzzy in some cases?
924 my %param = __PACKAGE__->smart_search_param(@_);
928 sub smart_search_param {
932 my $string = $opt{'search'};
933 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
936 map { my $table = $_;
937 my $search_sql = "FS::$table"->search_sql($string);
939 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
940 "FROM $table WHERE $search_sql";
942 FS::part_svc->svc_tables;
944 if ( $string =~ /^(\d+)$/ ) {
945 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
948 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
949 " ON (svc_all.svcnum = cust_svc.svcnum) ";
953 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
954 'null_right' => 'View/link unlinked services'
956 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
958 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
959 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
960 ' LEFT JOIN part_svc USING ( svcpart )';
963 'table' => 'cust_svc',
964 'select' => 'svc_all.svcnum AS svcnum, '.
965 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
967 'addl_from' => $addl_from,
969 'extra_sql' => $extra_sql,
976 # fix missing (deleted by mistake) svc_x records
977 warn "searching for missing svc_x records...\n";
979 'table' => 'cust_svc',
980 'select' => 'cust_svc.*',
981 'addl_from' => ' LEFT JOIN ( ' .
983 map { "SELECT svcnum FROM $_" }
984 FS::part_svc->svc_tables
985 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
986 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
988 my @svcs = qsearch(\%search);
989 warn "found ".scalar(@svcs)."\n";
991 local $FS::Record::nowarn_classload = 1; # for h_svc_
992 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
995 'hashref' => { history_action => 'delete' },
996 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
998 foreach my $cust_svc (@svcs) {
999 my $svcnum = $cust_svc->svcnum;
1000 my $svcdb = $cust_svc->part_svc->svcdb;
1001 $h_search{'hashref'}{'svcnum'} = $svcnum;
1002 $h_search{'table'} = "h_$svcdb";
1003 my $h_svc_x = qsearchs(\%h_search)
1005 my $class = "FS::$svcdb";
1006 my $new_svc_x = $class->new({ $h_svc_x->hash });
1007 my $error = $new_svc_x->insert;
1008 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1019 Behaviour of changing the svcpart of cust_svc records is undefined and should
1020 possibly be prohibited, and pkg_svc records are not checked.
1022 pkg_svc records are not checked in general (here).
1024 Deleting this record doesn't check or delete the svc_* record associated
1027 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1028 a DBI database handle is not yet implemented.
1032 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1033 schema.html from the base documentation