import torrus 1.0.9
[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 Storable qw(thaw);
6 use Data::Dumper;
7 use MIME::Base64;
8 use FS::Misc::DateTime qw( parse_datetime );
9 use FS::Record qw( qsearchs );
10 use FS::cust_pkg;
11 use FS::cust_main;
12 use FS::svc_acct;
13 use FS::svc_external;
14 use FS::svc_phone;
15
16 $DEBUG = 0;
17
18 #install_callback FS::UID sub {
19 #  $conf = new FS::Conf;
20 #};
21
22 =head1 NAME
23
24 FS::cust_pkg::Import - Batch customer importing
25
26 =head1 SYNOPSIS
27
28   use FS::cust_pkg::Import;
29
30   #import
31   FS::cust_pkg::Import::batch_import( {
32     file      => $file,      #filename
33     type      => $type,      #csv or xls
34     format    => $format,    #extended, extended-plus_company, svc_external,
35                              # or svc_external_svc_phone
36     agentnum  => $agentnum,
37     job       => $job,       #optional job queue job, for progressbar updates
38     pkgbatch  => $pkgbatch, #optional batch unique identifier
39   } );
40   die $error if $error;
41
42   #ajax helper
43   use FS::UI::Web::JSRPC;
44   my $server =
45     new FS::UI::Web::JSRPC 'FS::cust_pkg::Import::process_batch_import', $cgi;
46   print $server->process;
47
48 =head1 DESCRIPTION
49
50 Batch package importing.
51
52 =head1 SUBROUTINES
53
54 =item process_batch_import
55
56 Load a batch import as a queued JSRPC job
57
58 =cut
59
60 sub process_batch_import {
61   my $job = shift;
62
63   my $param = thaw(decode_base64(shift));
64   warn Dumper($param) if $DEBUG;
65   
66   my $files = $param->{'uploaded_files'}
67     or die "No files provided.\n";
68
69   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
70
71   my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
72   my $file = $dir. $files{'file'};
73
74   my $type;
75   if ( $file =~ /\.(\w+)$/i ) {
76     $type = lc($1);
77   } else {
78     #or error out???
79     warn "can't parse file type from filename $file; defaulting to CSV";
80     $type = 'csv';
81   }
82
83   my $error =
84     FS::cust_pkg::Import::batch_import( {
85       job      => $job,
86       file     => $file,
87       type     => $type,
88       'params' => { pkgbatch => $param->{pkgbatch} },
89       agentnum => $param->{'agentnum'},
90       'format' => $param->{'format'},
91     } );
92
93   unlink $file;
94
95   die "$error\n" if $error;
96
97 }
98
99 =item batch_import
100
101 =cut
102
103 my %formatfields = (
104   'default'      => [],
105   'svc_acct'     => [qw( username _password domsvc )],
106   'svc_phone'    => [qw( countrycode phonenum sip_password pin )],
107   'svc_external' => [qw( id title )],
108 );
109
110 sub _formatfields {
111   \%formatfields;
112 }
113
114 my %import_options = (
115   'table'         => 'cust_pkg',
116
117   'postinsert_callback' => sub {
118     my( $record, $param ) = @_;
119
120     my $formatfields = _formatfields;
121     foreach my $svc_x ( grep { $_ ne 'default' } keys %$formatfields ) {
122
123       my $ff = $formatfields->{$svc_x};
124
125       if ( grep $param->{"$svc_x.$_"}, @$ff ) {
126         my $svc_x = "FS::$svc_x"->new( {
127           'pkgnum'  => $record->pkgnum,
128           'svcpart' => $record->part_pkg->svcpart($svc_x),
129           map { $_ => $param->{"$svc_x.$_"} } @$ff
130         } );
131         my $error = $svc_x->insert;
132         return $error if $error;
133       }
134
135     }
136
137     return ''; #no error
138
139   },
140 );
141
142 sub _import_options {
143   \%import_options;
144 }
145
146 sub batch_import {
147   my $opt = shift;
148
149   my $iopt = _import_options;
150   $opt->{$_} = $iopt->{$_} foreach keys %$iopt;
151
152   my $agentnum  = delete $opt->{agentnum}; # i like closures (delete though?)
153
154   my $format = delete $opt->{'format'};
155   my @fields = ();
156
157   if ( $format =~ /^(.*)-agent_custid$/ ) {
158     $format = $1;
159     @fields = (
160       sub {
161         my( $self, $value ) = @_; # $conf, $param
162         my $cust_main = qsearchs('cust_main', {
163           'agentnum'     => $agentnum,
164           'agent_custid' => $value,
165         });
166         $self->custnum($cust_main->custnum) if $cust_main;
167       },
168     );
169   } else {
170     @fields = ( 'custnum' );
171   }
172
173   push @fields, ( 'pkgpart', 'discountnum' );
174
175   foreach my $field ( 
176     qw( start_date setup bill last_bill susp adjourn cancel expire )
177   ) {
178     push @fields, sub {
179       my( $self, $value ) = @_; # $conf, $param
180       #->$field has undesirable effects
181       $self->set($field, parse_datetime($value) ); #$field closure
182     };
183   }
184
185   my $formatfields = _formatfields();
186
187   die "unknown format $format" unless $formatfields->{$format};
188
189   foreach my $field ( @{ $formatfields->{$format} } ) {
190
191     push @fields, sub {
192       my( $self, $value, $conf, $param ) = @_;
193       $param->{"$format.$field"} = $value;
194     };
195
196   }
197
198   $opt->{'fields'} = \@fields;
199
200   FS::Record::batch_import( $opt );
201
202 }
203
204 =for comment
205
206     my $billtime = time;
207     my %cust_pkg = ( pkgpart => $pkgpart );
208     my %svc_x = ();
209     foreach my $field ( @fields ) {
210
211       if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) {
212
213         #$cust_pkg{$1} = parse_datetime( shift @$columns );
214         if ( $1 eq 'pkgpart' ) {
215           $cust_pkg{$1} = shift @columns;
216         } elsif ( $1 eq 'setup' ) {
217           $billtime = parse_datetime(shift @columns);
218         } else {
219           $cust_pkg{$1} = parse_datetime( shift @columns );
220         } 
221
222       } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
223
224         $svc_x{$1} = shift @columns;
225
226       } elsif ( $field =~ /^svc_external\.(id|title)$/ ) {
227
228         $svc_x{$1} = shift @columns;
229
230       } elsif ( $field =~ /^svc_phone\.(countrycode|phonenum|sip_password|pin)$/ ) {
231         $svc_x{$1} = shift @columns;
232        
233       } else {
234
235         #refnum interception
236         if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
237
238           my $referral = $columns[0];
239           my %hash = ( 'referral' => $referral,
240                        'agentnum' => $agentnum,
241                        'disabled' => '',
242                      );
243
244           my $part_referral = qsearchs('part_referral', \%hash )
245                               || new FS::part_referral \%hash;
246
247           unless ( $part_referral->refnum ) {
248             my $error = $part_referral->insert;
249             if ( $error ) {
250               $dbh->rollback if $oldAutoCommit;
251               return "can't auto-insert advertising source: $referral: $error";
252             }
253           }
254
255           $columns[0] = $part_referral->refnum;
256         }
257
258         my $value = shift @columns;
259         $cust_main{$field} = $value if length($value);
260       }
261     }
262
263     $cust_main{'payby'} = 'CARD'
264       if defined $cust_main{'payinfo'}
265       && length  $cust_main{'payinfo'};
266
267     my $invoicing_list = $cust_main{'invoicing_list'}
268                            ? [ delete $cust_main{'invoicing_list'} ]
269                            : [];
270
271     my $cust_main = new FS::cust_main ( \%cust_main );
272
273     use Tie::RefHash;
274     tie my %hash, 'Tie::RefHash'; #this part is important
275
276     if ( $cust_pkg{'pkgpart'} ) {
277       my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
278
279       my @svc_x = ();
280       my $svcdb = '';
281       if ( $svc_x{'username'} ) {
282         $svcdb = 'svc_acct';
283       } elsif ( $svc_x{'id'} || $svc_x{'title'} ) {
284         $svcdb = 'svc_external';
285       }
286
287       my $svc_phone = '';
288       if ( $svc_x{'countrycode'} || $svc_x{'phonenum'} ) {
289         $svc_phone = FS::svc_phone->new( {
290           map { $_ => delete($svc_x{$_}) }
291               qw( countrycode phonenum sip_password pin)
292         } );
293       }
294
295       if ( $svcdb || $svc_phone ) {
296         my $part_pkg = $cust_pkg->part_pkg;
297         unless ( $part_pkg ) {
298           $dbh->rollback if $oldAutoCommit;
299           return "unknown pkgpart: ". $cust_pkg{'pkgpart'};
300         } 
301         if ( $svcdb ) {
302           $svc_x{svcpart} = $part_pkg->svcpart_unique_svcdb( $svcdb );
303           my $class = "FS::$svcdb";
304           push @svc_x, $class->new( \%svc_x );
305         }
306         if ( $svc_phone ) {
307           $svc_phone->svcpart( $part_pkg->svcpart_unique_svcdb('svc_phone') );
308           push @svc_x, $svc_phone;
309         }
310       }
311
312       $hash{$cust_pkg} = \@svc_x;
313     }
314
315     my $error = $cust_main->insert( \%hash, $invoicing_list );
316
317     if ( $error ) {
318       $dbh->rollback if $oldAutoCommit;
319       return "can't insert customer". ( $line ? " for $line" : '' ). ": $error";
320     }
321
322     if ( $format eq 'simple' ) {
323
324       #false laziness w/bill.cgi
325       $error = $cust_main->bill( 'time' => $billtime );
326       if ( $error ) {
327         $dbh->rollback if $oldAutoCommit;
328         return "can't bill customer for $line: $error";
329       }
330   
331       $error = $cust_main->apply_payments_and_credits;
332       if ( $error ) {
333         $dbh->rollback if $oldAutoCommit;
334         return "can't bill customer for $line: $error";
335       }
336
337       $error = $cust_main->collect();
338       if ( $error ) {
339         $dbh->rollback if $oldAutoCommit;
340         return "can't collect customer for $line: $error";
341       }
342
343     }
344
345     $row++;
346
347     if ( $job && time - $min_sec > $last ) { #progress bar
348       $job->update_statustext( int(100 * $row / $count) );
349       $last = time;
350     }
351
352   }
353
354   $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
355
356   return "Empty file!" unless $row;
357
358   ''; #no error
359
360 }
361
362 =head1 BUGS
363
364 Not enough documentation.
365
366 =head1 SEE ALSO
367
368 L<FS::cust_main>, L<FS::cust_pkg>,
369 L<FS::svc_acct>, L<FS::svc_external>, L<FS::svc_phone>
370
371 =cut
372
373 1;