add disabled column to new tax rates, false laziness elimination, and bug fixes ...
[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->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       return "can't insert tax_rate for $line: $error";
646     }
647
648     $imported++;
649   }
650
651   for (grep { exists($delete{$_}) } keys %insert) {
652     if ( $job ) {  # progress bar
653       if ( time - $min_sec > $last ) {
654         my $error = $job->update_statustext(
655           int( 100 * $imported / $count )
656         );
657         die $error if $error;
658         $last = time;
659       }
660     }
661
662     my $old = qsearchs( 'tax_rate', $delete{$_} );
663     unless ($old) {
664       $dbh->rollback if $oldAutoCommit;
665       $old = $delete{$_};
666       return "can't find tax_rate to replace for: ".
667         #join(" ", map { "$_ => ". $old->{$_} } @fields);
668         join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
669     }
670     my $new = new FS::tax_rate( $insert{$_} );
671     $new->taxnum($old->taxnum);
672     my $error = $new->replace($old);
673
674     if ( $error ) {
675       $dbh->rollback if $oldAutoCommit;
676       return "can't insert tax_rate for $line: $error";
677     }
678
679     $imported++;
680     $imported++;
681   }
682
683   for (grep { !exists($insert{$_}) } keys %delete) {
684     if ( $job ) {  # progress bar
685       if ( time - $min_sec > $last ) {
686         my $error = $job->update_statustext(
687           int( 100 * $imported / $count )
688         );
689         die $error if $error;
690         $last = time;
691       }
692     }
693
694     my $tax_rate = qsearchs( 'tax_rate', $delete{$_} );
695     unless ($tax_rate) {
696       $dbh->rollback if $oldAutoCommit;
697       $tax_rate = $delete{$_};
698       return "can't find tax_rate to delete for: ".
699         #join(" ", map { "$_ => ". $tax_rate->{$_} } @fields);
700         join(" ", map { "$_ => ". $tax_rate->{$_} } keys(%$tax_rate) );
701     }
702     my $error = $tax_rate->delete;
703
704     if ( $error ) {
705       $dbh->rollback if $oldAutoCommit;
706       return "can't insert tax_rate for $line: $error";
707     }
708
709     $imported++;
710   }
711
712   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
713
714   return "Empty file!" unless $imported;
715
716   ''; #no error
717
718 }
719
720 =item process_batch
721
722 Load an batch import as a queued JSRPC job
723
724 =cut
725
726 sub process_batch {
727   my $job = shift;
728
729   my $param = thaw(decode_base64(shift));
730   my $format = $param->{'format'};        #well... this is all cch specific
731
732   my $files = $param->{'uploaded_files'}
733     or die "No files provided.";
734
735   my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
736
737   if ($format eq 'cch') {
738
739     my $oldAutoCommit = $FS::UID::AutoCommit;
740     local $FS::UID::AutoCommit = 0;
741     my $dbh = dbh;
742     my $error = '';
743
744     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
745                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
746                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
747                  'DETAIL',   'detail',    \&FS::tax_rate::batch_import,
748                );
749     while( scalar(@list) ) {
750       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
751       unless ($files{$file}) {
752         $error = "No $name supplied";
753         next;
754       }
755       my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
756       my $filename = "$dir/".  $files{$file};
757       open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
758
759       $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
760       close $fh;
761       unlink $filename or warn "Can't delete $filename: $!";
762     }
763     
764     if ($error) {
765       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
766       die $error;
767     }else{
768       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
769     }
770
771   }elsif ($format eq 'cch-update') {
772
773     my $oldAutoCommit = $FS::UID::AutoCommit;
774     local $FS::UID::AutoCommit = 0;
775     my $dbh = dbh;
776     my $error = '';
777     my @insert_list = ();
778     my @delete_list = ();
779
780     my @list = ( 'CODE',     'codefile',  \&FS::tax_class::batch_import,
781                  'PLUS4',    'plus4file', \&FS::cust_tax_location::batch_import,
782                  'TXMATRIX', 'txmatrix',  \&FS::part_pkg_taxrate::batch_import,
783                );
784     my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
785     while( scalar(@list) ) {
786       my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
787       unless ($files{$file}) {
788         $error = "No $name supplied";
789         next;
790       }
791       my $filename = "$dir/".  $files{$file};
792       open my $fh, "< $filename" or $error ||= "Can't open $name file $filename: $!";
793       unlink $filename or warn "Can't delete $filename: $!";
794
795       my $ifh = new File::Temp( TEMPLATE => "$name.insert.XXXXXXXX",
796                                 DIR      => $dir,
797                                 UNLINK   => 0,     #meh
798                               ) or die "can't open temp file: $!\n";
799
800       my $dfh = new File::Temp( TEMPLATE => "$name.delete.XXXXXXXX",
801                                 DIR      => $dir,
802                                 UNLINK   => 0,     #meh
803                               ) or die "can't open temp file: $!\n";
804
805       while(<$fh>) {
806         my $handle = '';
807         $handle = $ifh if $_ =~ /"I"\s*$/;
808         $handle = $dfh if $_ =~ /"D"\s*$/;
809         unless ($handle) {
810           $error = "bad input line: $_" unless $handle;
811           last;
812         }
813         print $handle $_;
814       }
815       close $fh;
816       close $ifh;
817       close $dfh;
818
819       push @insert_list, $name, $ifh->filename, $import_sub;
820       unshift @delete_list, $name, $dfh->filename, $import_sub;
821
822     }
823     while( scalar(@insert_list) ) {
824       my ($name, $file, $import_sub) =
825         (shift @insert_list, shift @insert_list, shift @insert_list);
826
827       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
828       $error ||=
829         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
830       close $fh;
831       unlink $file or warn "Can't delete $file: $!";
832     }
833     
834     $error = "No DETAIL supplied"
835       unless ($files{detail});
836     open my $fh, "< $dir/". $files{detail}
837       or $error ||= "Can't open DETAIL file: $!";
838     $error ||=
839       &FS::tax_rate::batch_import({ 'filehandle' => $fh, 'format' => $format },
840                                   $job);
841     close $fh;
842     unlink "$dir/". $files{detail} or warn "Can't delete $files{detail}: $!"
843       if $files{detail};
844
845     while( scalar(@delete_list) ) {
846       my ($name, $file, $import_sub) =
847         (shift @delete_list, shift @delete_list, shift @delete_list);
848
849       open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
850       $error ||=
851         &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
852       close $fh;
853       unlink $file or warn "Can't delete $file: $!";
854     }
855     
856     if ($error) {
857       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
858       die $error;
859     }else{
860       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
861     }
862
863   }else{
864     die "Unknown format: $format";
865   }
866
867 }
868
869 =item browse_queries PARAMS
870
871 Returns a list consisting of a hashref suited for use as the argument
872 to qsearch, and sql query string.  Each is based on the PARAMS hashref
873 of keys and values which frequently would be passed as C<scalar($cgi->Vars)>
874 from a form.  This conveniently creates the query hashref and count_query
875 string required by the browse and search elements.  As a side effect, 
876 the PARAMS hashref is untainted and keys with unexpected values are removed.
877
878 =cut
879
880 sub browse_queries {
881   my $params = shift;
882
883   my $query = {
884                 'table'     => 'tax_rate',
885                 'hashref'   => {},
886                 'order_by'  => 'ORDER BY geocode, taxclassnum',
887               },
888
889   my $extra_sql = '';
890
891   if ( $params->{data_vendor} =~ /^(\w+)$/ ) {
892     $extra_sql .= ' WHERE data_vendor = '. dbh->quote($1);
893   } else {
894     delete $params->{data_vendor};
895   }
896    
897   if ( $params->{geocode} =~ /^(\w+)$/ ) {
898     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
899                     'geocode LIKE '. dbh->quote($1.'%');
900   } else {
901     delete $params->{geocode};
902   }
903
904   if ( $params->{taxclassnum} =~ /^(\d+)$/ &&
905        qsearchs( 'tax_class', {'taxclassnum' => $1} )
906      )
907   {
908     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
909                   ' taxclassnum  = '. dbh->quote($1)
910   } else {
911     delete $params->{taxclassnun};
912   }
913
914   my $tax_type = $1
915     if ( $params->{tax_type} =~ /^(\d+)$/ );
916   delete $params->{tax_type}
917     unless $tax_type;
918
919   my $tax_cat = $1
920     if ( $params->{tax_cat} =~ /^(\d+)$/ );
921   delete $params->{tax_cat}
922     unless $tax_cat;
923
924   my @taxclassnum = ();
925   if ($tax_type || $tax_cat ) {
926     my $compare = "LIKE '". ( $tax_type || "%" ). ":". ( $tax_cat || "%" ). "'";
927     $compare = "= '$tax_type:$tax_cat'" if ($tax_type && $tax_cat);
928     @taxclassnum = map { $_->taxclassnum } 
929                    qsearch({ 'table'     => 'tax_class',
930                              'hashref'   => {},
931                              'extra_sql' => "WHERE taxclass $compare",
932                           });
933   }
934
935   $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ). '( '.
936                 join(' OR ', map { " taxclassnum  = $_ " } @taxclassnum ). ' )'
937     if ( @taxclassnum );
938
939   unless ($params->{'showdisabled'}) {
940     $extra_sql .= ( $extra_sql =~ /WHERE/i ? ' AND ' : ' WHERE ' ).
941                   "( disabled = '' OR disabled IS NULL )";
942   }
943
944   $query->{extra_sql} = $extra_sql;
945
946   return ($query, "SELECT COUNT(*) FROM tax_rate $extra_sql");
947 }
948
949 =back
950
951 =head1 BUGS
952
953   Mixing automatic and manual editing works poorly at present.
954
955 =head1 SEE ALSO
956
957 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
958 documentation.
959
960 =cut
961
962 1;
963