on upgrade, remove all leading/trailing whitespace from address fields, #72194
[freeside.git] / FS / FS / tax_rate_location.pm
1 package FS::tax_rate_location;
2
3 use strict;
4 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::Misc qw( csv_from_fixed );
7
8 =head1 NAME
9
10 FS::tax_rate_location - Object methods for tax_rate_location records
11
12 =head1 SYNOPSIS
13
14   use FS::tax_rate_location;
15
16   $record = new FS::tax_rate_location \%hash;
17   $record = new FS::tax_rate_location { 'column' => 'value' };
18
19   $error = $record->insert;
20
21   $error = $new_record->replace($old_record);
22
23   $error = $record->delete;
24
25   $error = $record->check;
26
27 =head1 DESCRIPTION
28
29 An FS::tax_rate_location object represents an example.  FS::tax_rate_location inherits from
30 FS::Record.  The following fields are currently supported:
31
32 =over 4
33
34 =item taxratelocationnum
35
36 Primary key (assigned automatically for new tax_rate_locations)
37
38 =item data_vendor
39
40 The tax data vendor
41
42 =item geocode
43
44 A unique geographic location code provided by the data vendor
45
46 =item city
47
48 City
49
50 =item county
51
52 County
53
54 =item state
55
56 State
57
58 =item disabled
59
60 If 'Y' this record is no longer active.
61
62
63 =back
64
65 =head1 METHODS
66
67 =over 4
68
69 =item new HASHREF
70
71 Creates a new tax rate location.  To add the record to the database, see
72  L<"insert">.
73
74 Note that this stores the hash reference, not a distinct copy of the hash it
75 points to.  You can ask the object for a copy with the I<hash> method.
76
77 =cut
78
79 sub table { 'tax_rate_location'; }
80
81 =item insert
82
83 Adds this record to the database.  If there is an error, returns the error,
84 otherwise returns false.
85
86 =cut
87
88 =item delete
89
90 Delete this record from the database.
91
92 =cut
93
94 sub delete {
95   return "Can't delete tax rate locations.  Set disable to 'Y' instead.";
96   # check that it is unused in any cust_bill_pkg_tax_location records instead?
97 }
98
99 =item replace OLD_RECORD
100
101 Replaces the OLD_RECORD with this one in the database.  If there is an error,
102 returns the error, otherwise returns false.
103
104 =cut
105
106 =item check
107
108 Checks all fields to make sure this is a valid tax rate location.  If there is
109 an error, returns the error, otherwise returns false.  Called by the insert
110 and replace methods.
111
112 =cut
113
114 sub check {
115   my $self = shift;
116
117   my $error = 
118     $self->ut_numbern('taxratelocationnum')
119     || $self->ut_textn('data_vendor')
120     || $self->ut_alpha('geocode')
121     || $self->ut_textn('city')
122     || $self->ut_textn('county')
123     || $self->ut_textn('state')
124     || $self->ut_enum('disabled', [ '', 'Y' ])
125   ;
126   return $error if $error;
127
128   my $t;
129   $t = qsearchs( 'tax_rate_location',
130                  { disabled => '',
131                    ( map { $_ => $self->$_ } qw( data_vendor geocode ) ),
132                  },
133                )
134     unless $self->disabled;
135
136   $t = $self->by_key( $self->taxratelocationnum )
137     if ( !$t && $self->taxratelocationnum );
138
139   return "geocode ". $self->geocode. " already in use for this vendor"
140     if ( $t && $t->taxratelocationnum != $self->taxratelocationnum );
141
142   return "may only be disabled"
143     if ( $t && scalar( grep { $t->$_ ne $self->$_ } 
144                        grep { $_ ne 'disabled' }
145                        $self->fields
146                      )
147        );
148
149   $self->SUPER::check;
150 }
151
152 =back
153
154 =head1 CLASS METHODS
155
156 =item location_sql KEY => VALUE, ...
157
158 Returns an SQL fragment identifying matching tax_rate_location /
159 cust_bill_pkg_tax_rate_location records.
160
161 Parameters are county, state, city and locationtaxid
162
163 =cut
164
165 sub location_sql {
166   my($class, %param) = @_;
167
168   my %pn = (
169    'city'          => 'tax_rate_location.city',
170    'county'        => 'tax_rate_location.county',
171    'state'         => 'tax_rate_location.state',
172    'locationtaxid' => 'cust_bill_pkg_tax_rate_location.locationtaxid',
173   );
174
175   my %ph = map { $pn{$_} => dbh->quote($param{$_}) } keys %pn;
176
177   join( ' AND ',
178     map { "( $_ = $ph{$_} OR $ph{$_} = '' AND $_ IS NULL)" } keys %ph
179   );
180
181 }
182
183 =back
184
185 =head1 SUBROUTINES
186
187 =over 4
188
189 =item batch_import
190
191 =cut
192
193 sub batch_import {
194   my ($param, $job) = @_;
195
196   my $fh = $param->{filehandle};
197   my $format = $param->{'format'};
198
199   my %insert = ();
200   my %delete = ();
201
202   my @fields;
203   my $hook;
204
205   my @column_lengths = ();
206   my @column_callbacks = ();
207   if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
208     $format =~ s/-fixed//;
209     my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
210     push @column_lengths, qw( 28 25 2 10 );
211     push @column_lengths, 1 if $format eq 'cch-update';
212     push @column_callbacks, $trim foreach (@column_lengths);
213   }
214
215   my $line;
216   my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
217   if ( $job || scalar(@column_callbacks) ) {
218     my $error =
219       csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
220     return $error if $error;
221   }
222
223   if ( $format eq 'cch' || $format eq 'cch-update' ) {
224     @fields = qw( city county state geocode );
225     push @fields, 'actionflag' if $format eq 'cch-update';
226
227     $hook = sub {
228       my $hash = shift;
229
230       $hash->{'data_vendor'} ='cch';
231
232       if (exists($hash->{'actionflag'}) && $hash->{'actionflag'} eq 'D') {
233         delete($hash->{actionflag});
234
235         $hash->{disabled} = '';
236         my $tax_rate_location = qsearchs('tax_rate_location', $hash);
237         return "Can't find tax_rate_location to delete: ".
238                join(" ", map { "$_ => ". $hash->{$_} } @fields)
239           unless $tax_rate_location;
240
241         $tax_rate_location->disabled('Y');
242         my $error = $tax_rate_location->replace;
243         return $error if $error;
244
245         delete($hash->{$_}) foreach (keys %$hash);
246       }
247
248       delete($hash->{'actionflag'});
249
250       '';
251
252     };
253
254   } elsif ( $format eq 'extended' ) {
255     die "unimplemented\n";
256     @fields = qw( );
257     $hook = sub {};
258   } else {
259     die "unknown format $format";
260   }
261
262   eval "use Text::CSV_XS;";
263   die $@ if $@;
264
265   my $csv = new Text::CSV_XS;
266
267   my $imported = 0;
268
269   local $SIG{HUP} = 'IGNORE';
270   local $SIG{INT} = 'IGNORE';
271   local $SIG{QUIT} = 'IGNORE';
272   local $SIG{TERM} = 'IGNORE';
273   local $SIG{TSTP} = 'IGNORE';
274   local $SIG{PIPE} = 'IGNORE';
275
276   my $oldAutoCommit = $FS::UID::AutoCommit;
277   local $FS::UID::AutoCommit = 0;
278   my $dbh = dbh;
279
280   while ( defined($line=<$fh>) ) {
281     $csv->parse($line) or do {
282       $dbh->rollback if $oldAutoCommit;
283       return "can't parse: ". $csv->error_input();
284     };
285
286     if ( $job ) {  # progress bar
287       if ( time - $min_sec > $last ) {
288         my $error = $job->update_statustext(
289           int( 100 * $imported / $count )
290         );
291         die $error if $error;
292         $last = time;
293       }
294     }
295
296     my @columns = $csv->fields();
297
298     my %tax_rate_location = ();
299     foreach my $field ( @fields ) {
300       $tax_rate_location{$field} = shift @columns;
301     }
302     if ( scalar( @columns ) ) {
303       $dbh->rollback if $oldAutoCommit;
304       return "Unexpected trailing columns in line (wrong format?) importing tax-rate_location: $line";
305     }
306
307     my $error = &{$hook}(\%tax_rate_location);
308     if ( $error ) {
309       $dbh->rollback if $oldAutoCommit;
310       return $error;
311     }
312
313     if (scalar(keys %tax_rate_location)) { #inserts only
314
315       my $tax_rate_location = new FS::tax_rate_location( \%tax_rate_location );
316       $error = $tax_rate_location->insert;
317
318       if ( $error ) {
319         $dbh->rollback if $oldAutoCommit;
320         return "can't insert tax_rate_location for $line: $error";
321       }
322
323     }
324
325     $imported++;
326
327   }
328
329   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
330
331   return "Empty file!" unless ($imported || $format eq 'cch-update');
332
333   ''; #no error
334
335 }
336
337 =head1 BUGS
338
339 Currently somewhat specific to CCH supplied data.
340
341 =head1 SEE ALSO
342
343 L<FS::Record>, schema.html from the base documentation.
344
345 =cut
346
347 1;
348