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