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 or return "No svcpart ". $self->svcpart.
375 " services in pkgpart ". $cust_pkg->pkgpart;
377 my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
379 #false laziness w/cust_pkg->part_svc
380 my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
384 return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
385 " services for pkgnum ". $self->pkgnum
395 Returns the displayed service number for this service: agent_svcid if it has a
396 value, svcnum otherwise
402 $self->agent_svcid || $self->svcnum;
407 Returns the definition for this service, as a FS::part_svc object (see
415 ? $self->{'_svcpart'}
416 : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
421 Returns the package this service belongs to, as a FS::cust_pkg object (see
428 qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
433 Returns the pkg_svc record for for this service, if applicable.
439 my $cust_pkg = $self->cust_pkg;
440 return undef unless $cust_pkg;
442 qsearchs( 'pkg_svc', { 'svcpart' => $self->svcpart,
443 'pkgpart' => $cust_pkg->pkgpart,
450 Returns the date this service was inserted.
456 $self->h_date('insert');
459 =item pkg_cancel_date
461 Returns the date this service's package was canceled. This normally only
462 exists for a service that's been preserved through cancellation with the
463 part_pkg.preserve flag.
467 sub pkg_cancel_date {
469 my $cust_pkg = $self->cust_pkg or return;
470 return $cust_pkg->getfield('cancel') || '';
475 Returns a list consisting of:
476 - The name of this service (from part_svc)
477 - A meaningful identifier (username, domain, or mail alias)
478 - The table name (i.e. svc_domain) for this service
483 my($label, $value, $svcdb) = $cust_svc->label;
487 Like the B<label> method, except the second item in the list ("meaningful
488 identifier") may be longer - typically, a full name is included.
492 sub label { shift->_label('svc_label', @_); }
493 sub label_long { shift->_label('svc_label_long', @_); }
498 my $svc_x = $self->svc_x
499 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
501 $self->$method($svc_x);
504 sub svc_label { shift->_svc_label('label', @_); }
505 sub svc_label_long { shift->_svc_label('label_long', @_); }
508 my( $self, $method, $svc_x ) = ( shift, shift, shift );
511 $self->part_svc->svc,
513 $self->part_svc->svcdb,
521 Returns a listref of html elements associated with this service's exports.
527 my $svc_x = $self->svc_x
528 or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
530 $svc_x->export_links;
533 =item export_getsettings
535 Returns two hashrefs of settings associated with this service's exports.
539 sub export_getsettings {
541 my $svc_x = $self->svc_x
542 or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
544 $svc_x->export_getsettings;
550 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
551 FS::svc_domain object, etc.)
557 my $svcdb = $self->part_svc->svcdb;
558 if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
559 $self->{'_svc_acct'};
561 require "FS/$svcdb.pm";
562 warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
563 ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
565 qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
569 =item seconds_since TIMESTAMP
571 See L<FS::svc_acct/seconds_since>. Equivalent to
572 $cust_svc->svc_x->seconds_since, but more efficient. Meaningless for records
573 where B<svcdb> is not "svc_acct".
577 #internal session db deprecated (or at least on hold)
578 sub seconds_since { 'internal session db deprecated'; };
579 ##note: implementation here, POD in FS::svc_acct
581 # my($self, $since) = @_;
583 # my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
586 # AND logout IS NOT NULL'
587 # ) or die $dbh->errstr;
588 # $sth->execute($self->svcnum, $since) or die $sth->errstr;
589 # $sth->fetchrow_arrayref->[0];
592 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
594 See L<FS::svc_acct/seconds_since_sqlradacct>. Equivalent to
595 $cust_svc->svc_x->seconds_since_sqlradacct, but more efficient. Meaningless
596 for records where B<svcdb> is not "svc_acct".
600 #note: implementation here, POD in FS::svc_acct
601 sub seconds_since_sqlradacct {
602 my($self, $start, $end) = @_;
604 my $mes = "$me seconds_since_sqlradacct:";
606 my $svc_x = $self->svc_x;
608 my @part_export = $self->part_svc->part_export_usage;
609 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
610 " service definition"
615 foreach my $part_export ( @part_export ) {
617 next if $part_export->option('ignore_accounting');
619 warn "$mes connecting to sqlradius database\n"
622 my $dbh = DBI->connect( map { $part_export->option($_) }
623 qw(datasrc username password) )
624 or die "can't connect to sqlradius database: ". $DBI::errstr;
626 warn "$mes connected to sqlradius database\n"
629 #select a unix time conversion function based on database type
630 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
632 my $username = $part_export->export_username($svc_x);
636 warn "$mes finding closed sessions completely within the given range\n"
641 if ($part_export->option('process_single_realm')) {
642 $realm = 'AND Realm = ?';
643 $realmparam = $part_export->option('realm');
646 my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
650 AND $str2time AcctStartTime) >= ?
651 AND $str2time AcctStopTime ) < ?
652 AND $str2time AcctStopTime ) > 0
653 AND AcctStopTime IS NOT NULL"
654 ) or die $dbh->errstr;
655 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
657 my $regular = $sth->fetchrow_arrayref->[0];
659 warn "$mes finding open sessions which start in the range\n"
662 # count session start->range end
663 $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
667 AND $str2time AcctStartTime ) >= ?
668 AND $str2time AcctStartTime ) < ?
669 AND ( ? - $str2time AcctStartTime ) ) < 86400
670 AND ( $str2time AcctStopTime ) = 0
671 OR AcctStopTime IS NULL )";
672 $sth = $dbh->prepare($query) or die $dbh->errstr;
675 ($realm ? $realmparam : ()),
679 or die $sth->errstr. " executing query $query";
680 my $start_during = $sth->fetchrow_arrayref->[0];
682 warn "$mes finding closed sessions which start before the range but stop during\n"
685 #count range start->session end
686 $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
690 AND $str2time AcctStartTime ) < ?
691 AND $str2time AcctStopTime ) >= ?
692 AND $str2time AcctStopTime ) < ?
693 AND $str2time AcctStopTime ) > 0
694 AND AcctStopTime IS NOT NULL"
695 ) or die $dbh->errstr;
696 $sth->execute( $start,
698 ($realm ? $realmparam : ()),
703 my $end_during = $sth->fetchrow_arrayref->[0];
705 warn "$mes finding closed sessions which start before the range but stop after\n"
708 # count range start->range end
709 # don't count open sessions anymore (probably missing stop record)
710 $sth = $dbh->prepare("SELECT COUNT(*)
714 AND $str2time AcctStartTime ) < ?
715 AND ( $str2time AcctStopTime ) >= ?
717 # OR AcctStopTime = 0
718 # OR AcctStopTime IS NULL )"
719 ) or die $dbh->errstr;
720 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
722 my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
724 $seconds += $regular + $end_during + $start_during + $entire_range;
726 warn "$mes done finding sessions\n"
735 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
737 See L<FS::svc_acct/attribute_since_sqlradacct>. Equivalent to
738 $cust_svc->svc_x->attribute_since_sqlradacct, but more efficient. Meaningless
739 for records where B<svcdb> is not "svc_acct".
743 #note: implementation here, POD in FS::svc_acct
744 #(false laziness w/seconds_since_sqlradacct above)
745 sub attribute_since_sqlradacct {
746 my($self, $start, $end, $attrib) = @_;
748 my $mes = "$me attribute_since_sqlradacct:";
750 my $svc_x = $self->svc_x;
752 my @part_export = $self->part_svc->part_export_usage;
753 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
754 " service definition"
760 foreach my $part_export ( @part_export ) {
762 next if $part_export->option('ignore_accounting');
764 warn "$mes connecting to sqlradius database\n"
767 my $dbh = DBI->connect( map { $part_export->option($_) }
768 qw(datasrc username password) )
769 or die "can't connect to sqlradius database: ". $DBI::errstr;
771 warn "$mes connected to sqlradius database\n"
774 #select a unix time conversion function based on database type
775 my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
777 my $username = $part_export->export_username($svc_x);
779 warn "$mes SUMing $attrib sessions\n"
784 if ($part_export->option('process_single_realm')) {
785 $realm = 'AND Realm = ?';
786 $realmparam = $part_export->option('realm');
789 my $sth = $dbh->prepare("SELECT SUM($attrib)
793 AND $str2time AcctStopTime ) >= ?
794 AND $str2time AcctStopTime ) < ?
795 AND AcctStopTime IS NOT NULL"
796 ) or die $dbh->errstr;
797 $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
800 my $row = $sth->fetchrow_arrayref;
801 $sum += $row->[0] if defined($row->[0]);
803 warn "$mes done SUMing sessions\n"
812 =item get_session_history TIMESTAMP_START TIMESTAMP_END
814 See L<FS::svc_acct/get_session_history>. Equivalent to
815 $cust_svc->svc_x->get_session_history, but more efficient. Meaningless for
816 records where B<svcdb> is not "svc_acct".
820 sub get_session_history {
821 my($self, $start, $end, $attrib) = @_;
825 my @part_export = $self->part_svc->part_export_usage;
826 die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
827 " service definition"
833 foreach my $part_export ( @part_export ) {
835 @{ $part_export->usage_sessions( $start, $end, $self->svc_x ) };
844 Returns an array of hashes representing the tickets linked to this service.
851 my $conf = FS::Conf->new;
852 my $num = $conf->config('cust_main-max_tickets') || 10;
855 if ( $conf->config('ticket_system') ) {
856 unless ( $conf->config('ticket_system-custom_priority_field') ) {
858 @tickets = @{ FS::TicketSystem->service_tickets($self->svcnum, $num) };
862 foreach my $priority (
863 $conf->config('ticket_system-custom_priority_field-values'), ''
865 last if scalar(@tickets) >= $num;
867 @{ FS::TicketSystem->service_tickets( $self->svcnum,
868 $num - scalar(@tickets),
885 =item smart_search OPTION => VALUE ...
887 Accepts the option I<search>, the string to search for. The string will
888 be searched for as a username, email address, IP address, MAC address,
889 phone number, and hardware serial number. Unlike the I<smart_search> on
890 customers, this always requires an exact match.
894 # though perhaps it should be fuzzy in some cases?
897 my %param = __PACKAGE__->smart_search_param(@_);
901 sub smart_search_param {
905 my $string = $opt{'search'};
906 $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
909 map { my $table = $_;
910 my $search_sql = "FS::$table"->search_sql($string);
912 "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
913 "FROM $table WHERE $search_sql";
915 FS::part_svc->svc_tables;
917 if ( $string =~ /^(\d+)$/ ) {
918 unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
921 my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
922 " ON (svc_all.svcnum = cust_svc.svcnum) ";
926 push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
927 'null_right' => 'View/link unlinked services'
929 my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
931 $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
932 FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
933 ' LEFT JOIN part_svc USING ( svcpart )';
936 'table' => 'cust_svc',
937 'select' => 'svc_all.svcnum AS svcnum, '.
938 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
940 'addl_from' => $addl_from,
942 'extra_sql' => $extra_sql,
949 # fix missing (deleted by mistake) svc_x records
950 warn "searching for missing svc_x records...\n";
952 'table' => 'cust_svc',
953 'select' => 'cust_svc.*',
954 'addl_from' => ' LEFT JOIN ( ' .
956 map { "SELECT svcnum FROM $_" }
957 FS::part_svc->svc_tables
958 ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
959 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
961 my @svcs = qsearch(\%search);
962 warn "found ".scalar(@svcs)."\n";
964 local $FS::Record::nowarn_classload = 1; # for h_svc_
965 local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
968 'hashref' => { history_action => 'delete' },
969 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
971 foreach my $cust_svc (@svcs) {
972 my $svcnum = $cust_svc->svcnum;
973 my $svcdb = $cust_svc->part_svc->svcdb;
974 $h_search{'hashref'}{'svcnum'} = $svcnum;
975 $h_search{'table'} = "h_$svcdb";
976 my $h_svc_x = qsearchs(\%h_search)
978 my $class = "FS::$svcdb";
979 my $new_svc_x = $class->new({ $h_svc_x->hash });
980 my $error = $new_svc_x->insert;
981 warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
992 Behaviour of changing the svcpart of cust_svc records is undefined and should
993 possibly be prohibited, and pkg_svc records are not checked.
995 pkg_svc records are not checked in general (here).
997 Deleting this record doesn't check or delete the svc_* record associated
1000 In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
1001 a DBI database handle is not yet implemented.
1005 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>,
1006 schema.html from the base documentation