1 package FS::cust_pkg::Import;
4 use vars qw( $DEBUG ); #$conf );
6 use FS::Misc::DateTime qw( parse_datetime );
7 use FS::Record qw( qsearchs );
17 #install_callback FS::UID sub {
18 # $conf = new FS::Conf;
23 FS::cust_pkg::Import - Batch customer importing
27 use FS::cust_pkg::Import;
30 FS::cust_pkg::Import::batch_import( {
31 file => $file, #filename
32 type => $type, #csv or xls
33 format => $format, #extended, extended-plus_company, svc_external,
34 # or svc_external_svc_phone
35 agentnum => $agentnum,
36 job => $job, #optional job queue job, for progressbar updates
37 pkgbatch => $pkgbatch, #optional batch unique identifier
42 use FS::UI::Web::JSRPC;
44 new FS::UI::Web::JSRPC 'FS::cust_pkg::Import::process_batch_import', $cgi;
45 print $server->process;
49 Batch package importing.
53 =item process_batch_import
55 Load a batch import as a queued JSRPC job
59 sub process_batch_import {
62 warn Dumper($param) if $DEBUG;
64 my $files = $param->{'uploaded_files'}
65 or die "No files provided.\n";
67 my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
69 my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
70 my $file = $dir. $files{'file'};
73 if ( $file =~ /\.(\w+)$/i ) {
77 warn "can't parse file type from filename $file; defaulting to CSV";
82 FS::cust_pkg::Import::batch_import( {
86 'params' => { pkgbatch => $param->{pkgbatch} },
87 agentnum => $param->{'agentnum'},
88 'format' => $param->{'format'},
93 die "$error\n" if $error;
104 'svc_acct' => [qw( username _password domsvc )],
105 'svc_broadband' => [qw( ip_addr description routernum blocknum sectornum speed_up speed_down )],
106 'svc_phone' => [qw( countrycode phonenum sip_password pin )],
107 'svc_external' => [qw( id title )],
108 'location' => [qw( address1 address2 city state zip country )],
109 'quan_price' => [qw( quantity setup_fee recur_fee invoice_details )],
116 my %import_options = (
117 'table' => 'cust_pkg',
119 'preinsert_callback' => sub {
120 my($record, $param) = @_;
122 my @location_params = grep { /^location\./ && length($param->{$_}) }
124 if (@location_params) {
125 my $cust_location = FS::cust_location->new({
126 'custnum' => $record->custnum,
128 foreach my $p (@location_params) {
129 $p =~ /^location.(\w+)$/;
130 $cust_location->set($1, $param->{$p});
133 my $error = $cust_location->find_or_insert; # this avoids duplicates
134 return "error creating location: $error" if $error;
135 $record->set('locationnum', $cust_location->locationnum);
138 $record->quantity( $param->{'quan_price.quantity'} )
139 if $param->{'quan_price.quantity'} > 0;
141 my $s = $param->{'quan_price.setup_fee'};
142 my $r = $param->{'quan_price.recur_fee'};
143 my $part_pkg = $record->part_pkg;
144 if ( ( length($s) && $s != $part_pkg->option('setup_fee') )
145 or ( length($r) && $r != $part_pkg->option('recur_fee') )
149 local($FS::part_pkg::skip_pkg_svc_hack) = 1;
151 my $custom_part_pkg = $part_pkg->clone;
152 $custom_part_pkg->disabled('Y');
153 my %options = $part_pkg->options;
154 $options{'setup_fee'} = $s if length($s);
155 $options{'recur_fee'} = $r if length($r);
156 my $error = $custom_part_pkg->insert( options=>\%options );
157 return "error customizing package: $error" if $error;
159 #not ->pkg_svc, we want to ignore links and clone the actual package def
160 foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
161 my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
162 $c_pkg_svc->pkgsvcnum('');
163 $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
164 my $error = $c_pkg_svc->insert;
165 return "error customizing package: $error" if $error;
168 $record->pkgpart( $custom_part_pkg->pkgpart );
175 'postinsert_callback' => sub {
176 my( $record, $param ) = @_;
178 if ( $param->{'quan_price.invoice_details'} ) {
181 foreach my $detail (split(/\|/, $param->{'quan_price.invoice_details'})) {
183 my $cust_pkg_detail = new FS::cust_pkg_detail {
184 'pkgnum' => $record->pkgnum,
187 'weight' => $weight++,
190 my $error = $cust_pkg_detail->insert;
191 return "error inserting invoice detail: $error" if $error;
197 my $formatfields = _formatfields;
198 foreach my $svc_x ( grep /^svc/, keys %$formatfields ) {
200 my $ff = $formatfields->{$svc_x};
202 if ( grep $param->{"$svc_x.$_"}, @$ff ) {
204 my $svc = "FS::$svc_x"->new( {
205 'pkgnum' => $record->pkgnum,
206 'svcpart' => $record->part_pkg->svcpart($svc_x),
207 map { $_ => $param->{"$svc_x.$_"} } @$ff
210 #this whole thing should be turned into a callback or config to turn on
211 if ( $svc_x eq 'svc_acct' && $svc->username =~ /\@/ ) {
212 my($username, $domain) = split(/\@/, $svc->username);
213 my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
214 || new FS::svc_domain { 'svcpart' => 1,
215 'domain' => $domain, };
216 unless ( $svc_domain->svcnum ) {
217 my $error = $svc_domain->insert;
218 return "error auto-inserting domain: $error" if $error;
220 $svc->username($username);
221 $svc->domsvc($svc_domain->svcnum);
224 my $error = $svc->insert;
225 return "error inserting service: $error" if $error;
235 sub _import_options {
242 my $iopt = _import_options;
243 $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
245 my $agentnum = delete $opt->{agentnum}; # i like closures (delete though?)
247 my $format = delete $opt->{'format'};
250 if ( $format =~ /^(.*)-agent_custid(-agent_pkgid)?$/ ) {
252 my $agent_pkgid = $2;
255 my( $self, $value ) = @_; # $conf, $param
256 my $cust_main = qsearchs('cust_main', {
257 'agentnum' => $agentnum,
258 'agent_custid' => $value,
260 $self->custnum($cust_main->custnum) if $cust_main;
263 push @fields, 'agent_pkgid' if $agent_pkgid;
265 @fields = ( 'custnum' );
268 if ( $format =~ /^(.*)-locationnum$/ ) {
270 push @fields, 'locationnum';
273 if ( $format =~ /^bulk_(.*)$/ ) {
277 $opt->{'postinsert_callback'} = sub {
278 my( $record, $param ) = @_;
280 my $formatfields = _formatfields;
281 foreach my $svc_x ( grep /^svc/, keys %$formatfields ) {
283 my $ff = $formatfields->{$svc_x};
285 if ( grep $param->{"$svc_x.$_"}, @$ff ) {
287 $param->{'svc_phone.phonenum'} =~ /^\s*(\d+)\s*\-\s*(\d+)\s*$/
288 or return 'Enter a phone number range, with dash as the separator';
289 my($start, $end) = ($1, $2);
290 if ( length($end) < length($start) ) {
291 $end = substr($start, 0, length($start) - length($end) ). $end;
294 foreach my $phonenum ( "$start" .. "$end" ) {
296 my $svc = "FS::$svc_x"->new( {
297 'pkgnum' => $record->pkgnum,
298 'svcpart' => $record->part_pkg->svcpart($svc_x),
299 map { $_ => $param->{"$svc_x.$_"} } @$ff
302 $svc->phonenum($phonenum);
303 #$svc->set_default_and_fixed;
304 my $error = $svc->insert;
305 return "error inserting service: $error" if $error;
319 push @fields, ( 'pkgpart', 'discountnum' );
321 my @date_fields = ();
322 if ( $format =~ /all_dates/ ) {
325 start_date setup bill last_bill susp adjourn
332 start_date setup bill last_bill susp adjourn
337 foreach my $field (@date_fields) {
339 my( $self, $value ) = @_; # $conf, $param
340 #->$field has undesirable effects
341 $self->set($field, parse_datetime($value) ); #$field closure
345 my @formats = split /-/, $format;
346 foreach my $f (@formats){
348 my $formatfields = _formatfields();
349 die "unknown format $format" unless $formatfields->{$f};
351 foreach my $field ( @{ $formatfields->{$f} } ) {
354 my( $self, $value, $conf, $param ) = @_;
355 $param->{"$f.$field"} = $value;
361 $opt->{'fields'} = \@fields;
363 FS::Record::batch_import( $opt );
369 Not enough documentation.
373 L<FS::cust_main>, L<FS::cust_pkg>,
374 L<FS::svc_acct>, L<FS::svc_external>, L<FS::svc_phone>