tax on tax
[freeside.git] / FS / FS / tax_rate.pm
1 package FS::tax_rate;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me
5              %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities
6              %tax_passtypes );
7 use Date::Parse;
8 use Storable qw( thaw );
9 use MIME::Base64;
10 use FS::Record qw( qsearch qsearchs dbh );
11 use FS::tax_class;
12 use FS::cust_bill_pkg;
13 use FS::cust_tax_location;
14 use FS::part_pkg_taxrate;
15 use FS::cust_main;
16
17 @ISA = qw( FS::Record );
18
19 $DEBUG = 0;
20 $me = '[FS::tax_rate]';
21
22 =head1 NAME
23
24 FS::tax_rate - Object methods for tax_rate objects
25
26 =head1 SYNOPSIS
27
28   use FS::tax_rate;
29
30   $record = new FS::tax_rate \%hash;
31   $record = new FS::tax_rate { 'column' => 'value' };
32
33   $error = $record->insert;
34
35   $error = $new_record->replace($old_record);
36
37   $error = $record->delete;
38
39   $error = $record->check;
40
41 =head1 DESCRIPTION
42
43 An FS::tax_rate object represents a tax rate, defined by locale.
44 FS::tax_rate inherits from FS::Record.  The following fields are
45 currently supported:
46
47 =over 4
48
49 =item taxnum
50
51 primary key (assigned automatically for new tax rates)
52
53 =item geocode
54
55 a geographic location code provided by a tax data vendor
56
57 =item data_vendor
58
59 the tax data vendor
60
61 =item location
62
63 a location code provided by a tax authority
64
65 =item taxclassnum
66
67 a foreign key into FS::tax_class - the type of tax
68 referenced but FS::part_pkg_taxrate
69 eitem effective_date
70
71 the time after which the tax applies
72
73 =item tax
74
75 percentage
76
77 =item excessrate
78
79 second bracket percentage 
80
81 =item taxbase
82
83 the amount to which the tax applies (first bracket)
84
85 =item taxmax
86
87 a cap on the amount of tax if a cap exists
88
89 =item usetax
90
91 percentage on out of jurisdiction purchases
92
93 =item useexcessrate
94
95 second bracket percentage on out of jurisdiction purchases
96
97 =item unittype
98
99 one of the values in %tax_unittypes
100
101 =item fee
102
103 amount of tax per unit
104
105 =item excessfee
106
107 second bracket amount of tax per unit
108
109 =item feebase
110
111 the number of units to which the fee applies (first bracket)
112
113 =item feemax
114
115 the most units to which fees apply (first and second brackets)
116
117 =item maxtype
118
119 a value from %tax_maxtypes indicating how brackets accumulate (i.e. monthly, per invoice, etc)
120
121 =item taxname
122
123 if defined, printed on invoices instead of "Tax"
124
125 =item taxauth
126
127 a value from %tax_authorities
128
129 =item basetype
130
131 a value from %tax_basetypes indicating the tax basis
132
133 =item passtype
134
135 a value from %tax_passtypes indicating how the tax should displayed to the customer
136
137 =item passflag
138
139 'Y', 'N', or blank indicating the tax can be passed to the customer
140
141 =item setuptax
142
143 if 'Y', this tax does not apply to setup fees
144
145 =item recurtax
146
147 if 'Y', this tax does not apply to recurring fees
148
149 =item manual
150
151 if 'Y', has been manually edited
152
153 =back
154
155 =head1 METHODS
156
157 =over 4
158
159 =item new HASHREF
160
161 Creates a new tax rate.  To add the tax rate to the database, see L<"insert">.
162
163 =cut
164
165 sub table { 'tax_rate'; }
166
167 =item insert
168
169 Adds this tax rate to the database.  If there is an error, returns the error,
170 otherwise returns false.
171
172 =item delete
173
174 Deletes this tax rate from the database.  If there is an error, returns the
175 error, otherwise returns false.
176
177 =item replace OLD_RECORD
178
179 Replaces the OLD_RECORD with this one in the database.  If there is an error,
180 returns the error, otherwise returns false.
181
182 =item check
183
184 Checks all fields to make sure this is a valid tax rate.  If there is an error,
185 returns the error, otherwise returns false.  Called by the insert and replace
186 methods.
187
188 =cut
189
190 sub check {
191   my $self = shift;
192
193   foreach (qw( taxbase taxmax )) {
194     $self->$_(0) unless $self->$_;
195   }
196
197   $self->ut_numbern('taxnum')
198     || $self->ut_text('geocode')
199     || $self->ut_textn('data_vendor')
200     || $self->ut_textn('location')
201     || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
202     || $self->ut_numbern('effective_date')
203     || $self->ut_float('tax')
204     || $self->ut_floatn('excessrate')
205     || $self->ut_money('taxbase')
206     || $self->ut_money('taxmax')
207     || $self->ut_floatn('usetax')
208     || $self->ut_floatn('useexcessrate')
209     || $self->ut_numbern('unittype')
210     || $self->ut_floatn('fee')
211     || $self->ut_floatn('excessfee')
212     || $self->ut_floatn('feemax')
213     || $self->ut_numbern('maxtype')
214     || $self->ut_textn('taxname')
215     || $self->ut_numbern('taxauth')
216     || $self->ut_numbern('basetype')
217     || $self->ut_numbern('passtype')
218     || $self->ut_enum('passflag', [ '', 'Y', 'N' ])
219     || $self->ut_enum('setuptax', [ '', 'Y' ] )
220     || $self->ut_enum('recurtax', [ '', 'Y' ] )
221     || $self->ut_enum('manual', [ '', 'Y' ] )
222     || $self->SUPER::check
223     ;
224
225 }
226
227 =item taxclass_description
228
229 Returns the human understandable value associated with the related
230 FS::tax_class.
231
232 =cut
233
234 sub taxclass_description {
235   my $self = shift;
236   my $tax_class = qsearchs('tax_class', {'taxclassnum' => $self->taxclassnum });
237   $tax_class ? $tax_class->description : '';
238 }
239
240 =item unittype_name
241
242 Returns the human understandable value associated with the unittype column
243
244 =cut
245
246 %tax_unittypes = ( '0' => 'access line',
247                    '1' => 'minute',
248                    '2' => 'account',
249 );
250
251 sub unittype_name {
252   my $self = shift;
253   $tax_unittypes{$self->unittype};
254 }
255
256 =item maxtype_name
257
258 Returns the human understandable value associated with the maxtype column
259
260 =cut
261
262 %tax_maxtypes = ( '0' => 'receipts per invoice',
263                   '1' => 'receipts per item',
264                   '2' => 'total utility charges per utility tax year',
265                   '3' => 'total charges per utility tax year',
266                   '4' => 'receipts per access line',
267                   '9' => 'monthly receipts per location',
268 );
269
270 sub maxtype_name {
271   my $self = shift;
272   $tax_maxtypes{$self->maxtype};
273 }
274
275 =item basetype_name
276
277 Returns the human understandable value associated with the basetype column
278
279 =cut
280
281 %tax_basetypes = ( '0'  => 'sale price',
282                    '1'  => 'gross receipts',
283                    '2'  => 'sales taxable telecom revenue',
284                    '3'  => 'minutes carried',
285                    '4'  => 'minutes billed',
286                    '5'  => 'gross operating revenue',
287                    '6'  => 'access line',
288                    '7'  => 'account',
289                    '8'  => 'gross revenue',
290                    '9'  => 'portion gross receipts attributable to interstate service',
291                    '10' => 'access line',
292                    '11' => 'gross profits',
293                    '12' => 'tariff rate',
294                    '14' => 'account',
295 );
296
297 sub basetype_name {
298   my $self = shift;
299   $tax_basetypes{$self->basetype};
300 }
301
302 =item taxauth_name
303
304 Returns the human understandable value associated with the taxauth column
305
306 =cut
307
308 %tax_authorities = ( '0' => 'federal',
309                      '1' => 'state',
310                      '2' => 'county',
311                      '3' => 'city',
312                      '4' => 'local',
313                      '5' => 'county administered by state',
314                      '6' => 'city administered by state',
315                      '7' => 'city administered by county',
316                      '8' => 'local administered by state',
317                      '9' => 'local administered by county',
318 );
319
320 sub taxauth_name {
321   my $self = shift;
322   $tax_authorities{$self->taxauth};
323 }
324
325 =item passtype_name
326
327 Returns the human understandable value associated with the passtype column
328
329 =cut
330
331 %tax_passtypes = ( '0' => 'separate tax line',
332                    '1' => 'separate surcharge line',
333                    '2' => 'surcharge not separated',
334                    '3' => 'included in base rate',
335 );
336
337 sub passtype_name {
338   my $self = shift;
339   $tax_passtypes{$self->passtype};
340 }
341
342 =item taxline CUST_BILL_PKG|AMOUNT, ...
343
344 Returns a listref of a name and an amount of tax calculated for the list
345 of packages/amounts.  If an error occurs, a message is returned as a scalar.
346
347 =cut
348
349 sub taxline {
350   my $self = shift;
351
352   my $taxable_charged = 0;
353   my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
354
355   warn "calculating taxes for ". $self->taxnum. " on ".
356     join (",", map { $_->pkgnum } @cust_bill_pkg)
357     if $DEBUG;
358
359   if ($self->passflag eq 'N') {
360     return "fatal: can't (yet) handle taxes not passed to the customer";
361   }
362
363   if ($self->maxtype != 0 && $self->maxtype != 9) {
364     return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
365       '" threshold';
366   }
367
368   if ($self->maxtype == 9) {
369     return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
370       '" threshold';  # "texas" tax
371   }
372
373   if ($self->basetype != 0 && $self->basetype != 1 &&
374       $self->basetype != 6 && $self->basetype != 7 &&
375       $self->basetype != 14
376   ) {
377     return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name. 
378       '" basis';
379   }
380
381   my $name = $self->taxname;
382   $name = 'Other surcharges'
383     if ($self->passtype == 2);
384   my $amount = 0;
385   
386   unless ($self->setuptax =~ /^Y$/i) {
387     $taxable_charged += $_->setup foreach @cust_bill_pkg;
388   }
389   unless ($self->recurtax =~ /^Y$/i) {
390     $taxable_charged += $_->recur foreach @cust_bill_pkg;
391   }
392
393   my $taxable_units = 0;
394   unless ($self->recurtax =~ /^Y$/i) {
395     if ($self->unittype == 0) {
396       $taxable_units += $_->units foreach @cust_bill_pkg;
397     }elsif ($self->unittype == 1) {
398       return qq!fatal: can't (yet) handle fee with minute unit type!;
399     }elsif ($self->unittype == 2) {
400       $taxable_units = 1;
401     }else {
402       return qq!fatal: can't (yet) handle unknown unit type in tax!.
403         $self->taxnum;
404     }
405   }
406
407   #
408   # XXX insert exemption handling here
409   #
410   # the tax or fee is applied to taxbase or feebase and then
411   # the excessrate or excess fee is applied to taxmax or feemax
412   #
413
414   $amount += $taxable_charged * $self->tax;
415   $amount += $taxable_units * $self->fee;
416   
417   warn "calculated taxes as [ $name, $amount ]\n"
418     if $DEBUG;
419
420   return [$name, $amount];
421
422 }
423
424 =item tax_on_tax CUST_MAIN
425
426 Returns a list of taxes which are candidates for taxing taxes for the
427 given customer (see L<FS::cust_main>)
428
429 =cut
430
431 sub tax_on_tax {
432   my $self = shift;
433   my $cust_main = shift;
434
435   warn "looking up taxes on tax ". $self->taxnum. " for customer ".
436     $cust_main->custnum
437     if $DEBUG;
438
439   my $geocode = $cust_main->geocode($self->data_vendor);
440
441   # CCH oddness in m2m
442   my $dbh = dbh;
443   my $extra_sql = ' AND ('.
444     join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
445                  qw(10 5 2)
446         ).
447     ')';
448
449   my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
450   my $select   = 'DISTINCT ON(taxclassnum) *';
451
452   # should qsearch preface columns with the table to facilitate joins?
453   my @taxclassnums = map { $_->taxclassnum }
454     qsearch( { 'table'     => 'part_pkg_taxrate',
455                'select'    => $select,
456                'hashref'   => { 'data_vendor'      => $self->data_vendor,
457                                 'taxclassnumtaxed' => $self->taxclassnum,
458                               },
459                'extra_sql' => $extra_sql,
460                'order_by'  => $order_by,
461            } );
462
463   return () unless @taxclassnums;
464
465   $extra_sql =
466     "AND (".  join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
467
468   qsearch({ 'table'     => 'tax_rate',
469             'hashref'   => { 'geocode' => $geocode, },
470             'extra_sql' => $extra_sql,
471          })
472
473 }
474
475 =back
476
477 =head1 SUBROUTINES
478
479 =over 4
480
481 =item batch_import
482
483 =cut
484
485 sub batch_import {
486   my ($param, $job) = @_;
487
488   my $fh = $param->{filehandle};
489   my $format = $param->{'format'};
490
491   my %insert = ();
492   my %delete = ();
493
494   my @fields;
495   my $hook;
496
497   my $line;
498   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
499   if ( $job ) {
500     $count++
501       while ( defined($line=<$fh>) );
502     seek $fh, 0, 0;
503   }
504   $count *=2;
505
506   if ( $format eq 'cch' || $format eq 'cch-update' ) {
507     @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
508                   excessrate effective_date taxauth taxtype taxcat taxname
509                   usetax useexcessrate fee unittype feemax maxtype passflag
510                   passtype basetype );
511     push @fields, 'actionflag' if $format eq 'cch-update';
512
513     $hook = sub {
514       my $hash = shift;
515
516       $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
517       $hash->{'data_vendor'} ='cch';
518       $hash->{'effective_date'} = str2time($hash->{'effective_date'});
519
520       my $taxclassid =
521         join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
522
523       my %tax_class = ( 'data_vendor'  => 'cch', 
524                         'taxclass' => $taxclassid,
525                       );
526
527       my $tax_class = qsearchs( 'tax_class', \%tax_class );
528       return "Error updating tax rate: no tax class $taxclassid"
529         unless $tax_class;
530
531       $hash->{'taxclassnum'} = $tax_class->taxclassnum;
532
533       foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
534         delete($hash->{$_});
535       }
536
537       my %passflagmap = ( '0' => '',
538                           '1' => 'Y',
539                           '2' => 'N',
540                         );
541       $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
542         if exists $passflagmap{$hash->{'passflag'}};
543
544       foreach (keys %$hash) {
545         $hash->{$_} = substr($hash->{$_}, 0, 80)
546           if length($hash->{$_}) > 80;
547       }
548
549       my $actionflag = delete($hash->{'actionflag'});
550       if ($actionflag eq 'I') {
551         $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
552       }elsif ($actionflag eq 'D') {
553         $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
554       }else{
555         return "Unexpected action flag: ". $hash->{'actionflag'};
556       }
557
558       '';
559
560     };
561
562   } elsif ( $format eq 'extended' ) {
563     die "unimplemented\n";
564     @fields = qw( );
565     $hook = sub {};
566   } else {
567     die "unknown format $format";
568   }
569
570   eval "use Text::CSV_XS;";
571   die $@ if $@;
572
573   my $csv = new Text::CSV_XS;
574
575   my $imported = 0;
576
577   local $SIG{HUP} = 'IGNORE';
578   local $SIG{INT} = 'IGNORE';
579   local $SIG{QUIT} = 'IGNORE';
580   local $SIG{TERM} = 'IGNORE';
581   local $SIG{TSTP} = 'IGNORE';
582   local $SIG{PIPE} = 'IGNORE';
583
584   my $oldAutoCommit = $FS::UID::AutoCommit;
585   local $FS::UID::AutoCommit = 0;
586   my $dbh = dbh;
587   
588   while ( defined($line=<$fh>) ) {
589     $csv->parse($line) or do {
590       $dbh->rollback if $oldAutoCommit;
591       return "can't parse: ". $csv->error_input();
592     };
593
594     if ( $job ) {  # progress bar
595       if ( time - $min_sec > $last ) {
596         my $error = $job->update_statustext(
597           int( 100 * $imported / $count )
598         );
599         die $error if $error;
600         $last = time;
601       }
602     }
603
604     my @columns = $csv->fields();
605
606     my %tax_rate = ( 'data_vendor' => $format );
607     foreach my $field ( @fields ) {
608       $tax_rate{$field} = shift @columns; 
609     }
610     if ( scalar( @columns ) ) {
611       $dbh->rollback if $oldAutoCommit;
612       return "Unexpected trailing columns in line (wrong format?): $line";
613     }
614
615     my $error = &{$hook}(\%tax_rate);
616     if ( $error ) {
617       $dbh->rollback if $oldAutoCommit;
618       return $error;
619     }
620
621     $imported++;
622
623   }
624
625   for (grep { !exists($delete{$_}) } keys %insert) {
626     if ( $job ) {  # progress bar
627       if ( time - $min_sec > $last ) {
628         my $error = $job->update_statustext(
629           int( 100 * $imported / $count )
630         );
631         die $error if $error;
632         $last = time;
633       }
634     }
635
636     my $tax_rate = new FS::tax_rate( $insert{$_} );
637     my $error = $tax_rate->insert;
638
639     if ( $error ) {
640       $dbh->rollback if $oldAutoCommit;
641       return "can't insert tax_rate for $line: $error";
642     }
643
644     $imported++;
645   }
646
647   for (grep { exists($delete{$_}) } keys %insert) {
648     if ( $job ) {  # progress bar
649       if ( time - $min_sec > $last ) {
650         my $error = $job->update_statustext(
651           int( 100 * $imported / $count )
652         );
653         die $error if $error;
654         $last = time;
655       }
656     }
657
658     my $old = qsearchs( 'tax_rate', $delete{$_} );
659     unless ($old) {
660       $dbh->rollback if $oldAutoCommit;
661       $old = $delete{$_};
662       return "can't find tax_rate to replace for: ".
663         #join(" ", map { "$_ => ". $old->{$_} } @fields);
664         join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
665     }
666     my $new = new FS::tax_rate( $insert{$_} );
667     $new->taxnum($old->taxnum);
668     my $error = $new->replace($old);
669
670     if ( $error ) {
671       $dbh->rollback if $oldAutoCommit;
672       return "can't insert tax_rate for $line: $error";
673     }
674
675     $imported++;
676     $imported++;
677   }
678
679   for (grep { !exists($insert{$_}) } keys %delete) {
680     if ( $job ) {  # progress bar
681       if ( time - $min_sec > $last ) {
682         my $error = $job->update_statustext(
683           int( 100 * $imported / $count )
684         );
685         die $error if $error;
686         $last = time;
687       }
688     }
689
690     my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
691     unless ($tax_rate) {
692       $dbh->rollback if $oldAutoCommit;
693       $tax_rate = $delete{$_};
694       return "can't find tax_rate to delete for: ".
695         #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
696         join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
697     }
698     my $error = $tax_rate->delete;
699
700     if ( $error ) {
701       $dbh->rollback if $oldAutoCommit;
702       return "can't insert tax_rate for $line: $error";
703     }
704
705     $imported++;
706   }
707
708   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
709
710   return "Empty file!" unless $imported;
711
712   ''; #no error
713
714 }
715
716 =item process_batch
717
718 Load an batch import as a queued JSRPC job
719
720 =cut
721
722 sub process_batch {
723   my $job = shift;
724
725   my $param = thaw(decode_base64(shift));
726   my $format = $param->{'format'};        #well... this is all cch specific
727
728   my $files = $param->{'uploaded_files'}
729     or die "No files provided.";
730
731   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
732
733   if ($format eq 'cch') {
734
735     my $oldAutoCommit = $FS::UID::AutoCommit;
736     local $FS::UID::AutoCommit = 0;
737     my $dbh = dbh;
738     my $error = '';
739
740     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
741                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
742                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
743                  'DETAIL',   'detail',    \&FS::tax_rate::batch_import,
744                );
745     while( scalar(@list) ) {
746       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
747       unless ($files{$file}) {
748         $error = "No $name supplied";
749         next;
750       }
751       my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
752       my $filename = "$dir/".  $files{$file};
753       open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
754
755       $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
756       close $fh;
757       unlink $filename or warn "Can't delete $filename: $!";
758     }
759     
760     if ($error) {
761       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
762       die $error;
763     }else{
764       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
765     }
766
767   }elsif ($format eq 'cch-update') {
768
769     my $oldAutoCommit = $FS::UID::AutoCommit;
770     local $FS::UID::AutoCommit = 0;
771     my $dbh = dbh;
772     my $error = '';
773     my @insert_list = ();
774     my @delete_list = ();
775
776     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
777                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
778                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
779                );
780     my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
781     while( scalar(@list) ) {
782       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
783       unless ($files{$file}) {
784         $error = "No $name supplied";
785         next;
786       }
787       my $filename = "$dir/".  $files{$file};
788       open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
789       unlink $filename or warn "Can't delete $filename: $!";
790
791       my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
792                                 DIR      => $dir,
793                                 UNLINK   => 0,     #meh
794                               ) or die "can't open temp file: $!\n";
795
796       my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
797                                 DIR      => $dir,
798                                 UNLINK   => 0,     #meh
799                               ) or die "can't open temp file: $!\n";
800
801       while(<$fh>) {
802         my $handle = '';
803         $handle = $ifh if $_ =~ /"I"\s*$/;
804         $handle = $dfh if $_ =~ /"D"\s*$/;
805         unless ($handle) {
806           $error = "bad input line: $_" unless $handle;
807           last;
808         }
809         print $handle $_;
810       }
811       close $fh;
812       close $ifh;
813       close $dfh;
814
815       push @insert_list, $name, $ifh->filename, $import_sub;
816       unshift @delete_list, $name, $dfh->filename, $import_sub;
817
818     }
819     while( scalar(@insert_list) ) {
820       my ($name, $file, $import_sub) =
821         (shift @insert_list, shift @insert_list, shift @insert_list);
822
823       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
824       $error ||=
825         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
826       close $fh;
827       unlink $file or warn "Can't delete $file: $!";
828     }
829     
830     $error = "No DETAIL supplied"
831       unless ($files{detail});
832     open my $fh, "< $dir/". $files{detail}
833       or $error ||= "Can't open DETAIL file: $!";
834     $error ||=
835       &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
836                                   $job);
837     close $fh;
838     unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
839       if $files{detail};
840
841     while( scalar(@delete_list) ) {
842       my ($name, $file, $import_sub) =
843         (shift @delete_list, shift @delete_list, shift @delete_list);
844
845       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
846       $error ||=
847         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
848       close $fh;
849       unlink $file or warn "Can't delete $file: $!";
850     }
851     
852     if ($error) {
853       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
854       die $error;
855     }else{
856       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
857     }
858
859   }else{
860     die "Unknown format: $format";
861   }
862
863 }
864
865 =back
866
867 =head1 BUGS
868
869   Mixing automatic and manual editing works poorly at present.
870
871 =head1 SEE ALSO
872
873 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
874 documentation.
875
876 =cut
877
878 1;
879