show credit balance on invoices, #11564
[freeside.git] / bin / b-move-customers
1 #!/usr/bin/perl -w
2
3 #script to move customers from one installation to another
4 # source is remote, destination is local
5
6 use strict;
7 use vars qw( $sdbh );
8 use DBI;
9 use FS::UID qw( adminsuidsetup dbh );
10 use FS::Schema qw( dbdef );
11 use FS::Record qw( qsearchs );
12 use FS::agent;
13 use FS::cust_main;
14 use FS::part_pkg;
15 use FS::part_svc;
16 use FS::cust_bill_ApplicationCommon;
17 use FS::svc_Common;
18 use FS::cust_event;
19 use FS::svc_domain;
20 use FS::cust_pkg;
21
22 my $DANGEROUS = 0;
23 my $DRY = 0;
24
25 my $source_datasrc = 'dbi:Pg:dbname=benson';
26
27 my $source_user = 'freeside';
28 my $source_pw = '';
29
30 my $dest_agentnum = 3;
31 my $src_agentnum = 1;
32 my $dest_refnum = 17;
33
34 my %domsvc_map = (
35   2 => 10375,
36 );
37
38 my %eventparts = (
39  'CARD' => [ 13, 14, 15 ],
40  'CHEK' => [],
41  'BILL' => [ 13, ],
42  'DCHK' => [],
43  'DCRD' => [ 13, ],
44  'COMP' => [],
45 );
46
47 #--
48
49 # target(local) setup
50
51 my $user = shift
52   or die "Usage:\n  (edit variables at top of script and then)\n".
53          "  b-move-customers user\n";
54 adminsuidsetup $user;
55
56 $FS::cust_main::ignore_expired_card = 1;
57 $FS::cust_main::ignore_expired_card = 1;
58 $FS::part_pkg::skip_pkg_svc_hack = 1;
59 $FS::part_pkg::skip_pkg_svc_hack = 1;
60 $FS::cust_bill_ApplicationCommon::skip_apply_to_lineitems_hack = 1;
61 $FS::cust_bill_ApplicationCommon::skip_apply_to_lineitems_hack = 1;
62 $FS::svc_Common::noexport_hack = 1;
63 $FS::svc_Common::noexport_hack = 1;
64 $FS::svc_domain::whois_hack = 1;
65 $FS::svc_domain::whois_hack = 1;
66 $FS::cust_pkg::disable_agentcheck = 1;
67 $FS::cust_pkg::disable_agentcheck = 1;
68
69 my $void_paynum = 2147483646; #top of int range
70
71 # -- 
72
73 # source(remote) setup
74
75 $sdbh = DBI->connect($source_datasrc, $source_user, $source_pw)
76   or die $DBI::errstr;
77
78 $sdbh->{ChopBlanks} = 1;
79
80 # --
81
82 my %map = ();
83 $map{'_DOMSVC'} = \%domsvc_map;
84
85 import_table('pkg_category', 'nomap' => 1);
86 import_table('pkg_class', 'nomap' => 1,
87     'preinsert_callback' => sub {
88         my($row, $object) = @_;
89         my $src_categorynum = $row->{'categorynum'};
90         my $dest_categorynum = $map{'pkg_category'}->{$src_categorynum};
91         if ( $dest_categorynum ) {
92           $object->categorynum($dest_categorynum);
93         }
94     }
95 );
96
97 import_table('reason_type', 'nomap' => 1);
98 foreach my $src_typenum ( keys %{ $map{'reason_type'} } ) {
99   import_table('reason', 'reason_type' => $src_typenum,
100                          'search'      => 'reason_type',
101                          'map'         => 'reason_type',
102               );
103 }
104
105 my $customer_sth = $sdbh->prepare(
106   "SELECT * FROM cust_main WHERE agentnum = $src_agentnum ORDER BY custnum"
107 ) or die $sdbh->errstr;
108
109 $customer_sth->execute or die $customer_sth->errstr;
110
111 my %referrals = ();
112
113 while ( my $customerrow = $customer_sth->fetchrow_hashref ) {
114
115     my $src_custnum = $customerrow->{'custnum'};
116
117     if ( $customerrow->{'referral_custnum'} ) {
118         warn "   $src_custnum has referral_custnum ". $customerrow->{'referral_custnum'};
119         $referrals{$src_custnum} = $customerrow->{'referral_custnum'};
120     };
121
122     my $cust_main = new FS::cust_main {
123       %{ $customerrow },
124       'custnum'      => '',
125       'referral_custnum' => '',
126       'refnum'       => $dest_refnum,
127       'agentnum'     => $dest_agentnum,
128       'agent_custid' => $src_custnum,
129     };
130
131     my $error = $cust_main->insert;
132     if ( $error ) {
133       warn "*** WARNING: error importing customer src custnum $src_custnum: $error";
134       next;
135     }
136
137     warn "inserting dest customer ". $cust_main->custnum. " for $src_custnum\n";
138
139     $map{'cust_main'}->{$src_custnum} = $cust_main->custnum;
140
141     #now import the relations, easy and hard:
142     
143     import_table( 'cust_location', 'custnum' => $src_custnum );
144
145     import_table( 'cust_main_note', 'custnum' => $src_custnum );
146
147     import_table( 'cust_pay', 'custnum' => $src_custnum );
148
149     import_table( 'cust_credit', 'custnum' => $src_custnum, 
150          'preinsert_callback' => sub {
151             my($row, $object) = @_;
152             my $src_reasonnum = $row->{'reasonnum'};
153             my $dest_reasonnum = $map{'reason'}->{$src_reasonnum};
154             if ( $dest_reasonnum ) {
155               $object->reasonnum($dest_reasonnum);
156             }
157         }
158     );
159
160     import_table( 'cust_refund', 'custnum' => $src_custnum,
161       'post_callback' => sub {
162         #my( $src_refundnum, $dst_refundnum ) = @_;
163         my $src_refundnum = shift;
164
165         # cust_pay_refund (map refundnum and paynum...)
166         import_table( 'cust_pay_refund',
167                       'refundnum' => $src_refundnum,
168                       'search'    => 'refundnum',
169                       'map'       => 'cust_refund',
170                       'map2'      => 'cust_pay',
171                       'map2key'   => 'paynum',
172                     );
173
174       },
175     );
176
177     # cust_pay_void
178     import_table( 'cust_pay_void', 'custnum' => $src_custnum,
179       'preinsert_callback' => sub {
180         my($row, $object) = @_;
181         $object->paynum( $void_paynum-- );
182       },
183     );
184
185 # no data in old db for:
186 #       cust_attachment, cust_statement, cdr, cdr_*, cust_bill_event,
187 #       cust_main_exemption, cust_pay_batch, cust_tax_*, cust_recon,
188 #       inventory_item, part_bill_event, part_device, part_export, 
189 #       part_pop_local, part_virtual_field, pay_batch, phone_*, 
190 #       payment_gateway_*, prepay_credit, port, radius_usergroup, 
191 #       rate_*, reg_code, reg_code_pkg, registrar, router, 
192 #       svc_acct, svc_acct_pop, svc_broadband, svc_external,
193 #       svc_forward, svc_phone, svc_www, tax_*, usage_class, virtual_field
194 # appears to be unused in old db: inventory_class
195 # ignore queue
196
197     #werid direct cust_main relations: 
198
199     warn "   inserting cust_pkg for src cust $src_custnum\n";
200     # cust_pkg (part_pkg, part_svc, etc.)
201     import_table( 'cust_pkg', 'custnum' => $src_custnum,
202       'preinsert_callback' => sub {
203         my($row, $object) = @_;
204         my $src_pkgpart = $row->{'pkgpart'} or die "wtf";
205         my $dest_pkgpart = $map{'part_pkg'}->{$src_pkgpart};
206         if ( $dest_pkgpart ) {
207           $object->pkgpart($dest_pkgpart);
208           return;
209         }
210
211         my $sth = $sdbh->prepare(
212           "SELECT * FROM part_pkg WHERE pkgpart = $src_pkgpart"
213         ) or die $sdbh->errstr;
214
215         $sth->execute or die $sth->errstr;
216
217         my $part_pkg_row = $sth->fetchrow_hashref
218           or die "cust_pkg.pkgpart missing in part_pkg?!";
219
220         my $hashref = {
221           %{ $part_pkg_row },
222           'pkgpart'  => '',
223         };
224         my $src_classnum = $part_pkg_row->{'classnum'};
225         $hashref->{'classnum'} = $map{'pkg_class'}->{ $src_classnum }
226           if $src_classnum;
227
228         my $part_pkg = new FS::part_pkg $hashref;
229
230         #$part_pkg->setuptax('') if $part_pkg->setuptax =~ /^\s+$/;
231         #$part_pkg->recurtax('') if $part_pkg->recurtax =~ /^\s+$/;
232
233         my $error = $part_pkg->insert( 'options' => {} );
234         die "*** FATAL: error importing part_pkg src pkgpart $src_pkgpart ".
235             ": $error"
236           if $error;
237
238         $map{ 'part_pkg' }->{ $part_pkg_row->{'pkgpart'} } = $part_pkg->pkgpart;
239         
240         # part_pkg_option
241         import_table( 'part_pkg_option',
242                       'pkgpart' => $src_pkgpart,
243                       'search' => 'pkgpart',
244                       'map'    => 'part_pkg',
245                     );
246         
247         my $osth = $sdbh->prepare(
248           "SELECT * FROM part_pkg_option WHERE pkgpart = $src_pkgpart"
249         ) or die $sdbh->errstr;
250
251         # pkg_svc, part_svc, part_svc_column
252         import_table( 'pkg_svc',
253           'pkgpart' => $src_pkgpart,
254           'search'  => 'pkgpart',
255           'map'     => 'part_pkg',
256           'preinsert_callback' => sub {
257
258             my($row, $object) = @_;
259             my $src_svcpart = $row->{'svcpart'} or die "wtf2";
260             my $dest_svcpart = $map{'part_svc'}->{$src_svcpart};
261             if ( $dest_svcpart ) {
262               $object->svcpart($dest_svcpart);
263               return;
264             }
265
266             my $sth = $sdbh->prepare(
267               "SELECT * FROM part_svc WHERE svcpart = $src_svcpart"
268             ) or die $sdbh->errstr;
269
270             $sth->execute or die $sth->errstr;
271
272             my $part_svc_row = $sth->fetchrow_hashref
273               or die "svcpart missing in part_svc?!";
274
275             my $hashref = {
276               %{ $part_svc_row },
277               'svcpart' => '',
278             };
279
280             my $part_svc = new FS::part_svc $hashref;
281             $part_svc->disabled('') if $part_svc->disabled =~ /^\s+$/;
282             my $error = $part_svc->insert;
283             die "*** FATAL: error importing part_svc src svcpart $src_svcpart ".
284                 ": $error"
285               if $error;
286
287             $map{ 'part_svc' }->{ $part_svc_row->{'svcpart'} } = $part_svc->svcpart;
288
289             # part_svc_column
290             import_table( 'part_svc_column',
291                           'svcpart' => $src_svcpart,
292                           'search'  => 'svcpart',
293                           'map'     => 'part_svc',
294                           'preinsert_callback' => sub {
295                             my($row, $object) = @_;
296                             if ( $object->columnname eq 'domsvc' ) {
297                                $object->columnvalue( $map{'_DOMSVC'}->{ $object->columnvalue } );
298                             }
299                           },
300                         );
301         
302             #what we came here for in the first place
303             $object->svcpart( $part_svc->svcpart );
304
305           }
306         );
307
308         #what we came here for in the first place
309         $object->pkgpart( $part_pkg->pkgpart );
310
311       },
312
313       'post_callback' => sub {
314         #my( $src_pkgnum, $dst_pkgnum ) = @_;
315         my $src_pkgnum = shift;
316
317         #XXX grr... action makes this very hard... 
318         ## cust_pkg_reason (shit, and bring in/remap reasons)
319         #import_table( 'cust_pkg_reason',
320         #                'pkgnum'  => $src_pkgnum,
321         #                'search'  => 'pkgnum',
322         #                'map'     => 'cust_pkg',
323         #                'map2'    => 'reason',
324         #                'map2key' => 'reasonnum',
325         #            );
326
327         #cust_svc
328         import_table( 'cust_svc',
329                         'pkgnum'  => $src_pkgnum,
330                         'search'  => 'pkgnum',
331                         'map'     => 'cust_pkg',
332                         'map2'    => 'part_svc',
333                         'map2key' => 'svcpart',
334                         'post_callback' => sub {
335                           #my( $src_svcnum, $dst_svcnum ) = @_;
336                           my $src_svcnum = shift;
337
338                           #svc_domain
339                           import_table( 'svc_domain',
340                                           'svcnum' => $src_svcnum,
341                                           'search' => 'svcnum',
342                                           'map'    => 'cust_svc',
343                                           'noblank_primary' => 1,
344                                       );
345
346                         },
347                     );
348         
349         import_table('cust_pkg_detail', 
350                         'pkgnum'  => $src_pkgnum,
351                         'search'  => 'pkgnum',
352                         'map'     => 'cust_pkg',
353                      );
354
355       },
356
357     );
358     # end of cust_pkg (part_pkg, part_svc, etc.)
359
360     warn "   inserting cust_bill for src cust $src_custnum\n";
361     # cust_bill (invnum move)
362     import_table( 'cust_bill', 'custnum' => $src_custnum,
363       'preinsert_callback' => sub {
364         my($row, $object) = @_;
365         $object->agent_invid( $row->{'invnum'} );
366       },
367       'post_callback' => sub {
368         my( $src_invnum, $dst_invnum ) = @_;
369         #my $src_invnum = shift;
370
371         # cust_bill_pkg ( map invnum and pkgnum... )
372         import_table( 'cust_bill_pkg',
373                       'invnum' => $src_invnum,
374                       'search'  => 'invnum',
375                       'map'     => 'cust_bill',
376                       'map2'    => 'cust_pkg',
377                       'map2key' => 'pkgnum',
378                       'post_callback' => sub {
379                         my $src_billpkgnum = shift;
380
381                         import_table( 'cust_bill_pkg_detail',
382                                       'cust_bill_pkg.billpkgnum' => $src_billpkgnum,
383                                       'search'    => 'cust_bill_pkg.billpkgnum',
384                                       'map'       => 'cust_bill_pkg',
385                                       'addl_from' => 'left join cust_bill_pkg using ( invnum, pkgnum )',
386                                     );
387
388                       },
389                     );
390
391         # cust_credit_bill (map invnum and crednum... )
392         import_table( 'cust_credit_bill',
393                       'invnum' => $src_invnum,
394                       'search'  => 'invnum',
395                       'map'     => 'cust_bill',
396                       'map2'    => 'cust_credit',
397                       'map2key' => 'crednum',
398                       'post_callback' => sub {
399                         my $src_creditbillnum = shift;
400                         #map creditbillnum and billpkgnum
401                         import_table( 'cust_credit_bill_pkg',
402                                       'creditbillnum' => $src_creditbillnum,
403                                       'search'    => 'creditbillnum',
404                                       'map'       => 'cust_credit_bill',
405                                       'map2'      => 'cust_bill_pkg',
406                                       'map2key'   => 'billpkgnum',
407                                     );
408
409                       },
410                     );
411
412         # cust_bill_pay (map invnum and paynum...)
413         import_table( 'cust_bill_pay',
414                       'invnum' => $src_invnum,
415                       'search'  => 'invnum',
416                       'map'     => 'cust_bill',
417                       'map2'    => 'cust_pay',
418                       'map2key' => 'paynum',
419                       'post_callback' => sub {
420                         my $src_billpaynum = shift;
421                         #map billpaynum and billpkgnum
422                         import_table( 'cust_bill_pay_pkg',
423                                       'billpaynum' => $src_billpaynum,
424                                       'search'    => 'billpaynum',
425                                       'map'       => 'cust_bill_pay',
426                                       'map2'      => 'cust_bill_pkg',
427                                       'map2key'   => 'billpkgnum',
428                                     );
429                       },
430                     );
431
432         #need to do something about events. mark initial stuff as done
433         foreach my $eventpart ( @{ $eventparts{$cust_main->payby} } ) {
434
435           my $cust_event = new FS::cust_event {
436             'eventpart' => $eventpart,
437             'tablenum'  => $dst_invnum,
438             '_date'     => time, # XXX something?  probably not
439             'status'    => 'done',
440           };
441
442           my $error = $cust_event->insert;
443           die "*** FATAL: error inserting cust_event for eventpart $eventpart,".
444               " tablenum (invnum) $dst_invnum: $error"
445             if $error;
446
447         }
448
449       },
450     );
451
452     # ---
453
454        # (not used in old db: cust_bill_pay_batch, cust_pkg_option)
455
456     # ---
457
458     # (not in old db: cust_bill_pkg_display, cust_bill_pkg_tax_location,
459     #  cust_bill_pkg_tax_rate_location, cust_tax_adjustment, cust_svc_option, )
460     # (not used in old db: cust_tax_exempt_pkg)
461
462     #do this last, so no notices go out
463     import_table( 'cust_main_invoice', 'custnum' => $src_custnum );
464
465     #dbh->commit or die dbh->errstr;
466     warn "customer ". $cust_main->custnum. " inserted\n";
467     #exit;
468
469 }
470
471 foreach my $agent_custid ( keys %referrals ) {
472     my $referred_cust = qsearchs('cust_main', 
473                                     { 'agentnum' => $dest_agentnum, 
474                                       'agent_custid' => $agent_custid,
475                                     }
476                                 );
477     $referred_cust->referral_custnum($map{'cust_main'}->{$referrals{$agent_custid}});
478     $referred_cust->replace;
479 }
480
481
482 warn "import successful!\n";
483 if ( $DRY ) {
484   warn "rolling back (dry run)\n";
485   dbh->rollback or die dbh->errstr;
486   warn "rolled back\n"
487 } else {
488   warn "commiting\n";
489   dbh->commit or die dbh->errstr;
490   warn "committed\n";
491 }
492
493 sub import_table {
494   my( $table, %opt ) = @_;
495
496   eval "use FS::$table;";
497   die $@ if $@;
498
499   my $map = $opt{'map'} || 'cust_main';
500   my $search = $opt{'search'} || 'custnum';
501
502   $opt{'insert_opts'} ||= [];
503
504   my $primary_key = dbdef->table($table)->primary_key;
505
506   my $addl_from = defined($opt{'addl_from'}) ? $opt{'addl_from'} : '';
507
508   my $sth = $sdbh->prepare(
509     "SELECT * FROM $table $addl_from ".
510     ( $opt{'nomap'} ? '' : " WHERE $search = ". $opt{$search} )
511   ) or die $sdbh->errstr;
512
513   $sth->execute or die "(searching $table): ". $sth->errstr;
514
515   while ( my $row = $sth->fetchrow_hashref ) {
516     #my $src_custnum = $customerrow->{'custnum'};
517
518     my $hashref = { %$row };
519     $hashref->{$primary_key} = ''
520       unless $opt{'noblank_primary'};
521     $hashref->{ $search } = $map{$map}->{ $row->{$search} }
522       unless $opt{'nomap'};
523
524     if ( $opt{'map2'} ) {
525       my $key2 = $opt{'map2key'};
526       $hashref->{$key2} = $map{ $opt{'map2'} }->{ $row->{$key2} }
527         unless $opt{map2key} eq 'pkgnum' && (    $row->{$key2} eq '0'
528                                               || $row->{$key2} eq '-1'
529                                             )
530             or ! defined($row->{$key2})
531             or $row->{$key2} eq '';
532       #warn "map $opt{map2}.$opt{map2key}: ". $row->{$key2}. " to ". $map{ $opt{'map2'} }->{ $row->{$key2} };
533     }
534
535     if ( $opt{'map3'} ) {
536       my $key3 = $opt{'map3key'};
537       $hashref->{$key3} = $map{ $opt{'map3'} }->{ $row->{$key3} };
538     }
539
540     my $object = eval "new FS::$table \$hashref;";
541     die $@ if $@;
542
543     &{ $opt{preinsert_callback} }( $row, $object )
544       if $opt{preinsert_callback};
545
546     my $error = $object->insert( @{ $opt{'insert_opts'} } );
547     if ( $error ) {
548       warn "*** WARNING: error importing $table src $primary_key ". $row->{$primary_key}. ": $error";
549       next;
550     }
551
552     $map{ $table }->{ $row->{$primary_key} } = $object->get($primary_key);
553
554     &{ $opt{post_callback} }( $row->{$primary_key}, $object->get($primary_key) )
555       if $opt{post_callback};
556
557   }
558
559 }
560
561 1;
562