1 package FS::part_export;
2 use base qw( FS::option_Common FS::m2m_Common );
5 use vars qw( @ISA @EXPORT_OK $DEBUG %exports );
8 use FS::Record qw( qsearch qsearchs dbh );
10 use FS::part_export_option;
11 use FS::part_export_machine;
12 use FS::svc_export_machine;
13 use FS::export_cust_svc;
15 #for export modules, though they should probably just use it themselves
18 @EXPORT_OK = qw(export_info);
24 FS::part_export - Object methods for part_export records
30 $record = new FS::part_export \%hash;
31 $record = new FS::part_export { 'column' => 'value' };
33 #($new_record, $options) = $template_recored->clone( $svcpart );
35 $error = $record->insert( { 'option' => 'value' } );
36 $error = $record->insert( \%options );
38 $error = $new_record->replace($old_record);
40 $error = $record->delete;
42 $error = $record->check;
46 An FS::part_export object represents an export of Freeside data to an external
47 provisioning system. FS::part_export inherits from FS::Record. The following
48 fields are currently supported:
52 =item exportnum - primary key
54 =item exportname - Descriptive name
56 =item machine - Machine name
58 =item exporttype - Export type
60 =item nodomain - blank or "Y" : usernames are exported to this service with no domain
62 =item default_machine - For exports that require a machine to be selected for
63 each service (see L<FS::svc_export_machine>), the one to use as the default.
65 =item no_suspend - Don't export service suspensions. In the future there may
66 be "no_*" options for the other service actions.
76 Creates a new export. To add the export to the database, see L<"insert">.
78 Note that this stores the hash reference, not a distinct copy of the hash it
79 points to. You can ask the object for a copy with the I<hash> method.
83 # the new method can be inherited from FS::Record, if a table method is defined
85 sub table { 'part_export'; }
91 #An alternate constructor. Creates a new export by duplicating an existing
92 #export. The given svcpart is assigned to the new export.
94 #Returns a list consisting of the new export object and a hashref of options.
100 # my $class = ref($self);
101 # my %hash = $self->hash;
102 # $hash{'exportnum'} = '';
103 # $hash{'svcpart'} = shift;
104 # ( $class->new( \%hash ),
105 # { map { $_->optionname => $_->optionvalue }
106 # qsearch('part_export_option', { 'exportnum' => $self->exportnum } )
113 Adds this record to the database. If there is an error, returns the error,
114 otherwise returns false.
116 If a hash reference of options is supplied, part_export_option records are
117 created (see L<FS::part_export_option>).
124 local $SIG{HUP} = 'IGNORE';
125 local $SIG{INT} = 'IGNORE';
126 local $SIG{QUIT} = 'IGNORE';
127 local $SIG{TERM} = 'IGNORE';
128 local $SIG{TSTP} = 'IGNORE';
129 local $SIG{PIPE} = 'IGNORE';
130 my $oldAutoCommit = $FS::UID::AutoCommit;
131 local $FS::UID::AutoCommit = 0;
134 my $error = $self->SUPER::insert(@_)
136 # use replace to do all the part_export_machine and default_machine stuff
138 $dbh->rollback if $oldAutoCommit;
142 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
148 Delete this record from the database.
152 #foreign keys would make this much less tedious... grr dumb mysql
156 local $SIG{HUP} = 'IGNORE';
157 local $SIG{INT} = 'IGNORE';
158 local $SIG{QUIT} = 'IGNORE';
159 local $SIG{TERM} = 'IGNORE';
160 local $SIG{TSTP} = 'IGNORE';
161 local $SIG{PIPE} = 'IGNORE';
162 my $oldAutoCommit = $FS::UID::AutoCommit;
163 local $FS::UID::AutoCommit = 0;
166 # delete associated export_cust_svc
167 foreach my $export_cust_svc (
168 qsearch('export_cust_svc',{ 'exportnum' => $self->exportnum })
170 my $error = $export_cust_svc->delete;
172 $dbh->rollback if $oldAutoCommit;
177 # clean up export_nas records
178 my $error = $self->process_m2m(
179 'link_table' => 'export_nas',
180 'target_table' => 'nas',
182 ) || $self->process_m2m(
183 'link_table' => 'export_svc',
184 'target_table' => 'part_svc',
186 ) || $self->SUPER::delete;
188 $dbh->rollback if $oldAutoCommit;
192 foreach my $export_svc ( $self->export_svc ) {
193 my $error = $export_svc->delete;
195 $dbh->rollback if $oldAutoCommit;
200 foreach my $part_export_machine ( $self->part_export_machine ) {
201 my $error = $part_export_machine->delete;
203 $dbh->rollback if $oldAutoCommit;
208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 =item replace [ OLD_RECORD ] [ HASHREF | OPTION => VALUE ... ]
214 Replaces the OLD_RECORD with this one in the database. If there is an error,
215 returns the error, otherwise returns false.
217 If a list or hash reference of options is supplied, option records are created
224 my $old = $self->replace_old;
226 local $SIG{HUP} = 'IGNORE';
227 local $SIG{INT} = 'IGNORE';
228 local $SIG{QUIT} = 'IGNORE';
229 local $SIG{TERM} = 'IGNORE';
230 local $SIG{TSTP} = 'IGNORE';
231 local $SIG{PIPE} = 'IGNORE';
233 my $oldAutoCommit = $FS::UID::AutoCommit;
234 local $FS::UID::AutoCommit = 0;
238 if ( $self->part_export_machine_textarea ) {
240 my %part_export_machine = map { $_->machine => $_ }
241 $self->part_export_machine;
243 my @machines = map { $_ =~ s/^\s+//; $_ =~ s/\s+$//; $_ }
246 $self->part_export_machine_textarea;
248 foreach my $machine ( @machines ) {
250 if ( $part_export_machine{$machine} ) {
252 if ( $part_export_machine{$machine}->disabled eq 'Y' ) {
253 $part_export_machine{$machine}->disabled('');
254 $error = $part_export_machine{$machine}->replace;
256 $dbh->rollback if $oldAutoCommit;
261 if ( $self->default_machine_name eq $machine ) {
262 $self->default_machine( $part_export_machine{$machine}->machinenum );
265 delete $part_export_machine{$machine}; #so we don't disable it below
269 my $part_export_machine = new FS::part_export_machine {
270 'exportnum' => $self->exportnum,
271 'machine' => $machine
273 $error = $part_export_machine->insert;
275 $dbh->rollback if $oldAutoCommit;
279 if ( $self->default_machine_name eq $machine ) {
280 $self->default_machine( $part_export_machine->machinenum );
286 foreach my $part_export_machine ( values %part_export_machine ) {
287 $part_export_machine->disabled('Y');
288 $error = $part_export_machine->replace;
290 $dbh->rollback if $oldAutoCommit;
295 if ( $old->machine ne '_SVC_MACHINE' ) {
296 # then set up the default for any already-attached export_svcs
297 foreach my $export_svc ( $self->export_svc ) {
298 my @svcs = qsearch('cust_svc', { 'svcpart' => $export_svc->svcpart });
299 foreach my $cust_svc ( @svcs ) {
300 my $svc_export_machine = FS::svc_export_machine->new({
301 'exportnum' => $self->exportnum,
302 'svcnum' => $cust_svc->svcnum,
303 'machinenum' => $self->default_machine,
305 $error ||= $svc_export_machine->insert;
309 $dbh->rollback if $oldAutoCommit;
312 } # if switching to selectable hosts
314 } elsif ( $old->machine eq '_SVC_MACHINE' ) {
315 # then we're switching from selectable to non-selectable
316 foreach my $svc_export_machine (
317 qsearch('svc_export_machine', { 'exportnum' => $self->exportnum })
319 $error ||= $svc_export_machine->delete;
322 $dbh->rollback if $oldAutoCommit;
328 $error = $self->SUPER::replace(@_);
330 $dbh->rollback if $oldAutoCommit;
334 if ( $self->machine eq '_SVC_MACHINE' and ! $self->default_machine ) {
335 $dbh->rollback if $oldAutoCommit;
336 return "no default export host selected";
339 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
345 Checks all fields to make sure this is a valid export. If there is
346 an error, returns the error, otherwise returns false. Called by the insert
354 $self->ut_numbern('exportnum')
355 || $self->ut_textn('exportname')
356 || $self->ut_domainn('machine')
357 || $self->ut_alpha('exporttype')
358 || $self->ut_flag('no_suspend')
361 if ( $self->machine eq '_SVC_MACHINE' ) {
362 $error ||= $self->ut_numbern('default_machine')
364 $self->set('default_machine', '');
367 return $error if $error;
369 $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
372 $self->deprecated(1); #BLAH
381 Returns a label for this export, "exportname||exportype (machine)".
387 ($self->exportname || $self->exporttype ). ' ('. $self->machine. ')';
392 Returns a label for this export, "exportname: exporttype to machine".
399 my $label = $self->exportname
400 ? '<B>'. $self->exportname. '</B>: ' #<BR>'.
403 $label .= $self->exporttype;
405 $label .= ' to '. ( $self->machine eq '_SVC_MACHINE'
406 ? 'per-service hostname'
417 #Returns the service definition (see L<FS::part_svc>) for this export.
423 # qsearchs('part_svc', { svcpart => $self->svcpart } );
428 croak "FS::part_export::part_svc deprecated";
429 #confess "FS::part_export::part_svc deprecated";
434 Returns a list of associated FS::svc_* records.
440 map { $_->svc_x } $self->cust_svc;
445 Returns a list of associated FS::cust_svc records.
451 map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
452 grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
456 =item part_export_machine
458 Returns all machines as FS::part_export_machine objects (see
459 L<FS::part_export_machine>).
463 sub part_export_machine {
465 map { $_ } #behavior of sort undefined in scalar context
466 sort { $a->machine cmp $b->machine }
467 qsearch('part_export_machine', { 'exportnum' => $self->exportnum } );
472 Returns a list of associated FS::export_svc records.
476 Returns a list of associated FS::export_device records.
478 =item part_export_option
480 Returns all options as FS::part_export_option objects (see
481 L<FS::part_export_option>).
485 sub part_export_option {
487 $self->option_objects;
492 Returns a list of option names and values suitable for assigning to a hash.
494 =item option OPTIONNAME
496 Returns the option value for the given name, or the empty string.
500 Reblesses the object into the FS::part_export::EXPORTTYPE class, where
501 EXPORTTYPE is the object's I<exporttype> field. There should be better docs
502 on how to create new exports, but until then, see L</NEW EXPORT CLASSES>.
508 my $exporttype = $self->exporttype;
509 my $class = ref($self). "::$exporttype";
512 bless($self, $class) unless $@;
516 =item svc_machine SVC_X
518 Return the export hostname for SVC_X.
523 my( $self, $svc_x ) = @_;
525 return $self->machine unless $self->machine eq '_SVC_MACHINE';
527 my $svc_export_machine = qsearchs('svc_export_machine', {
528 'svcnum' => $svc_x->svcnum,
529 'exportnum' => $self->exportnum,
532 if (!$svc_export_machine) {
533 warn "No hostname selected for ".($self->exportname || $self->exporttype);
534 return $self->default_export_machine->machine;
537 return $svc_export_machine->part_export_machine->machine;
540 =item default_export_machine
542 Return the default export hostname for this export.
546 sub default_export_machine {
548 my $machinenum = $self->default_machine;
550 my $default_machine = FS::part_export_machine->by_key($machinenum);
551 return $default_machine->machine if $default_machine;
553 # this should not happen
554 die "no default export hostname for export ".$self->exportnum;
557 =item export_insert SVC_OBJECT
561 # Do not overload! Overload _export_insert instead
566 if ( $FS::svc_Common::noexport_hack ) {
567 carp "export_insert() suppressed by noexport_hack" if $DEBUG;
570 $self->_export_insert(@_);
576 # my $method = $AUTOLOAD;
577 # #$method =~ s/::(\w+)$/::_$1/; #infinite loop prevention
578 # $method =~ s/::(\w+)$/_$1/; #infinite loop prevention
579 # $self->$method(@_);
582 =item export_replace NEW OLD
586 # Do not overload! Overload _export_replace instead
591 if ( $FS::svc_Common::noexport_hack ) {
592 carp "export_replace() suppressed by noexport_hack" if $DEBUG;
595 $self->_export_replace(@_);
602 # Do not overload! Overload _export_delete instead
607 if ( $FS::svc_Common::noexport_hack ) {
608 carp "export_delete() suppressed by noexport_hack" if $DEBUG;
611 $self->_export_delete(@_);
618 # Do not overload! Overload _export_suspend instead
623 if ( $FS::svc_Common::noexport_hack ) {
624 carp "export_suspend() suppressed by noexport_hack" if $DEBUG;
627 $self->_export_suspend(@_);
630 =item export_unsuspend
634 # Do not overload! Overload _export_unsuspend instead
636 sub export_unsuspend {
639 if ( $FS::svc_Common::noexport_hack ) {
640 carp "export_unsuspend() suppressed by noexport_hack" if $DEBUG;
643 $self->_export_unsuspend(@_);
646 #fallbacks providing useful error messages intead of infinite loops
649 return "_export_insert: unknown export type ". $self->exporttype;
652 sub _export_replace {
654 return "_export_replace: unknown export type ". $self->exporttype;
659 return "_export_delete: unknown export type ". $self->exporttype;
662 #call svcdb-specific fallbacks
664 sub _export_suspend {
666 #warn "warning: _export_suspened unimplemented for". ref($self);
668 my $new = $svc_x->clone_suspended;
669 $self->_export_replace( $new, $svc_x );
672 sub _export_unsuspend {
674 #warn "warning: _export_unsuspend unimplemented for ". ref($self);
676 my $old = $svc_x->clone_kludge_unsuspend;
677 $self->_export_replace( $svc_x, $old );
680 =item get_remoteid SVC
682 Returns the remote id for this export for the given service.
687 my ($self, $svc_x) = @_;
689 my $export_cust_svc = qsearchs('export_cust_svc',{
690 'exportnum' => $self->exportnum,
691 'svcnum' => $svc_x->svcnum
694 return $export_cust_svc ? $export_cust_svc->remoteid : '';
697 =item set_remoteid SVC VALUE
699 Sets the remote id for this export for the given service.
700 See L<FS::export_cust_svc>.
702 If value is true, inserts or updates export_cust_svc record.
703 If value is false, deletes any existing record.
705 Returns error message, blank on success.
710 my ($self, $svc_x, $value) = @_;
712 my $export_cust_svc = qsearchs('export_cust_svc',{
713 'exportnum' => $self->exportnum,
714 'svcnum' => $svc_x->svcnum
717 local $SIG{HUP} = 'IGNORE';
718 local $SIG{INT} = 'IGNORE';
719 local $SIG{QUIT} = 'IGNORE';
720 local $SIG{TERM} = 'IGNORE';
721 local $SIG{TSTP} = 'IGNORE';
722 local $SIG{PIPE} = 'IGNORE';
724 my $oldAutoCommit = $FS::UID::AutoCommit;
725 local $FS::UID::AutoCommit = 0;
730 if ($export_cust_svc) {
731 $export_cust_svc->set('remoteid',$value);
732 $error = $export_cust_svc->replace;
734 $export_cust_svc = new FS::export_cust_svc {
735 'exportnum' => $self->exportnum,
736 'svcnum' => $svc_x->svcnum,
739 $error = $export_cust_svc->insert;
742 if ($export_cust_svc) {
743 $error = $export_cust_svc->delete;
744 } #otherwise, it already doesn't exist
747 if ($oldAutoCommit) {
748 $dbh->rollback if $error;
749 $dbh->commit unless $error;
755 =item export_links SVC_OBJECT ARRAYREF
757 Adds a list of web elements to ARRAYREF specific to this export and SVC_OBJECT.
758 The elements are displayed in the UI to lead the the operator to external
759 configuration, monitoring, and similar tools.
761 =item export_getsettings SVC_OBJECT SETTINGS_HASHREF DEFAUTS_HASHREF
763 Adds a hashref of settings to SETTINGSREF specific to this export and
764 SVC_OBJECT. The elements can be displayed in the UI on the service view.
766 DEFAULTSREF is a hashref with the same keys where true values indicate the
767 setting is a default (and thus can be displayed in the UI with less emphasis,
768 or hidden by default).
772 Adds one or more "action" links to the export's display in
773 browse/part_export.cgi. Should return pairs of values. The first is
774 the link label; the second is the Mason path to a document to load.
775 The document will show in a popup.
785 Returns the 'weight' element from the export's %info hash, or 0 if there is
792 export_info()->{$self->exporttype}->{'weight'} || 0;
797 Returns a reference to (a copy of) the export's %info hash.
804 %{ export_info()->{$self->exporttype} }
808 =item get_dids SELECTION
810 Does several things, which is unfortunate. DID phone numbers are organized
811 in a sort-of hierarchy: state, areacode, exchange, number. Or, for some
812 vendors: state, region, number. But not always that, either.
814 SELECTION is one or more field/value pairs specifying parts of the hierarchy
815 that have already been selected. C<get_dids> will then return an arrayref of
816 the possible values for the next selection level. Note that these are not
817 actual DIDs except at the lowest level.
819 Generally, 'state' alone will return an array of area codes or region names
822 'state' and 'areacode' together will return an array of either:
823 - exchange strings of the form "New York (212-555-XXXX)"
824 - ratecenter names of the form "New York, NY"
826 These strings are sent back to the UI and offered as options so that the user
827 can choose the local calling area they like.
829 'areacode' and 'exchange', or 'state' and 'ratecenter', or 'region' by itself
830 will return an array of actual DID numbers.
832 Passing 'tollfree' with a true value will override the whole hierarchy and
833 return an array of tollfree numbers.
835 C<get_dids> methods should report errors via die().
839 # no stub; can('get_dids') should return false by default
841 #default fallbacks... FS::part_export::DID_Common ?
842 sub can_get_dids { 0; }
843 sub get_dids_can_tollfree { 0; }
844 sub get_dids_can_manual { 0; }
845 sub get_dids_can_edit { 0; } #don't use without can_manual, otherwise the
846 # DID selector provisions a new number from
847 # inventory each edit
848 sub get_dids_npa_select { 1; }
850 # get_dids_npa_select: if true, then prompt to select state, then area code,
851 # then city/exchange, then phone number.
852 # if false, then prompt to select state (actually province), then "region",
855 # get_dids_can_manual: if true, then there will be a radio button to enter
856 # a phone number manually.
858 # get_dids_can_tollfree: if true, then the user will be prompted to choose
859 # both a regular and a toll-free number. The export can have a
860 # 'restrict_selection' option to enable only one or the other of those. See
861 # part_export/vitelity.pm for an example.
863 # get_dids_can_edit: if true, then the user can use the selector again to
864 # change the phone number for a service. if false, then they can't (have to
865 # reprovision completely).
869 Returns the role that SVC occupies with respect to this export, if any.
870 This is part of the part_svc's export configuration.
877 my $cust_svc = $svc_x->cust_svc or return '';
878 my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum,
879 svcpart => $cust_svc->svcpart })
884 =item svc_with_role { SVC | PKGNUM }, ROLE
886 Given a svc_* object SVC or pkgnum PKG, and a role name ROLE, finds the
887 service(s) in the same package that are linked to this export with ROLE.
893 my $svc_or_pkgnum = shift;
896 if ( ref $svc_or_pkgnum ) {
897 $pkgnum = $svc_or_pkgnum->cust_svc->pkgnum or return '';
899 $pkgnum = $svc_or_pkgnum;
901 my $role_info = $self->info->{roles}->{$role}
902 or die "role '$role' does not exist for export '".$self->exporttype."'\n";
903 my $svcdb = $role_info->{svcdb};
907 'addl_from' => ' JOIN cust_svc USING (svcnum)' .
908 ' JOIN export_svc USING (svcpart)',
909 'extra_sql' => " WHERE cust_svc.pkgnum = $pkgnum" .
910 " AND export_svc.exportnum = ".$self->exportnum .
911 " AND export_svc.role = '$role'",
913 if ( $role_info->{multiple} ) {
917 warn "multiple $role services in pkgnum $pkgnum; returning the first one.\n";
929 =item export_info [ SVCDB ]
931 Returns a hash reference of the exports for the given I<svcdb>, or if no
932 I<svcdb> is specified, for all exports. The keys of the hash are
933 I<exporttype>s and the values are again hash references containing information
936 'desc' => 'Description',
938 'option' => { label=>'Option Label' },
939 'option2' => { label=>'Another label' },
941 'nodomain' => 'Y', #or ''
942 'notes' => 'Additional notes',
948 return $exports{$_[0]} || {} if @_;
949 #{ map { %{$exports{$_}} } keys %exports };
950 my $r = { map { %{$exports{$_}} } keys %exports };
954 sub _upgrade_data { #class method
955 my ($class, %opts) = @_;
957 my @part_export_option = qsearch('part_export_option', { 'optionname' => 'overlimit_groups' });
958 foreach my $opt ( @part_export_option ) {
959 next if $opt->optionvalue =~ /^[\d\s]+$/ || !$opt->optionvalue;
960 my @groupnames = split(' ',$opt->optionvalue);
963 foreach my $groupname ( @groupnames ) {
964 my $g = qsearchs('radius_group', { 'groupname' => $groupname } );
966 $g = new FS::radius_group {
967 'groupname' => $groupname,
968 'description' => $groupname,
971 die $error if $error;
973 push @groupnums, $g->groupnum;
975 $opt->optionvalue(join(' ',@groupnums));
976 $error = $opt->replace;
977 die $error if $error;
979 # for exports that have selectable hostnames, make sure all services
980 # have a hostname selected
981 foreach my $part_export (
982 qsearch('part_export', { 'machine' => '_SVC_MACHINE' })
985 my $exportnum = $part_export->exportnum;
986 my $machinenum = $part_export->default_machine;
988 my ($first) = $part_export->part_export_machine;
990 # user intervention really is required.
991 die "Export $exportnum has no hostname options defined.\n".
992 "You must correct this before upgrading.\n";
994 # warn about this, because we might not choose the right one
995 warn "Export $exportnum (". $part_export->exporttype.
996 ") has no default hostname. Setting to ".$first->machine."\n";
997 $machinenum = $first->machinenum;
998 $part_export->set('default_machine', $machinenum);
999 my $error = $part_export->replace;
1000 die $error if $error;
1003 # the service belongs to a service def that uses this export
1004 # and there is not a hostname selected for this export for that service
1005 my $join = ' JOIN export_svc USING ( svcpart )'.
1006 ' LEFT JOIN svc_export_machine'.
1007 ' ON ( cust_svc.svcnum = svc_export_machine.svcnum'.
1008 ' AND export_svc.exportnum = svc_export_machine.exportnum )';
1010 my @svcs = qsearch( {
1011 'select' => 'cust_svc.*',
1012 'table' => 'cust_svc',
1013 'addl_from' => $join,
1014 'extra_sql' => ' WHERE svcexportmachinenum IS NULL'.
1015 ' AND export_svc.exportnum = '.$part_export->exportnum,
1017 foreach my $cust_svc (@svcs) {
1018 my $svc_export_machine = FS::svc_export_machine->new({
1019 'exportnum' => $exportnum,
1020 'machinenum' => $machinenum,
1021 'svcnum' => $cust_svc->svcnum,
1023 my $error = $svc_export_machine->insert;
1024 die $error if $error;
1030 $exports_in_use{ref $_} = 1 foreach qsearch('part_export', {});
1031 foreach (keys(%exports_in_use)) {
1032 $_->_upgrade_exporttype(%opts) if $_->can('_upgrade_exporttype');
1036 #=item exporttype2svcdb EXPORTTYPE
1038 #Returns the applicable I<svcdb> for an I<exporttype>.
1042 #sub exporttype2svcdb {
1043 # my $exporttype = $_[0];
1044 # foreach my $svcdb ( keys %exports ) {
1045 # return $svcdb if grep { $exporttype eq $_ } keys %{$exports{$svcdb}};
1050 #false laziness w/part_pkg & cdr
1051 foreach my $INC ( @INC ) {
1052 foreach my $file ( glob("$INC/FS/part_export/*.pm") ) {
1053 warn "attempting to load export info from $file\n" if $DEBUG;
1054 $file =~ /\/(\w+)\.pm$/ or do {
1055 warn "unrecognized file in $INC/FS/part_export/: $file\n";
1059 my $info = eval "use FS::part_export::$mod; ".
1060 "\\%FS::part_export::$mod\::info;";
1062 die "error using FS::part_export::$mod (skipping): $@\n" if $@;
1065 unless ( keys %$info ) {
1066 warn "no %info hash found in FS::part_export::$mod, skipping\n"
1067 unless $mod =~ /^(passwdfile|null|.+_Common)$/; #hack but what the heck
1070 warn "got export info from FS::part_export::$mod: $info\n" if $DEBUG;
1073 ref($info->{'svc'}) ? @{$info->{'svc'}} : $info->{'svc'}
1076 warn "blank svc for FS::part_export::$mod (skipping)\n";
1079 $exports{$svc}->{$mod} = $info;
1086 =head1 NEW EXPORT CLASSES
1088 A module should be added in FS/FS/part_export/ (an example may be found in
1089 eg/export_template.pm)
1093 Hmm... cust_export class (not necessarily a database table...) ... ?
1095 deprecated column...
1099 L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_acct>,
1101 L<FS::svc_forward>, L<FS::Record>, schema.html from the base documentation.