tax data update bug fixes and error message improvements
[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_snumbern('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->ut_enum('disabled', [ '', 'Y' ] )
223     || $self->SUPER::check
224     ;
225
226 }
227
228 =item taxclass_description
229
230 Returns the human understandable value associated with the related
231 FS::tax_class.
232
233 =cut
234
235 sub taxclass_description {
236   my $self = shift;
237   my $tax_class = qsearchs('tax_class', {'taxclassnum' => $self->taxclassnum });
238   $tax_class ? $tax_class->description : '';
239 }
240
241 =item unittype_name
242
243 Returns the human understandable value associated with the unittype column
244
245 =cut
246
247 %tax_unittypes = ( '0' => 'access line',
248                    '1' => 'minute',
249                    '2' => 'account',
250 );
251
252 sub unittype_name {
253   my $self = shift;
254   $tax_unittypes{$self->unittype};
255 }
256
257 =item maxtype_name
258
259 Returns the human understandable value associated with the maxtype column
260
261 =cut
262
263 %tax_maxtypes = ( '0' => 'receipts per invoice',
264                   '1' => 'receipts per item',
265                   '2' => 'total utility charges per utility tax year',
266                   '3' => 'total charges per utility tax year',
267                   '4' => 'receipts per access line',
268                   '9' => 'monthly receipts per location',
269 );
270
271 sub maxtype_name {
272   my $self = shift;
273   $tax_maxtypes{$self->maxtype};
274 }
275
276 =item basetype_name
277
278 Returns the human understandable value associated with the basetype column
279
280 =cut
281
282 %tax_basetypes = ( '0'  => 'sale price',
283                    '1'  => 'gross receipts',
284                    '2'  => 'sales taxable telecom revenue',
285                    '3'  => 'minutes carried',
286                    '4'  => 'minutes billed',
287                    '5'  => 'gross operating revenue',
288                    '6'  => 'access line',
289                    '7'  => 'account',
290                    '8'  => 'gross revenue',
291                    '9'  => 'portion gross receipts attributable to interstate service',
292                    '10' => 'access line',
293                    '11' => 'gross profits',
294                    '12' => 'tariff rate',
295                    '14' => 'account',
296 );
297
298 sub basetype_name {
299   my $self = shift;
300   $tax_basetypes{$self->basetype};
301 }
302
303 =item taxauth_name
304
305 Returns the human understandable value associated with the taxauth column
306
307 =cut
308
309 %tax_authorities = ( '0' => 'federal',
310                      '1' => 'state',
311                      '2' => 'county',
312                      '3' => 'city',
313                      '4' => 'local',
314                      '5' => 'county administered by state',
315                      '6' => 'city administered by state',
316                      '7' => 'city administered by county',
317                      '8' => 'local administered by state',
318                      '9' => 'local administered by county',
319 );
320
321 sub taxauth_name {
322   my $self = shift;
323   $tax_authorities{$self->taxauth};
324 }
325
326 =item passtype_name
327
328 Returns the human understandable value associated with the passtype column
329
330 =cut
331
332 %tax_passtypes = ( '0' => 'separate tax line',
333                    '1' => 'separate surcharge line',
334                    '2' => 'surcharge not separated',
335                    '3' => 'included in base rate',
336 );
337
338 sub passtype_name {
339   my $self = shift;
340   $tax_passtypes{$self->passtype};
341 }
342
343 =item taxline CUST_BILL_PKG|AMOUNT, ...
344
345 Returns a listref of a name and an amount of tax calculated for the list
346 of packages/amounts.  If an error occurs, a message is returned as a scalar.
347
348 =cut
349
350 sub taxline {
351   my $self = shift;
352
353   my $name = $self->taxname;
354   $name = 'Other surcharges'
355     if ($self->passtype == 2);
356   my $amount = 0;
357   
358   return [$name, $amount]  # we always know how to handle disabled taxes
359     if $self->disabled;
360
361   my $taxable_charged = 0;
362   my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
363
364   warn "calculating taxes for ". $self->taxnum. " on ".
365     join (",", map { $_->pkgnum } @cust_bill_pkg)
366     if $DEBUG;
367
368   if ($self->passflag eq 'N') {
369     return "fatal: can't (yet) handle taxes not passed to the customer";
370   }
371
372   if ($self->maxtype != 0 && $self->maxtype != 9) {
373     return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
374       '" threshold';
375   }
376
377   if ($self->maxtype == 9) {
378     return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name. 
379       '" threshold';  # "texas" tax
380   }
381
382   if ($self->basetype != 0 && $self->basetype != 1 &&
383       $self->basetype != 6 && $self->basetype != 7 &&
384       $self->basetype != 14
385   ) {
386     return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name. 
387       '" basis';
388   }
389
390   unless ($self->setuptax =~ /^Y$/i) {
391     $taxable_charged += $_->setup foreach @cust_bill_pkg;
392   }
393   unless ($self->recurtax =~ /^Y$/i) {
394     $taxable_charged += $_->recur foreach @cust_bill_pkg;
395   }
396
397   my $taxable_units = 0;
398   unless ($self->recurtax =~ /^Y$/i) {
399     if ($self->unittype == 0) {
400       $taxable_units += $_->units foreach @cust_bill_pkg;
401     }elsif ($self->unittype == 1) {
402       return qq!fatal: can't (yet) handle fee with minute unit type!;
403     }elsif ($self->unittype == 2) {
404       $taxable_units = 1;
405     }else {
406       return qq!fatal: can't (yet) handle unknown unit type in tax!.
407         $self->taxnum;
408     }
409   }
410
411   #
412   # XXX insert exemption handling here
413   #
414   # the tax or fee is applied to taxbase or feebase and then
415   # the excessrate or excess fee is applied to taxmax or feemax
416   #
417
418   $amount += $taxable_charged * $self->tax;
419   $amount += $taxable_units * $self->fee;
420   
421   warn "calculated taxes as [ $name, $amount ]\n"
422     if $DEBUG;
423
424   return [$name, $amount];
425
426 }
427
428 =item tax_on_tax CUST_MAIN
429
430 Returns a list of taxes which are candidates for taxing taxes for the
431 given customer (see L<FS::cust_main>)
432
433 =cut
434
435 sub tax_on_tax {
436   my $self = shift;
437   my $cust_main = shift;
438
439   warn "looking up taxes on tax ". $self->taxnum. " for customer ".
440     $cust_main->custnum
441     if $DEBUG;
442
443   my $geocode = $cust_main->geocode($self->data_vendor);
444
445   # CCH oddness in m2m
446   my $dbh = dbh;
447   my $extra_sql = ' AND ('.
448     join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
449                  qw(10 5 2)
450         ).
451     ')';
452
453   my $order_by = 'ORDER BY taxclassnum, length(geocode) desc';
454   my $select   = 'DISTINCT ON(taxclassnum) *';
455
456   # should qsearch preface columns with the table to facilitate joins?
457   my @taxclassnums = map { $_->taxclassnum }
458     qsearch( { 'table'     => 'part_pkg_taxrate',
459                'select'    => $select,
460                'hashref'   => { 'data_vendor'      => $self->data_vendor,
461                                 'taxclassnumtaxed' => $self->taxclassnum,
462                               },
463                'extra_sql' => $extra_sql,
464                'order_by'  => $order_by,
465            } );
466
467   return () unless @taxclassnums;
468
469   $extra_sql =
470     "AND (".  join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
471
472   qsearch({ 'table'     => 'tax_rate',
473             'hashref'   => { 'geocode' => $geocode, },
474             'extra_sql' => $extra_sql,
475          })
476
477 }
478
479 =back
480
481 =head1 SUBROUTINES
482
483 =over 4
484
485 =item batch_import
486
487 =cut
488
489 sub batch_import {
490   my ($param, $job) = @_;
491
492   my $fh = $param->{filehandle};
493   my $format = $param->{'format'};
494
495   my %insert = ();
496   my %delete = ();
497
498   my @fields;
499   my $hook;
500
501   my $line;
502   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
503   if ( $job ) {
504     $count++
505       while ( defined($line=<$fh>) );
506     seek $fh, 0, 0;
507   }
508   $count *=2;
509
510   if ( $format eq 'cch' || $format eq 'cch-update' ) {
511     @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax
512                   excessrate effective_date taxauth taxtype taxcat taxname
513                   usetax useexcessrate fee unittype feemax maxtype passflag
514                   passtype basetype );
515     push @fields, 'actionflag' if $format eq 'cch-update';
516
517     $hook = sub {
518       my $hash = shift;
519
520       $hash->{'actionflag'} ='I' if ($hash->{'data_vendor'} eq 'cch');
521       $hash->{'data_vendor'} ='cch';
522       $hash->{'effective_date'} = str2time($hash->{'effective_date'});
523
524       my $taxclassid =
525         join(':', map{ $hash->{$_} } qw(taxtype taxcat) );
526
527       my %tax_class = ( 'data_vendor'  => 'cch', 
528                         'taxclass' => $taxclassid,
529                       );
530
531       my $tax_class = qsearchs( 'tax_class', \%tax_class );
532       return "Error updating tax rate: no tax class $taxclassid"
533         unless $tax_class;
534
535       $hash->{'taxclassnum'} = $tax_class->taxclassnum;
536
537       foreach (qw( inoutcity inoutlocal taxtype taxcat )) {
538         delete($hash->{$_});
539       }
540
541       my %passflagmap = ( '0' => '',
542                           '1' => 'Y',
543                           '2' => 'N',
544                         );
545       $hash->{'passflag'} = $passflagmap{$hash->{'passflag'}}
546         if exists $passflagmap{$hash->{'passflag'}};
547
548       foreach (keys %$hash) {
549         $hash->{$_} = substr($hash->{$_}, 0, 80)
550           if length($hash->{$_}) > 80;
551       }
552
553       my $actionflag = delete($hash->{'actionflag'});
554       if ($actionflag eq 'I') {
555         $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
556       }elsif ($actionflag eq 'D') {
557         $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
558       }else{
559         return "Unexpected action flag: ". $hash->{'actionflag'};
560       }
561
562       '';
563
564     };
565
566   } elsif ( $format eq 'extended' ) {
567     die "unimplemented\n";
568     @fields = qw( );
569     $hook = sub {};
570   } else {
571     die "unknown format $format";
572   }
573
574   eval "use Text::CSV_XS;";
575   die $@ if $@;
576
577   my $csv = new Text::CSV_XS;
578
579   my $imported = 0;
580
581   local $SIG{HUP} = 'IGNORE';
582   local $SIG{INT} = 'IGNORE';
583   local $SIG{QUIT} = 'IGNORE';
584   local $SIG{TERM} = 'IGNORE';
585   local $SIG{TSTP} = 'IGNORE';
586   local $SIG{PIPE} = 'IGNORE';
587
588   my $oldAutoCommit = $FS::UID::AutoCommit;
589   local $FS::UID::AutoCommit = 0;
590   my $dbh = dbh;
591   
592   while ( defined($line=<$fh>) ) {
593     $csv->parse($line) or do {
594       $dbh->rollback if $oldAutoCommit;
595       return "can't parse: ". $csv->error_input();
596     };
597
598     if ( $job ) {  # progress bar
599       if ( time - $min_sec > $last ) {
600         my $error = $job->update_statustext(
601           int( 100 * $imported / $count )
602         );
603         die $error if $error;
604         $last = time;
605       }
606     }
607
608     my @columns = $csv->fields();
609
610     my %tax_rate = ( 'data_vendor' => $format );
611     foreach my $field ( @fields ) {
612       $tax_rate{$field} = shift @columns; 
613     }
614     if ( scalar( @columns ) ) {
615       $dbh->rollback if $oldAutoCommit;
616       return "Unexpected trailing columns in line (wrong format?): $line";
617     }
618
619     my $error = &{$hook}(\%tax_rate);
620     if ( $error ) {
621       $dbh->rollback if $oldAutoCommit;
622       return $error;
623     }
624
625     $imported++;
626
627   }
628
629   for (grep { !exists($delete{$_}) } keys %insert) {
630     if ( $job ) {  # progress bar
631       if ( time - $min_sec > $last ) {
632         my $error = $job->update_statustext(
633           int( 100 * $imported / $count )
634         );
635         die $error if $error;
636         $last = time;
637       }
638     }
639
640     my $tax_rate = new FS::tax_rate( $insert{$_} );
641     my $error = $tax_rate->insert;
642
643     if ( $error ) {
644       $dbh->rollback if $oldAutoCommit;
645       my $hashref = $insert{$_};
646       $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
647       return "can't insert tax_rate for $line: $error";
648     }
649
650     $imported++;
651   }
652
653   for (grep { exists($delete{$_}) } keys %insert) {
654     if ( $job ) {  # progress bar
655       if ( time - $min_sec > $last ) {
656         my $error = $job->update_statustext(
657           int( 100 * $imported / $count )
658         );
659         die $error if $error;
660         $last = time;
661       }
662     }
663
664     my $old = qsearchs( 'tax_rate', $delete{$_} );
665     unless ($old) {
666       $dbh->rollback if $oldAutoCommit;
667       $old = $delete{$_};
668       return "can't find tax_rate to replace for: ".
669         #join(" ", map { "$_ => ". $old->{$_} } @fields);
670         join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
671     }
672     my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => ''  });
673     $new->taxnum($old->taxnum);
674     my $error = $new->replace($old);
675
676     if ( $error ) {
677       $dbh->rollback if $oldAutoCommit;
678       my $hashref = $insert{$_};
679       $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
680       return "can't replace tax_rate for $line: $error";
681     }
682
683     $imported++;
684     $imported++;
685   }
686
687   for (grep { !exists($insert{$_}) } keys %delete) {
688     if ( $job ) {  # progress bar
689       if ( time - $min_sec > $last ) {
690         my $error = $job->update_statustext(
691           int( 100 * $imported / $count )
692         );
693         die $error if $error;
694         $last = time;
695       }
696     }
697
698     my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
699     unless ($tax_rate) {
700       $dbh->rollback if $oldAutoCommit;
701       $tax_rate = $delete{$_};
702       return "can't find tax_rate to delete for: ".
703         #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
704         join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
705     }
706     my $error = $tax_rate->delete;
707
708     if ( $error ) {
709       $dbh->rollback if $oldAutoCommit;
710       my $hashref = $delete{$_};
711       $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
712       return "can't delete tax_rate for $line: $error";
713     }
714
715     $imported++;
716   }
717
718   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
719
720   return "Empty file!" unless $imported;
721
722   ''; #no error
723
724 }
725
726 =item process_batch
727
728 Load a batch import as a queued JSRPC job
729
730 =cut
731
732 sub process_batch {
733   my $job = shift;
734
735   my $param = thaw(decode_base64(shift));
736   my $format = $param->{'format'};        #well... this is all cch specific
737
738   my $files = $param->{'uploaded_files'}
739     or die "No files provided.";
740
741   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
742
743   if ($format eq 'cch') {
744
745     my $oldAutoCommit = $FS::UID::AutoCommit;
746     local $FS::UID::AutoCommit = 0;
747     my $dbh = dbh;
748     my $error = '';
749
750     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
751                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
752                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
753                  'DETAIL',   'detail',    \&FS::tax_rate::batch_import,
754                );
755     while( scalar(@list) ) {
756       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
757       unless ($files{$file}) {
758         $error = "No $name supplied";
759         next;
760       }
761       my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
762       my $filename = "$dir/".  $files{$file};
763       open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
764
765       $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
766       close $fh;
767       unlink $filename or warn "Can't delete $filename: $!";
768     }
769     
770     if ($error) {
771       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
772       die $error;
773     }else{
774       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
775     }
776
777   }elsif ($format eq 'cch-update') {
778
779     my $oldAutoCommit = $FS::UID::AutoCommit;
780     local $FS::UID::AutoCommit = 0;
781     my $dbh = dbh;
782     my $error = '';
783     my @insert_list = ();
784     my @delete_list = ();
785
786     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
787                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
788                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
789                );
790     my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
791     while( scalar(@list) ) {
792       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
793       unless ($files{$file}) {
794         $error = "No $name supplied";
795         next;
796       }
797       my $filename = "$dir/".  $files{$file};
798       open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
799       unlink $filename or warn "Can't delete $filename: $!";
800
801       my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
802                                 DIR      => $dir,
803                                 UNLINK   => 0,     #meh
804                               ) or die "can't open temp file: $!\n";
805
806       my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
807                                 DIR      => $dir,
808                                 UNLINK   => 0,     #meh
809                               ) or die "can't open temp file: $!\n";
810
811       while(<$fh>) {
812         my $handle = '';
813         $handle = $ifh if $_ =~ /"I"\s*$/;
814         $handle = $dfh if $_ =~ /"D"\s*$/;
815         unless ($handle) {
816           $error = "bad input line: $_" unless $handle;
817           last;
818         }
819         print $handle $_;
820       }
821       close $fh;
822       close $ifh;
823       close $dfh;
824
825       push @insert_list, $name, $ifh->filename, $import_sub;
826       unshift @delete_list, $name, $dfh->filename, $import_sub;
827
828     }
829     while( scalar(@insert_list) ) {
830       my ($name, $file, $import_sub) =
831         (shift @insert_list, shift @insert_list, shift @insert_list);
832
833       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
834       $error ||=
835         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
836       close $fh;
837       unlink $file or warn "Can't delete $file: $!";
838     }
839     
840     $error ||= "No DETAIL supplied"
841       unless ($files{detail});
842     open my $fh, "< $dir/". $files{detail}
843       or $error ||= "Can't open DETAIL file: $!";
844     $error ||=
845       &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
846                                   $job);
847     close $fh;
848     unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
849       if $files{detail};
850
851     while( scalar(@delete_list) ) {
852       my ($name, $file, $import_sub) =
853         (shift @delete_list, shift @delete_list, shift @delete_list);
854
855       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
856       $error ||=
857         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
858       close $fh;
859       unlink $file or warn "Can't delete $file: $!";
860     }
861     
862     if ($error) {
863       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
864       die $error;
865     }else{
866       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
867     }
868
869   }else{
870     die "Unknown format: $format";
871   }
872
873 }
874
875 =item browse_queries PARAMS
876
877 Returns a list consisting of a hashref suited for use as the argument
878 to qsearch, and sql query string.  Each is based on the PARAMS hashref
879 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
880 from a form.  This conveniently creates the query hashref and count_query
881 string required by the browse and search elements.  As a side effect, 
882 the PARAMS hashref is untainted and keys with unexpected values are removed.
883
884 =cut
885
886 sub browse_queries {
887   my $params = shift;
888
889   my $query = {
890                 'table'     => 'tax_rate',
891                 'hashref'   => {},
892                 'order_by'  => 'ORDER BY geocode, taxclassnum',
893               },
894
895   my $extra_sql = '';
896
897   if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
898     $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
899   } else {
900     delete $params->{data_vendor};
901   }
902    
903   if ( $params->{geocode} =~ /^(\w+)$/ ) {
904     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
905                     'geocode LIKE '. dbh->quote($1.'%');
906   } else {
907     delete $params->{geocode};
908   }
909
910   if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
911        qsearchs( 'tax_class', {'taxclassnum' => $1} )
912      )
913   {
914     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
915                   ' taxclassnum  = '. dbh->quote($1)
916   } else {
917     delete $params->{taxclassnun};
918   }
919
920   my $tax_type = $1
921     if ( $params->{tax_type} =~ /^(\d+)$/ );
922   delete $params->{tax_type}
923     unless $tax_type;
924
925   my $tax_cat = $1
926     if ( $params->{tax_cat} =~ /^(\d+)$/ );
927   delete $params->{tax_cat}
928     unless $tax_cat;
929
930   my @taxclassnum = ();
931   if ($tax_type || $tax_cat ) {
932     my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
933     $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
934     @taxclassnum = map { $_->taxclassnum } 
935                    qsearch({ 'table'     => 'tax_class',
936                              'hashref'   => {},
937                              'extra_sql' => "WHERE taxclass $compare",
938                           });
939   }
940
941   $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
942                 join(' OR ', map { " taxclassnum  = $_ " } @taxclassnum ). ' )'
943     if ( @taxclassnum );
944
945   unless ($params->{'showdisabled'}) {
946     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
947                   "( disabled = '' OR disabled IS NULL )";
948   }
949
950   $query->{extra_sql} = $extra_sql;
951
952   return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
953 }
954
955 =back
956
957 =head1 BUGS
958
959   Mixing automatic and manual editing works poorly at present.
960
961 =head1 SEE ALSO
962
963 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
964 documentation.
965
966 =cut
967
968 1;
969