fix 'Can't call method "setup" on an undefined value' error when using into rates...
[freeside.git] / FS / FS / part_pkg_taxrate.pm
1 package FS::part_pkg_taxrate;
2
3 use strict;
4 use vars qw( @ISA );
5 use Date::Parse;
6 use DateTime;
7 use DateTime::Format::Strptime;
8 use FS::UID qw(dbh);
9 use FS::Record qw( qsearch qsearchs );
10 use FS::part_pkg_taxproduct;
11 use FS::Misc qw(csv_from_fixed);
12
13 @ISA = qw(FS::Record);
14
15 =head1 NAME
16
17 FS::part_pkg_taxrate - Object methods for part_pkg_taxrate records
18
19 =head1 SYNOPSIS
20
21   use FS::part_pkg_taxrate;
22
23   $record = new FS::part_pkg_taxrate \%hash;
24   $record = new FS::part_pkg_taxrate { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::part_pkg_taxrate object maps packages onto tax rates.
37 FS::part_pkg_taxrate inherits from FS::Record.  The following fields are
38 currently supported:
39
40 =over 4
41
42 =item pkgtaxratenum
43
44 Primary key
45
46 =item data_vendor
47
48 Tax data vendor
49
50 =item geocode
51
52 Tax vendor location code
53
54 =item taxproductnum
55
56 Class of package for tax purposes, Index into FS::part_pkg_taxproduct
57
58 =item city
59
60 city
61
62 =item county
63
64 county
65
66 =item state
67
68 state
69
70 =item local
71
72 local
73
74 =item country
75
76 country
77
78 =item taxclassnum
79
80 Class of tax index into FS::tax_taxclass and FS::tax_rate
81
82 =item taxclassnumtaxed
83
84 Class of tax taxed by this entry.
85
86 =item taxable
87
88 taxable
89
90 =item effdate
91
92 effdate
93
94 =back
95
96 =head1 METHODS
97
98 =over 4
99
100 =item new HASHREF
101
102 Creates a new customer (location), package, tax rate mapping.  To add the
103 mapping to the database, see L<"insert">.
104
105 Note that this stores the hash reference, not a distinct copy of the hash it
106 points to.  You can ask the object for a copy with the I<hash> method.
107
108 =cut
109
110 sub table { 'part_pkg_taxrate'; }
111
112 =item insert
113
114 Adds this record to the database.  If there is an error, returns the error,
115 otherwise returns false.
116
117 =cut
118
119 =item delete
120
121 Delete this record from the database.
122
123 =cut
124
125 =item replace OLD_RECORD
126
127 Replaces the OLD_RECORD with this one in the database.  If there is an error,
128 returns the error, otherwise returns false.
129
130 =cut
131
132 =item check
133
134 Checks all fields to make sure this is a valid tax rate mapping.  If there is
135 an error, returns the error, otherwise returns false.  Called by the insert
136 and replace methods.
137
138 =cut
139
140 sub check {
141   my $self = shift;
142
143   my $error = 
144     $self->ut_numbern('pkgtaxratenum')
145     || $self->ut_textn('data_vendor')
146     || $self->ut_textn('geocode')
147     || $self->
148          ut_foreign_key('taxproductnum', 'part_pkg_taxproduct', 'taxproductnum')
149     || $self->ut_textn('city')
150     || $self->ut_textn('county')
151     || $self->ut_textn('state')
152     || $self->ut_textn('local')
153     || $self->ut_text('country')
154     || $self->ut_foreign_keyn('taxclassnumtaxed', 'tax_class', 'taxclassnum')
155     || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
156     || $self->ut_snumbern('effdate')
157     || $self->ut_enum('taxable', [ 'Y', '' ])
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 $imported = 0;
178   my @fields;
179   my $hook;
180
181   my @column_lengths = ();
182   my @column_callbacks = ();
183   if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
184     $format =~ s/-fixed//;
185     my $date_format = sub { my $r='';
186                             /^(\d{4})(\d{2})(\d{2})$/ && ($r="$3/$2/$1");
187                             $r;
188                           };
189     $column_callbacks[16] = $date_format;
190     push @column_lengths, qw( 28 25 2 1 10 4 30 3 100 2 2 2 2 1 2 2 8 1 );
191     push @column_lengths, 1 if $format eq 'cch-update';
192   }
193
194   my $line;
195   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
196   if ( $job || scalar(@column_callbacks) ) {
197     my $error =
198       csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
199     return $error if $error;
200   }
201
202   if ( $format eq 'cch' ||  $format eq 'cch-update' ) {
203     @fields = qw( city county state local geocode group groupdesc item
204                   itemdesc provider customer taxtypetaxed taxcattaxed
205                   taxable taxtype taxcat effdate rectype );
206     push @fields, 'actionflag' if $format eq 'cch-update';
207
208     $imported++ if $format eq 'cch-update';  #empty file ok
209
210     $hook = sub { 
211       my $hash = shift;
212
213       unless ( $hash->{'rectype'} eq 'R' or $hash->{'rectype'} eq 'T' ) {
214         delete($hash->{$_}) for (keys %$hash);
215         return;
216       }
217
218       $hash->{'data_vendor'} = 'cch';
219
220       my %providers = ( '00' => 'Regulated LEC',
221                         '01' => 'Regulated IXC',
222                         '02' => 'Unregulated LEC',
223                         '03' => 'Unregulated IXC',
224                         '04' => 'ISP',
225                         '05' => 'Wireless',
226                       );
227
228       my %customers = ( '00' => 'Residential',
229                         '01' => 'Commercial',
230                         '02' => 'Industrial',
231                         '09' => 'Lifeline',
232                         '10' => 'Senior Citizen',
233                       );
234
235       my $taxproduct =
236         join(':', map{ $hash->{$_} } qw(group item provider customer ) );
237
238       my %part_pkg_taxproduct = ( 'data_vendor' => 'cch', 
239                                   'taxproduct' => $taxproduct,
240                                 );
241
242       my $part_pkg_taxproduct = qsearchs( 'part_pkg_taxproduct', 
243                                           { %part_pkg_taxproduct }
244                                         );
245
246       unless ($part_pkg_taxproduct) {
247         return "Can't find part_pkg_taxproduct for txmatrix deletion: ".
248                join(" ", map { "$_ => ". $hash->{$_} } @fields)
249           if ($hash->{'actionfield'} && $hash->{'actionflag'} eq 'D');
250
251         $part_pkg_taxproduct{'description'} = 
252           join(' : ', (map{ $hash->{$_} } qw(groupdesc itemdesc)),
253                       $providers{$hash->{'provider'}} || '',
254                       $customers{$hash->{'customer'}} || '',
255               );
256         $part_pkg_taxproduct = new FS::part_pkg_taxproduct \%part_pkg_taxproduct;
257         my $error = $part_pkg_taxproduct->insert;
258         return "Error inserting tax product (part_pkg_taxproduct): $error"
259           if $error;
260
261       }
262       $hash->{'taxproductnum'} = $part_pkg_taxproduct->taxproductnum;
263
264       delete($hash->{$_})
265         for qw(group groupdesc item itemdesc provider customer rectype );
266
267       my %map = ( 'taxclassnum'      => [ 'taxtype', 'taxcat' ],
268                   'taxclassnumtaxed' => [ 'taxtypetaxed', 'taxcattaxed' ],
269                 );
270
271       for my $item (keys %map) {
272         my $class = join(':', map($hash->{$_}, @{$map{$item}}));
273         my $tax_class =
274           qsearchs( 'tax_class',
275                     { data_vendor => 'cch',
276                       'taxclass' => $class,
277                     }
278                   );
279         $hash->{$item} = $tax_class->taxclassnum
280           if $tax_class;
281
282         return "Can't find tax class for txmatrix deletion: ".
283                join(" ", map { "$_ => ". $hash->{$_} } @fields)
284           if ( $hash->{'actionflag'} && $hash->{'actionflag'} eq 'D' &&
285                !$tax_class && $class ne ':'
286              );
287
288         delete($hash->{$_}) foreach @{$map{$item}};
289       }
290
291       my $parser = new DateTime::Format::Strptime( pattern => "%m/%d/%Y",
292                                                    time_zone => 'floating',
293                                                  );
294       my $dt = $parser->parse_datetime( $hash->{'effdate'} );
295       return "Can't parse effdate ". $hash->{'effdate'}. ': '. $parser->errstr
296         unless $dt;
297       $hash->{'effdate'} = $dt->epoch;
298  
299       $hash->{'country'} = 'US'; # CA is available
300
301       $hash->{'taxable'} = '' if ($hash->{'taxable'} eq 'N');
302
303       if (exists($hash->{actionflag}) && $hash->{actionflag} eq 'D') {
304         delete($hash->{actionflag});
305
306         foreach my $intfield (qw( taxproductnum taxclassnum effdate )) {
307           if ( $hash->{$intfield} eq '' ) {
308             return "$intfield is empty in search! -- ".
309                    join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) );
310           }
311         }
312
313         my $part_pkg_taxrate = qsearchs('part_pkg_taxrate', $hash);
314         unless ( $part_pkg_taxrate ) {
315           if ( $hash->{taxproductnum} ) {
316             my $taxproduct =
317               qsearchs( 'part_pkg_taxproduct',
318                         { 'taxproductnum' => $hash->{taxproductnum} }
319                       );
320             $hash->{taxproductnum} .= ' ( '. $taxproduct->taxproduct. ' )'
321               if $taxproduct;
322           }
323           return "Can't find part_pkg_taxrate to delete: ".
324                  join(" ", map { "$_ => *". $hash->{$_}. '*' } keys(%$hash) );
325         }
326
327         my $error = $part_pkg_taxrate->delete;
328         return $error if $error;
329
330         delete($hash->{$_}) foreach (keys %$hash);
331       }
332
333       delete($hash->{actionflag});
334
335       '';
336     };
337
338   } elsif ( $format eq 'extended' ) {
339     die "unimplemented\n";
340     @fields = qw( );
341     $hook = sub {};
342   } else {
343     die "unknown format $format";
344   }
345
346   eval "use Text::CSV_XS;";
347   die $@ if $@;
348
349   my $csv = new Text::CSV_XS;
350
351   local $SIG{HUP} = 'IGNORE';
352   local $SIG{INT} = 'IGNORE';
353   local $SIG{QUIT} = 'IGNORE';
354   local $SIG{TERM} = 'IGNORE';
355   local $SIG{TSTP} = 'IGNORE';
356   local $SIG{PIPE} = 'IGNORE';
357
358   my $oldAutoCommit = $FS::UID::AutoCommit;
359   local $FS::UID::AutoCommit = 0;
360   my $dbh = dbh;
361   
362   while ( defined($line=<$fh>) ) {
363     $csv->parse($line) or do {
364       $dbh->rollback if $oldAutoCommit;
365       return "can't parse: ". $csv->error_input();
366     };
367
368
369     if ( $job ) {  # progress bar
370       if ( time - $min_sec > $last ) {
371         my $error = $job->update_statustext(
372           int( 100 * $imported / $count ). ",Importing tax matrix"
373         );
374         die $error if $error;
375         $last = time;
376       }
377     }
378
379     my @columns = $csv->fields();
380
381     my %part_pkg_taxrate = ( 'data_vendor' => $format );
382     foreach my $field ( @fields ) {
383       $part_pkg_taxrate{$field} = shift @columns; 
384     }
385     if ( scalar( @columns ) ) {
386       $dbh->rollback if $oldAutoCommit;
387       return "Unexpected trailing columns in line (wrong format?): $line";
388     }
389
390     my $error = &{$hook}(\%part_pkg_taxrate);
391     if ( $error ) {
392       $dbh->rollback if $oldAutoCommit;
393       return $error;
394     }
395     next unless scalar(keys %part_pkg_taxrate);
396
397
398     my $part_pkg_taxrate = new FS::part_pkg_taxrate( \%part_pkg_taxrate );
399     $error = $part_pkg_taxrate->insert;
400
401     if ( $error ) {
402       $dbh->rollback if $oldAutoCommit;
403       return "can't insert part_pkg_taxrate for $line: $error";
404     }
405
406     $imported++;
407   }
408
409   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
410
411   return "Empty file!" unless ( $imported || $format eq 'cch-update' );
412
413   ''; #no error
414
415 }
416
417 =back
418
419 =head1 BUGS
420
421 =head1 SEE ALSO
422
423 L<FS::Record>, schema.html from the base documentation.
424
425 =cut
426
427 1;
428
429