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 );
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,
326 $dbh->rollback if $oldAutoCommit;
327 return $error if $error;
331 #my $error = $new->SUPER::replace($old, @_);
332 my $error = $new->SUPER::replace($old);
334 $dbh->rollback if $oldAutoCommit;
335 return $error if $error;
338 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
345 Checks all fields to make sure this is a valid service. If there is an error,
346 returns the error, otherwise returns false. Called by the insert and
355 $self->ut_numbern('svcnum')
356 || $self->ut_numbern('pkgnum')
357 || $self->ut_number('svcpart')
358 || $self->ut_numbern('agent_svcid')
359 || $self->ut_numbern('overlimit')
361 return $error if $error;
363 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
364 return "Unknown svcpart" unless $part_svc;
366 if ( $self->pkgnum && ! $ignore_quantity ) {
368 #slightly inefficient since ->pkg_svc will also look it up, but fixing
369 # a much larger perf problem and have bigger fish to fry
370 my $cust_pkg = $self->cust_pkg;
372 my $pkg_svc = $self->pkg_svc
373 || new FS::pkg_svc { 'svcpart' => $self->svcpart,
374 'pkgpart' => $cust_pkg->pkgpart,
378 #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
379 foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
380 my $addon_pkg_svc = qsearchs('pkg_svc', {
381 pkgpart => $part_pkg_link->dst_pkgpart,
382 svcpart => $self->svcpart,
384 $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
388 #better error message? UI shouldn't get here
389 return "No svcpart ". $self->svcpart.
390 " services in pkgpart ". $cust_pkg->pkgpart
391 unless $pkg_svc->quantity > 0;
393 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
395 #false laziness w/cust_pkg->part_svc
396 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
400 #better error message? again, UI shouldn't get here
401 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
402 " services for pkgnum ". $self->pkgnum
412 Returns the displayed service number for this service: agent_svcid if it has a
413 value, svcnum otherwise
419 $self->agent_svcid || $self->svcnum;
424 Returns the definition for this service, as a FS::part_svc object (see
432 ? $self->{'_svcpart'}
433 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
438 Returns the package this service belongs to, as a FS::cust_pkg object (see
443 Returns the pkg_svc record for for this service, if applicable.
449 my $cust_pkg = $self->cust_pkg;
450 return undef unless $cust_pkg;
452 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
453 'pkgpart' => $cust_pkg->pkgpart,
460 Returns the date this service was inserted.
466 $self->h_date('insert');
469 =item pkg_cancel_date
471 Returns the date this service's package was canceled. This normally only
472 exists for a service that's been preserved through cancellation with the
473 part_pkg.preserve flag.
477 sub pkg_cancel_date {
479 my $cust_pkg = $self->cust_pkg or return;
480 return $cust_pkg->getfield('cancel') || '';
485 Returns a list consisting of:
486 - The name of this service (from part_svc)
487 - A meaningful identifier (username, domain, or mail alias)
488 - The table name (i.e. svc_domain) for this service
493 my($label, $value, $svcdb) = $cust_svc->label;
497 Like the B<label> method, except the second item in the list ("meaningful
498 identifier") may be longer - typically, a full name is included.
502 sub label { shift->_label('svc_label', @_); }
503 sub label_long { shift->_label('svc_label_long', @_); }
508 my $svc_x = $self->svc_x
509 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
511 $self->$method($svc_x);
514 sub svc_label { shift->_svc_label('label', @_); }
515 sub svc_label_long { shift->_svc_label('label_long', @_); }
518 my( $self, $method, $svc_x ) = ( shift, shift, shift );
521 $self->part_svc->svc,
523 $self->part_svc->svcdb,
531 Returns a listref of html elements associated with this service's exports.
537 my $svc_x = $self->svc_x
538 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
540 $svc_x->export_links;
543 =item export_getsettings
545 Returns two hashrefs of settings associated with this service's exports.
549 sub export_getsettings {
551 my $svc_x = $self->svc_x
552 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
554 $svc_x->export_getsettings;
560 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
561 FS::svc_domain object, etc.)
567 my $svcdb = $self->part_svc->svcdb;
568 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
569 $self->{'_svc_acct'};
571 require "FS/$svcdb.pm";
572 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
573 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
575 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
579 =item seconds_since TIMESTAMP
581 See L<FS::svc_acct/seconds_since>. Equivalent to
582 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
583 where B<svcdb> is not "svc_acct".
587 #internal session db deprecated (or at least on hold)
588 sub seconds_since { 'internal session db deprecated'; };
589 ##note: implementation here, POD in FS::svc_acct
591 # my($self, $since) = @_;
593 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
596 # AND logout IS NOT NULL'
597 # ) or die $dbh->errstr;
598 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
599 # $sth->fetchrow_arrayref->[0];
602 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
604 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
605 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
606 for records where B<svcdb> is not "svc_acct".
610 #note: implementation here, POD in FS::svc_acct
611 sub seconds_since_sqlradacct {
612 my($self, $start, $end) = @_;
614 my $mes = "$me seconds_since_sqlradacct:";
616 my $svc_x = $self->svc_x;
618 my @part_export = $self->part_svc->part_export_usage;
619 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
620 " service definition"
625 foreach my $part_export ( @part_export ) {
627 next if $part_export->option('ignore_accounting');
629 warn "$mes connecting to sqlradius database\n"
632 my $dbh = DBI->connect( map { $part_export->option($_) }
633 qw(datasrc username password) )
634 or die "can't connect to sqlradius database: ". $DBI::errstr;
636 warn "$mes connected to sqlradius database\n"
639 #select a unix time conversion function based on database type
640 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
642 my $username = $part_export->export_username($svc_x);
646 warn "$mes finding closed sessions completely within the given range\n"
651 if ($part_export->option('process_single_realm')) {
652 $realm = 'AND Realm = ?';
653 $realmparam = $part_export->option('realm');
656 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
660 AND $str2time AcctStartTime) >= ?
661 AND $str2time AcctStopTime ) < ?
662 AND $str2time AcctStopTime ) > 0
663 AND AcctStopTime IS NOT NULL"
664 ) or die $dbh->errstr;
665 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
667 my $regular = $sth->fetchrow_arrayref->[0];
669 warn "$mes finding open sessions which start in the range\n"
672 # count session start->range end
673 $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
677 AND $str2time AcctStartTime ) >= ?
678 AND $str2time AcctStartTime ) < ?
679 AND ( ? - $str2time AcctStartTime ) ) < 86400
680 AND ( $str2time AcctStopTime ) = 0
681 OR AcctStopTime IS NULL )";
682 $sth = $dbh->prepare($query) or die $dbh->errstr;
685 ($realm ? $realmparam : ()),
689 or die $sth->errstr. " executing query $query";
690 my $start_during = $sth->fetchrow_arrayref->[0];
692 warn "$mes finding closed sessions which start before the range but stop during\n"
695 #count range start->session end
696 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
700 AND $str2time AcctStartTime ) < ?
701 AND $str2time AcctStopTime ) >= ?
702 AND $str2time AcctStopTime ) < ?
703 AND $str2time AcctStopTime ) > 0
704 AND AcctStopTime IS NOT NULL"
705 ) or die $dbh->errstr;
706 $sth->execute( $start,
708 ($realm ? $realmparam : ()),
713 my $end_during = $sth->fetchrow_arrayref->[0];
715 warn "$mes finding closed sessions which start before the range but stop after\n"
718 # count range start->range end
719 # don't count open sessions anymore (probably missing stop record)
720 $sth = $dbh->prepare("SELECT COUNT(*)
724 AND $str2time AcctStartTime ) < ?
725 AND ( $str2time AcctStopTime ) >= ?
727 # OR AcctStopTime = 0
728 # OR AcctStopTime IS NULL )"
729 ) or die $dbh->errstr;
730 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
732 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
734 $seconds += $regular + $end_during + $start_during + $entire_range;
736 warn "$mes done finding sessions\n"
745 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
747 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
748 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
749 for records where B<svcdb> is not "svc_acct".
753 #note: implementation here, POD in FS::svc_acct
754 #(false laziness w/seconds_since_sqlradacct above)
755 sub attribute_since_sqlradacct {
756 my($self, $start, $end, $attrib) = @_;
758 my $mes = "$me attribute_since_sqlradacct:";
760 my $svc_x = $self->svc_x;
762 my @part_export = $self->part_svc->part_export_usage;
763 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
764 " service definition"
770 foreach my $part_export ( @part_export ) {
772 next if $part_export->option('ignore_accounting');
774 warn "$mes connecting to sqlradius database\n"
777 my $dbh = DBI->connect( map { $part_export->option($_) }
778 qw(datasrc username password) )
779 or die "can't connect to sqlradius database: ". $DBI::errstr;
781 warn "$mes connected to sqlradius database\n"
784 #select a unix time conversion function based on database type
785 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
787 my $username = $part_export->export_username($svc_x);
789 warn "$mes SUMing $attrib sessions\n"
794 if ($part_export->option('process_single_realm')) {
795 $realm = 'AND Realm = ?';
796 $realmparam = $part_export->option('realm');
799 my $sth = $dbh->prepare("SELECT SUM($attrib)
803 AND $str2time AcctStopTime ) >= ?
804 AND $str2time AcctStopTime ) < ?
805 AND AcctStopTime IS NOT NULL"
806 ) or die $dbh->errstr;
807 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
810 my $row = $sth->fetchrow_arrayref;
811 $sum += $row->[0] if defined($row->[0]);
813 warn "$mes done SUMing sessions\n"
822 =item get_session_history TIMESTAMP_START TIMESTAMP_END
824 See L<FS::svc_acct/get_session_history>. Equivalent to
825 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
826 records where B<svcdb> is not "svc_acct".
830 sub get_session_history {
831 my($self, $start, $end, $attrib) = @_;
835 my @part_export = $self->part_svc->part_export_usage;
836 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
837 " service definition"
843 foreach my $part_export ( @part_export ) {
845 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
852 =item tickets [ STATUS ]
854 Returns an array of hashes representing the tickets linked to this service.
856 An optional status (or arrayref or hashref of statuses) may be specified.
862 my $status = ( @_ && $_[0] ) ? shift : '';
864 my $conf = FS::Conf->new;
865 my $num = $conf->config('cust_main-max_tickets') || 10;
868 if ( $conf->config('ticket_system') ) {
869 unless ( $conf->config('ticket_system-custom_priority_field') ) {
871 @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
880 foreach my $priority (
881 $conf->config('ticket_system-custom_priority_field-values'), ''
883 last if scalar(@tickets) >= $num;
885 @{ FS::TicketSystem->service_tickets( $self->svcnum,
886 $num - scalar(@tickets),
904 =item smart_search OPTION => VALUE ...
906 Accepts the option I<search>, the string to search for. The string will
907 be searched for as a username, email address, IP address, MAC address,
908 phone number, and hardware serial number. Unlike the I<smart_search> on
909 customers, this always requires an exact match.
913 # though perhaps it should be fuzzy in some cases?
916 my %param = __PACKAGE__->smart_search_param(@_);
920 sub smart_search_param {
924 my $string = $opt{'search'};
925 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
928 map { my $table = $_;
929 my $search_sql = "FS::$table"->search_sql($string);
931 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
932 "FROM $table WHERE $search_sql";
934 FS::part_svc->svc_tables;
936 if ( $string =~ /^(\d+)$/ ) {
937 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
940 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
941 " ON (svc_all.svcnum = cust_svc.svcnum) ";
945 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
946 'null_right' => 'View/link unlinked services'
948 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
950 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
951 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
952 ' LEFT JOIN part_svc USING ( svcpart )';
955 'table' => 'cust_svc',
956 'select' => 'svc_all.svcnum AS svcnum, '.
957 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
959 'addl_from' => $addl_from,
961 'extra_sql' => $extra_sql,
968 # fix missing (deleted by mistake) svc_x records
969 warn "searching for missing svc_x records...\n";
971 'table' => 'cust_svc',
972 'select' => 'cust_svc.*',
973 'addl_from' => ' LEFT JOIN ( ' .
975 map { "SELECT svcnum FROM $_" }
976 FS::part_svc->svc_tables
977 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
978 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
980 my @svcs = qsearch(\%search);
981 warn "found ".scalar(@svcs)."\n";
983 local $FS::Record::nowarn_classload = 1; # for h_svc_
984 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
987 'hashref' => { history_action => 'delete' },
988 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
990 foreach my $cust_svc (@svcs) {
991 my $svcnum = $cust_svc->svcnum;
992 my $svcdb = $cust_svc->part_svc->svcdb;
993 $h_search{'hashref'}{'svcnum'} = $svcnum;
994 $h_search{'table'} = "h_$svcdb";
995 my $h_svc_x = qsearchs(\%h_search)
997 my $class = "FS::$svcdb";
998 my $new_svc_x = $class->new({ $h_svc_x->hash });
999 my $error = $new_svc_x->insert;
1000 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
1011 Behaviour of changing the svcpart of cust_svc records is undefined and should
1012 possibly be prohibited, and pkg_svc records are not checked.
1014 pkg_svc records are not checked in general (here).
1016 Deleting this record doesn't check or delete the svc_* record associated
1019 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1020 a DBI database handle is not yet implemented.
1024 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1025 schema.html from the base documentation