93bd88d7b6293f89c0948b322535ab1f405c1af1
[freeside.git] / FS / FS / cust_pkg / Import.pm
1 package FS::cust_pkg::Import;
2
3 use strict;
4 use vars qw( $DEBUG ); #$conf );
5 use Data::Dumper;
6 use FS::Misc::DateTime qw( parse_datetime );
7 use FS::Record qw( qsearchs );
8 use FS::cust_pkg;
9 use FS::cust_main;
10 use FS::svc_acct;
11 use FS::svc_external;
12 use FS::svc_phone;
13 use FS::svc_domain;
14
15 $DEBUG = 0;
16
17 #install_callback FS::UID sub {
18 #  $conf = new FS::Conf;
19 #};
20
21 =head1 NAME
22
23 FS::cust_pkg::Import - Batch customer importing
24
25 =head1 SYNOPSIS
26
27   use FS::cust_pkg::Import;
28
29   #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
38   } );
39   die $error if $error;
40
41   #ajax helper
42   use FS::UI::Web::JSRPC;
43   my $server =
44     new FS::UI::Web::JSRPC 'FS::cust_pkg::Import::process_batch_import', $cgi;
45   print $server->process;
46
47 =head1 DESCRIPTION
48
49 Batch package importing.
50
51 =head1 SUBROUTINES
52
53 =item process_batch_import
54
55 Load a batch import as a queued JSRPC job
56
57 =cut
58
59 sub process_batch_import {
60   my $job = shift;
61   my $param = shift;
62   warn Dumper($param) if $DEBUG;
63   
64   my $files = $param->{'uploaded_files'}
65     or die "No files provided.\n";
66
67   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
68
69   my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
70   my $file = $dir. $files{'file'};
71
72   my $type;
73   if ( $file =~ /\.(\w+)$/i ) {
74     $type = lc($1);
75   } else {
76     #or error out???
77     warn "can't parse file type from filename $file; defaulting to CSV";
78     $type = 'csv';
79   }
80
81   my $error =
82     FS::cust_pkg::Import::batch_import( {
83       job      => $job,
84       file     => $file,
85       type     => $type,
86       'params' => { pkgbatch => $param->{pkgbatch} },
87       agentnum => $param->{'agentnum'},
88       'format' => $param->{'format'},
89     } );
90
91   unlink $file;
92
93   die "$error\n" if $error;
94
95 }
96
97 =item batch_import
98
99 =cut
100
101 my %formatfields = (
102   'default'      => [],
103   'all_dates'    => [],
104   'svc_acct'     => [qw( username _password domsvc )],
105   'svc_phone'    => [qw( countrycode phonenum sip_password pin )],
106   'svc_external' => [qw( id title )],
107   'location'     => [qw( address1 address2 city state zip country )],
108   'quan_price'   => [qw( quantity setup_fee recur_fee invoice_details )],
109 );
110
111 sub _formatfields {
112   \%formatfields;
113 }
114
115 my %import_options = (
116   'table'         => 'cust_pkg',
117
118   'preinsert_callback'  => sub {
119     my($record, $param) = @_;
120
121     my @location_params = grep { /^location\./ && length($param->{$_}) }
122                             keys %$param;
123     if (@location_params) {
124       my $cust_location = FS::cust_location->new({
125           'custnum' => $record->custnum,
126       });
127       foreach my $p (@location_params) {
128         $p =~ /^location.(\w+)$/;
129         $cust_location->set($1, $param->{$p});
130       }
131
132       my $error = $cust_location->find_or_insert; # this avoids duplicates
133       return "error creating location: $error" if $error;
134       $record->set('locationnum', $cust_location->locationnum);
135     }
136
137     $record->quantity( $param->{'quan_price.quantity'} )
138       if $param->{'quan_price.quantity'} > 0;
139     
140     my $s = $param->{'quan_price.setup_fee'};
141     my $r = $param->{'quan_price.recur_fee'};
142     my $part_pkg = $record->part_pkg;
143     if (    ( length($s) && $s != $part_pkg->option('setup_fee') )
144          or ( length($r) && $r != $part_pkg->option('recur_fee') )
145        )
146     {
147
148       local($FS::part_pkg::skip_pkg_svc_hack) = 1;
149
150       my $custom_part_pkg = $part_pkg->clone;
151       $custom_part_pkg->disabled('Y');
152       my %options = $part_pkg->options;
153       $options{'setup_fee'} = $s if length($s);
154       $options{'recur_fee'} = $r if length($r);
155       my $error = $custom_part_pkg->insert( options=>\%options );
156       return "error customizing package: $error" if $error;
157
158       #not ->pkg_svc, we want to ignore links and clone the actual package def
159       foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
160         my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
161         $c_pkg_svc->pkgsvcnum('');
162         $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
163         my $error = $c_pkg_svc->insert;
164         return "error customizing package: $error" if $error;
165       }
166
167       $record->pkgpart( $custom_part_pkg->pkgpart );
168     }
169
170
171     '';
172   },
173
174   'postinsert_callback' => sub {
175     my( $record, $param ) = @_;
176
177     if ( $param->{'quan_price.invoice_details'} ) {
178
179       my $weight = 0;
180       foreach my $detail (split(/\|/, $param->{'quan_price.invoice_details'})) {
181
182         my $cust_pkg_detail = new FS::cust_pkg_detail {
183           'pkgnum'     => $record->pkgnum,
184           'detail'     => $detail,
185           'detailtype' => 'I',
186           'weight'     => $weight++,
187         };
188
189         my $error = $cust_pkg_detail->insert;
190         return "error inserting invoice detail: $error" if $error;
191
192       }
193
194     }
195
196     my $formatfields = _formatfields;
197     foreach my $svc_x ( grep /^svc/, keys %$formatfields ) {
198
199       my $ff = $formatfields->{$svc_x};
200
201       if ( grep $param->{"$svc_x.$_"}, @$ff ) {
202
203         my $svc = "FS::$svc_x"->new( {
204           'pkgnum'  => $record->pkgnum,
205           'svcpart' => $record->part_pkg->svcpart($svc_x),
206           map { $_ => $param->{"$svc_x.$_"} } @$ff
207         } );
208
209         #this whole thing should be turned into a callback or config to turn on
210         if ( $svc_x eq 'svc_acct' && $svc->username =~ /\@/ ) {
211           my($username, $domain) = split(/\@/, $svc->username);
212           my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
213                          || new FS::svc_domain { 'svcpart' => 1,
214                                                  'domain'  => $domain, };
215           unless ( $svc_domain->svcnum ) {
216             my $error = $svc_domain->insert;
217             return "error auto-inserting domain: $error" if $error;
218           }
219           $svc->username($username);
220           $svc->domsvc($svc_domain->svcnum);
221         }
222
223         my $error = $svc->insert;
224         return "error inserting service: $error" if $error;
225       }
226
227     }
228
229     return ''; #no error
230
231   },
232 );
233
234 sub _import_options {
235   \%import_options;
236 }
237
238 sub batch_import {
239   my $opt = shift;
240
241   my $iopt = _import_options;
242   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
243
244   my $agentnum  = delete $opt->{agentnum}; # i like closures (delete though?)
245
246   my $format = delete $opt->{'format'};
247   my @fields = ();
248
249   if ( $format =~ /^(.*)-agent_custid(-agent_pkgid)?$/ ) {
250     $format = $1;
251     my $agent_pkgid = $2;
252     @fields = (
253       sub {
254         my( $self, $value ) = @_; # $conf, $param
255         my $cust_main = qsearchs('cust_main', {
256           'agentnum'     => $agentnum,
257           'agent_custid' => $value,
258         });
259         $self->custnum($cust_main->custnum) if $cust_main;
260       },
261     );
262     push @fields, 'agent_pkgid' if $agent_pkgid;
263   } else {
264     @fields = ( 'custnum' );
265   }
266
267   if ( $format =~ /^(.*)-locationnum$/ ) {
268     $format = $1;
269     push @fields, 'locationnum';
270   }
271
272   if ( $format =~ /^bulk_(.*)$/ ) {
273
274     $format = $1;
275
276     $opt->{'postinsert_callback'} = sub {
277       my( $record, $param ) = @_;
278
279       my $formatfields = _formatfields;
280       foreach my $svc_x ( grep /^svc/, keys %$formatfields ) {
281
282         my $ff = $formatfields->{$svc_x};
283
284         if ( grep $param->{"$svc_x.$_"}, @$ff ) {
285
286           $param->{'svc_phone.phonenum'} =~ /^\s*(\d+)\s*\-\s*(\d+)\s*$/
287             or return 'Enter a phone number range, with dash as the separator';
288           my($start, $end) = ($1, $2);
289           if ( length($end) < length($start) ) {
290             $end = substr($start, 0, length($start) - length($end) ). $end;
291           }
292
293           foreach my $phonenum ( "$start" .. "$end" ) {
294
295             my $svc = "FS::$svc_x"->new( {
296               'pkgnum'  => $record->pkgnum,
297               'svcpart' => $record->part_pkg->svcpart($svc_x),
298               map { $_ => $param->{"$svc_x.$_"} } @$ff
299             } );
300
301             $svc->phonenum($phonenum);
302             #$svc->set_default_and_fixed;
303             my $error = $svc->insert;
304             return "error inserting service: $error" if $error;
305
306           }
307
308         }
309
310       }
311
312       return ''; #no error
313
314     };
315
316   }
317
318   push @fields, ( 'pkgpart', 'discountnum' );
319
320   my @date_fields = ();
321   if ( $format =~ /all_dates/ ) {
322     @date_fields = qw(
323       order_date
324       start_date setup bill last_bill susp adjourn
325       resume
326       cancel expire
327       contract_end dundate
328     );
329   } else {
330     @date_fields = qw(
331       start_date setup bill last_bill susp adjourn
332       cancel expire
333     );
334   }
335
336   foreach my $field (@date_fields) { 
337     push @fields, sub {
338       my( $self, $value ) = @_; # $conf, $param
339       #->$field has undesirable effects
340       $self->set($field, parse_datetime($value) ); #$field closure
341     };
342   }
343
344   my @formats = split /-/, $format;
345   foreach my $f (@formats){
346
347     my $formatfields = _formatfields();
348     die "unknown format $format" unless $formatfields->{$f};
349
350     foreach my $field ( @{ $formatfields->{$f} } ) {
351
352       push @fields, sub {
353         my( $self, $value, $conf, $param ) = @_;
354         $param->{"$f.$field"} = $value;
355       };
356
357     }
358   }
359
360   $opt->{'fields'} = \@fields;
361
362   FS::Record::batch_import( $opt );
363
364 }
365
366 =head1 BUGS
367
368 Not enough documentation.
369
370 =head1 SEE ALSO
371
372 L<FS::cust_main>, L<FS::cust_pkg>,
373 L<FS::svc_acct>, L<FS::svc_external>, L<FS::svc_phone>
374
375 =cut
376
377 1;