eliminate some false laziness in FS::Misc::send_email vs. msg_template/email.pm send_...
[freeside.git] / FS / FS / part_pkg_taxproduct.pm
1 package FS::part_pkg_taxproduct;
2
3 use strict;
4 use vars qw( @ISA $delete_kludge );
5 use FS::Record qw( qsearch dbh );
6 use Text::CSV_XS;
7
8 @ISA = qw(FS::Record);
9 $delete_kludge = 0;
10
11 =head1 NAME
12
13 FS::part_pkg_taxproduct - Object methods for part_pkg_taxproduct records
14
15 =head1 SYNOPSIS
16
17   use FS::part_pkg_taxproduct;
18
19   $record = new FS::part_pkg_taxproduct \%hash;
20   $record = new FS::part_pkg_taxproduct { '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::part_pkg_taxproduct object represents a tax product.
33 FS::part_pkg_taxproduct inherits from FS::Record.  The following fields are
34 currently supported:
35
36 =over 4
37
38 =item taxproductnum
39
40 Primary key
41
42 =item data_vendor
43
44 Tax data vendor
45
46 =item taxproduct
47
48 Tax product id from the vendor
49
50 =item description
51
52 A human readable description of the id in taxproduct
53
54 =back
55
56 =head1 METHODS
57
58 =over 4
59
60 =item new HASHREF
61
62 Creates a new tax product.  To add the tax product to the database, see L<"insert">.
63
64 Note that this stores the hash reference, not a distinct copy of the hash it
65 points to.  You can ask the object for a copy with the I<hash> method.
66
67 =cut
68
69 sub table { 'part_pkg_taxproduct'; }
70
71 =item insert
72
73 Adds this record to the database.  If there is an error, returns the error,
74 otherwise returns false.
75
76 =cut
77
78 =item delete
79
80 Delete this record from the database.
81
82 =cut
83
84 sub delete {
85   my $self = shift;
86
87   return "Can't delete a tax product which has attached package tax rates!"
88     if qsearch( 'part_pkg_taxrate', { 'taxproductnum' => $self->taxproductnum } );
89
90   unless ( $delete_kludge ) {
91     return "Can't delete a tax product which has attached packages!"
92       if qsearch( 'part_pkg', { 'taxproductnum' => $self->taxproductnum } );
93   }
94
95   $self->SUPER::delete(@_);
96 }
97
98 =item replace OLD_RECORD
99
100 Replaces the OLD_RECORD with this one in the database.  If there is an error,
101 returns the error, otherwise returns false.
102
103 =cut
104
105 =item check
106
107 Checks all fields to make sure this is a valid tax product.  If there is
108 an error, returns the error, otherwise returns false.  Called by the insert
109 and replace methods.
110
111 =cut
112
113 sub check {
114   my $self = shift;
115
116   my $error = 
117     $self->ut_numbern('taxproductnum')
118     || $self->ut_textn('data_vendor')
119     || $self->ut_text('taxproduct')
120     || $self->ut_textn('description')
121   ;
122   return $error if $error;
123
124   $self->SUPER::check;
125 }
126
127 =item part_pkg_taxrate GEOCODE
128
129 Returns the L<FS::part_pkg_taxrate> records (tax definitions) that can apply 
130 to this tax product category in the location identified by GEOCODE.
131
132 =cut
133
134 # actually only returns one arbitrary record for each taxclassnum, making 
135 # it useful only for retrieving the taxclassnums
136
137 sub part_pkg_taxrate {
138   my $self = shift;
139   my $data_vendor = $self->data_vendor; # because duh
140   my $geocode = shift;
141
142   my $dbh = dbh;
143
144   # CCH oddness in m2m
145   my $extra_sql .= "AND part_pkg_taxrate.data_vendor = '$data_vendor' ".
146                    "AND (".
147     join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
148                  qw(10 5 2)
149         ).
150     ')';
151   # much more CCH oddness in m2m -- this is kludgy
152   my $tpnums = join(',',
153     map { $_->taxproductnum }
154     $self->expand_cch_taxproduct
155   );
156
157   # if there are no taxproductnums, there are no matching tax classes
158   return if length($tpnums) == 0;
159
160   $extra_sql .= " AND taxproductnum IN($tpnums)";
161
162   my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )';
163   my $order_by = 'ORDER BY taxclassnum, length(geocode) desc, length(taxproduct) desc';
164   my $select   = 'DISTINCT ON(taxclassnum) *, taxproduct';
165
166   # should qsearch preface columns with the table to facilitate joins?
167   qsearch( { 'table'     => 'part_pkg_taxrate',
168              'select'    => $select,
169              'hashref'   => { 'taxable' => 'Y' },
170              'addl_from' => $addl_from,
171              'extra_sql' => $extra_sql,
172              'order_by'  => $order_by,
173          } );
174 }
175
176 =item expand_cch_taxproduct
177
178 Returns the full set of part_pkg_taxproduct records that are "implied" by 
179 this one.
180
181 =cut
182
183 sub expand_cch_taxproduct {
184   my $self = shift;
185   my $class = shift;
186
187   my ($a,$b,$c,$d) = split ':', $self->taxproduct;
188   $a = '' unless $a; $b = '' unless $b; $c = '' unless $c; $d = '' unless $d;
189   my $taxproducts = join(',',
190     "'${a}:${b}:${c}:${d}'",
191     "'${a}:${b}:${c}:'",
192     "'${a}:${b}::${d}'",
193     "'${a}:${b}::'"
194   );
195   qsearch( {
196       'table'     => 'part_pkg_taxproduct',
197       'hashref'   => { 'data_vendor'=>'cch' },
198       'extra_sql' => "AND taxproduct IN($taxproducts)",
199   } );
200 }
201
202
203 =back
204
205 =cut
206
207 sub batch_import {
208   my ($param, $job) = @_;
209
210   my $oldAutoCommit = $FS::UID::AutoCommit;
211   local $FS::UID::AutoCommit = 0;
212   my $dbh = dbh;
213
214   my $fh = $param->{filehandle};
215   my $format = $param->{format};
216   die "unsupported part_pkg_taxproduct format '$format'"
217     unless $format eq 'billsoft';
218
219   # this is slightly silly
220   my @lines = <$fh>;
221   my $lines = scalar @lines;
222   seek($fh, 0, 0);
223   
224   my $imported = 0;
225   my $csv = Text::CSV_XS->new;
226   my $error;
227   # for importing the "transervdesc.txt" file
228   while ( my $row = $csv->getline($fh) ) {
229     if (!defined $row) {
230       $dbh->rollback if $oldAutoCommit;
231       return "can't parse: ". $csv->error_input();
232     }
233
234     if ( $job ) {
235       $job->update_statustext(
236         int( 100 * $imported / $lines ) . ',Inserting tax product records'
237       );
238     }
239
240     # columns 0-2: irrelevant here
241     my $taxproduct = $row->[3] . ':' . $row->[5];
242     my $description = $row->[4];
243     $description =~ s/\s+$//;
244     $description .= ':' . $row->[6];
245     $description =~ s/\s+$//;
246     my $ppt = qsearchs('part_pkg_taxproduct', {
247       'data_vendor' => 'billsoft',
248       'taxproduct'  => $taxproduct
249     });
250     if ( $ppt ) {
251       $ppt->set('description', $description);
252       $ppt->set('note', $row->[7]);
253       $error = $ppt->replace;
254     } else {
255       $ppt = FS::part_pkg_taxproduct->new({
256           'data_vendor' => 'billsoft',
257           'taxproduct'  => $taxproduct,
258           'description' => $description,
259           'note'        => $row->[7],
260       });
261       $error = $ppt->insert;
262     }
263     if ( $error ) {
264       $dbh->rollback if $oldAutoCommit;
265       return "error inserting part_pkg_taxproduct $taxproduct: $error\n";
266     }
267     $imported++;
268   }
269
270   $dbh->commit if $oldAutoCommit;
271   return '';
272 }
273
274 =head1 BUGS
275
276 Confusingly named.  It has nothing to do with part_pkg.
277
278 =head1 SEE ALSO
279
280 L<FS::Record>, schema.html from the base documentation.
281
282 =cut
283
284 1;
285