fix cch update adding a TAXCAT, RT#21687
[freeside.git] / FS / FS / tax_class.pm
1 package FS::tax_class;
2
3 use strict;
4 use vars qw( @ISA );
5 use FS::UID qw(dbh);
6 use FS::Record qw( qsearch qsearchs );
7 use FS::Misc qw( csv_from_fixed );
8
9 @ISA = qw(FS::Record);
10
11 =head1 NAME
12
13 FS::tax_class - Object methods for tax_class records
14
15 =head1 SYNOPSIS
16
17   use FS::tax_class;
18
19   $record = new FS::tax_class \%hash;
20   $record = new FS::tax_class { 'column' => 'value' };
21
22   $error = $record->insert;
23
24   $error = $new_record->replace($old_record);
25
26   $error = $record->delete;
27
28   $error = $record->check;
29
30 =head1 DESCRIPTION
31
32 An FS::tax_class object represents a tax class.  FS::tax_class
33 inherits from FS::Record.  The following fields are currently supported:
34
35 =over 4
36
37 =item taxclassnum
38
39 Primary key
40
41 =item data_vendor
42
43 Vendor of the tax data
44
45 =item taxclass
46
47 Tax class
48
49 =item description
50
51 Human readable description of the tax class
52
53 =back
54
55 =head1 METHODS
56
57 =over 4
58
59 =item new HASHREF
60
61 Creates a new tax class.  To add the tax class to the database, see L<"insert">.
62
63 Note that this stores the hash reference, not a distinct copy of the hash it
64 points to.  You can ask the object for a copy with the I<hash> method.
65
66 =cut
67
68 sub table { 'tax_class'; }
69
70 =item insert
71
72 Adds this record to the database.  If there is an error, returns the error,
73 otherwise returns false.
74
75 =cut
76
77 =item delete
78
79 Delete this record from the database.
80
81 =cut
82
83 sub delete {
84   my $self = shift;
85
86   return "Can't delete a tax class which has package tax rates!"
87     if qsearch( 'part_pkg_taxrate', { 'taxclassnumtaxed' => $self->taxclassnum } );
88
89   return "Can't delete a tax class which has package tax overrides!"
90     if qsearch( 'part_pkg_taxoverride', { 'taxclassnum' => $self->taxclassnum } );
91
92   local $SIG{HUP} = 'IGNORE';
93   local $SIG{INT} = 'IGNORE';
94   local $SIG{QUIT} = 'IGNORE';
95   local $SIG{TERM} = 'IGNORE';
96   local $SIG{TSTP} = 'IGNORE';
97   local $SIG{PIPE} = 'IGNORE';
98
99   my $oldAutoCommit = $FS::UID::AutoCommit;
100   local $FS::UID::AutoCommit = 0;
101   my $dbh = dbh;
102
103   foreach my $tax_rate (
104     qsearch( 'tax_rate', { taxclassnum=>$self->taxclassnum } )
105   ) {
106     my $error = $tax_rate->delete;
107     if ( $error ) {
108       $dbh->rollback if $oldAutoCommit;
109       return $error;
110     }
111   }
112
113   foreach my $part_pkg_taxrate (
114     qsearch( 'part_pkg_taxrate', { taxclassnum=>$self->taxclassnum } )
115   ) {
116     my $error = $part_pkg_taxrate->delete;
117     if ( $error ) {
118       $dbh->rollback if $oldAutoCommit;
119       return $error;
120     }
121   }
122
123   my $error = $self->SUPER::delete(@_);
124   if ( $error ) {
125     $dbh->rollback if $oldAutoCommit;
126     return $error;
127   }
128
129   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
130
131   '';
132
133 }
134
135 =item replace OLD_RECORD
136
137 Replaces the OLD_RECORD with this one in the database.  If there is an error,
138 returns the error, otherwise returns false.
139
140 =cut
141
142 =item check
143
144 Checks all fields to make sure this is a valid tax class.  If there is
145 an error, returns the error, otherwise returns false.  Called by the insert
146 and replace methods.
147
148 =cut
149
150 sub check {
151   my $self = shift;
152
153   my $error = 
154     $self->ut_numbern('taxclassnum')
155     || $self->ut_text('taxclass')
156     || $self->ut_textn('data_vendor')
157     || $self->ut_textn('description')
158   ;
159   return $error if $error;
160
161   $self->SUPER::check;
162 }
163
164 =item batch_import
165
166 Loads part_pkg_taxrate records from an external CSV file.  If there is
167 an error, returns the error, otherwise returns false. 
168
169 =cut 
170
171 sub batch_import {
172   my ($param, $job) = @_;
173
174   my $fh = $param->{filehandle};
175   my $format = $param->{'format'};
176
177   my @fields;
178   my $hook;
179   my $endhook;
180   my $data = {};
181   my $imported = 0;
182   my $dbh = dbh;
183
184   my @column_lengths = ();
185   my @column_callbacks = ();
186   if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
187     $format =~ s/-fixed//;
188     push @column_lengths, qw( 8 10 3 2 2 10 100 );
189     push @column_lengths, 1 if $format eq 'cch-update';
190   }
191
192   my $line;
193   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
194   if ( $job || scalar(@column_lengths) ) {
195     my $error = csv_from_fixed(\$fh, \$count, \@column_lengths);
196     return $error if $error;
197   }
198
199   if ( $format eq 'cch' || $format eq 'cch-update' ) {
200     @fields = qw( table name pos length number value description );
201     push @fields, 'actionflag' if $format eq 'cch-update';
202
203     $hook = sub { 
204       my $hash = shift;
205
206       if ($hash->{'table'} eq 'DETAIL') {
207         push @{$data->{'taxcat'}}, [ $hash->{'value'}, $hash->{'description'} ]
208           if ($hash->{'name'} eq 'TAXCAT' &&
209              (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
210
211         push @{$data->{'taxtype'}}, [ $hash->{'value'}, $hash->{'description'} ]
212           if ($hash->{'name'} eq 'TAXTYPE' &&
213              (!exists($hash->{actionflag}) || $hash->{actionflag} eq 'I') );
214
215         if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
216           my $name = $hash->{'name'};
217           my $value = $hash->{'value'};
218           return "Bad value for $name: $value"
219             unless $value =~ /^\d+$/;
220
221           if ($name eq 'TAXCAT' || $name eq 'TAXTYPE') {
222             my @tax_class = qsearch( 'tax_class',
223                                      { 'data_vendor' => 'cch' },
224                                      '',
225                                      "AND taxclass LIKE '".
226                                        ($name eq 'TAXTYPE' ? $value : '%').":".
227                                        ($name eq 'TAXCAT' ? $value : '%')."'",
228                                    );
229             foreach (@tax_class) {
230               my $error = $_->delete;
231               return $error if $error;
232             }
233           }
234         }
235
236       }
237
238       delete($hash->{$_})
239         for qw( data_vendor table name pos length number value description );
240       delete($hash->{actionflag}) if exists($hash->{actionflag});
241
242       '';
243
244     };
245
246     $endhook = sub { 
247
248       my $sql = "SELECT DISTINCT ".
249          "substring(taxclass from 1 for position(':' in taxclass)-1),".
250          "substring(description from 1 for position(':' in description)-1) ".
251          "FROM tax_class WHERE data_vendor='cch'";
252
253       my $sth = $dbh->prepare($sql) or die $dbh->errstr;
254       $sth->execute or die $sth->errstr;
255       my @old_types = @{$sth->fetchall_arrayref};
256
257       $sql = "SELECT DISTINCT ".
258          "substring(taxclass from position(':' in taxclass)+1),".
259          "substring(description from position(':' in description)+1) ".
260          "FROM tax_class WHERE data_vendor='cch'";
261
262       $sth = $dbh->prepare($sql) or die $dbh->errstr;
263       $sth->execute or die $sth->errstr;
264       my @old_cats = @{$sth->fetchall_arrayref};
265
266       my $catcount  = exists($data->{'taxcat'})  ? scalar(@{$data->{'taxcat'}})
267                                                  : 0;
268       my $typecount = exists($data->{'taxtype'}) ? scalar(@{$data->{'taxtype'}})
269                                                  : 0;
270
271       my $count = scalar(@old_types) * $catcount
272                 + $typecount * (scalar(@old_cats) + $catcount);
273
274       $imported = 1 if $format eq 'cch-update';  #empty file ok
275
276       foreach my $type (@old_types) {
277         foreach my $cat (@{$data->{'taxcat'}}) {
278
279           if ( $job ) {  # progress bar
280             if ( time - $min_sec > $last ) {
281               my $error = $job->update_statustext(
282                 int( 100 * $imported / $count ). ",Importing tax classes"
283               );
284               die $error if $error;
285               $last = time;
286             }
287           }
288
289           my %hash = ( 'data_vendor' => 'cch',
290                        'taxclass'    => $type->[0].':'.$cat->[0],
291                        'description' => $type->[1].':'.$cat->[1],
292                      );
293           unless ( qsearchs('tax_class', \%hash) ) {
294             my $tax_class = new FS::tax_class \%hash;
295             my $error = $tax_class->insert;
296
297             return "can't insert tax_class for ".
298                    " old TAXTYPE ". $type->[0].':'.$type->[1].
299                    " and new TAXCAT ". $cat->[0].':'. $cat->[1].
300                    " : $error"
301               if $error;
302           }
303
304           $imported++;
305           
306         }
307       }
308
309       foreach my $type (@{$data->{'taxtype'}}) {
310         foreach my $cat (@old_cats, @{$data->{'taxcat'}}) {
311
312           if ( $job ) {  # progress bar
313             if ( time - $min_sec > $last ) {
314               my $error = $job->update_statustext(
315                 int( 100 * $imported / $count ). ",Importing tax classes"
316               );
317               die $error if $error;
318               $last = time;
319             }
320           }
321
322           my $tax_class =
323             new FS::tax_class( { 'data_vendor' => 'cch',
324                                  'taxclass'    => $type->[0].':'.$cat->[0],
325                                  'description' => $type->[1].':'.$cat->[1],
326                              } );
327           my $error = $tax_class->insert;
328           return "can't insert tax_class for new TAXTYPE $type and TAXCAT $cat: $error" if $error;
329           $imported++;
330         }
331       }
332
333       '';
334     };
335
336   } elsif ( $format eq 'extended' ) {
337     die "unimplemented\n";
338     @fields = qw( );
339     $hook = sub {};
340   } else {
341     die "unknown format $format";
342   }
343
344   eval "use Text::CSV_XS;";
345   die $@ if $@;
346
347   my $csv = new Text::CSV_XS;
348
349   local $SIG{HUP} = 'IGNORE';
350   local $SIG{INT} = 'IGNORE';
351   local $SIG{QUIT} = 'IGNORE';
352   local $SIG{TERM} = 'IGNORE';
353   local $SIG{TSTP} = 'IGNORE';
354   local $SIG{PIPE} = 'IGNORE';
355
356   my $oldAutoCommit = $FS::UID::AutoCommit;
357   local $FS::UID::AutoCommit = 0;
358   
359   while ( defined($line=<$fh>) ) {
360
361     if ( $job ) {  # progress bar
362       if ( time - $min_sec > $last ) {
363         my $error = $job->update_statustext(
364           int( 100 * $imported / $count ). ",Importing tax classes"
365         );
366         die $error if $error;
367         $last = time;
368       }
369     }
370
371     $csv->parse($line) or do {
372       $dbh->rollback if $oldAutoCommit;
373       return "can't parse: ". $csv->error_input();
374     };
375
376     my @columns = $csv->fields();
377
378     my %tax_class = ( 'data_vendor' => $format );
379     foreach my $field ( @fields ) {
380       $tax_class{$field} = shift @columns; 
381     }
382     if ( scalar( @columns ) ) {
383       $dbh->rollback if $oldAutoCommit;
384       return "Unexpected trailing columns in line (wrong format?) importing tax_class: $line";
385     }
386
387     my $error = &{$hook}(\%tax_class);
388     if ( $error ) {
389       $dbh->rollback if $oldAutoCommit;
390       return $error;
391     }
392
393     next unless scalar(keys %tax_class);
394
395     my $tax_class = new FS::tax_class( \%tax_class );
396     $error = $tax_class->insert;
397     if ( $error ) {
398       $dbh->rollback if $oldAutoCommit;
399       return "can't insert tax_class for $line: $error";
400     }
401
402     $imported++;
403   }
404
405   my $error = &{$endhook}();
406   if ( $error ) {
407     $dbh->rollback if $oldAutoCommit;
408     return "can't run end hook: $error";
409   }
410
411   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
412
413   return "Empty File!" unless ($imported || $format eq 'cch-update');
414
415   ''; #no error
416
417 }
418
419 =back
420
421 =head1 BUGS
422
423 =head1 SEE ALSO
424
425 L<FS::Record>, schema.html from the base documentation.
426
427 =cut
428
429 1;
430
431