have \& in invoice_latexreturnaddress translate to & in HTML, RT#4426
[freeside.git] / FS / FS / cust_main_county.pm
1 package FS::cust_main_county;
2
3 use strict;
4 use vars qw( @ISA @EXPORT_OK $conf
5              @cust_main_county %cust_main_county $countyflag );
6 use Exporter;
7 use FS::Record qw( qsearch dbh );
8
9 @ISA = qw( FS::Record );
10 @EXPORT_OK = qw( regionselector );
11
12 @cust_main_county = ();
13 $countyflag = '';
14
15 #ask FS::UID to run this stuff for us later
16 $FS::UID::callback{'FS::cust_main_county'} = sub { 
17   $conf = new FS::Conf;
18 };
19
20 =head1 NAME
21
22 FS::cust_main_county - Object methods for cust_main_county objects
23
24 =head1 SYNOPSIS
25
26   use FS::cust_main_county;
27
28   $record = new FS::cust_main_county \%hash;
29   $record = new FS::cust_main_county { 'column' => 'value' };
30
31   $error = $record->insert;
32
33   $error = $new_record->replace($old_record);
34
35   $error = $record->delete;
36
37   $error = $record->check;
38
39   ($county_html, $state_html, $country_html) =
40     FS::cust_main_county::regionselector( $county, $state, $country );
41
42 =head1 DESCRIPTION
43
44 An FS::cust_main_county object represents a tax rate, defined by locale.
45 FS::cust_main_county inherits from FS::Record.  The following fields are
46 currently supported:
47
48 =over 4
49
50 =item taxnum - primary key (assigned automatically for new tax rates)
51
52 =item state
53
54 =item county
55
56 =item country
57
58 =item tax - percentage
59
60 =item taxclass
61
62 =item exempt_amount
63
64 =item taxname - if defined, printed on invoices instead of "Tax"
65
66 =item setuptax - if 'Y', this tax does not apply to setup fees
67
68 =item recurtax - if 'Y', this tax does not apply to recurring fees
69
70 =back
71
72 =head1 METHODS
73
74 =over 4
75
76 =item new HASHREF
77
78 Creates a new tax rate.  To add the tax rate to the database, see L<"insert">.
79
80 =cut
81
82 sub table { 'cust_main_county'; }
83
84 =item insert
85
86 Adds this tax rate to the database.  If there is an error, returns the error,
87 otherwise returns false.
88
89 =item delete
90
91 Deletes this tax rate from the database.  If there is an error, returns the
92 error, otherwise returns false.
93
94 =item replace OLD_RECORD
95
96 Replaces the OLD_RECORD with this one in the database.  If there is an error,
97 returns the error, otherwise returns false.
98
99 =item check
100
101 Checks all fields to make sure this is a valid tax rate.  If there is an error,
102 returns the error, otherwise returns false.  Called by the insert and replace
103 methods.
104
105 =cut
106
107 sub check {
108   my $self = shift;
109
110   $self->exempt_amount(0) unless $self->exempt_amount;
111
112   $self->ut_numbern('taxnum')
113     || $self->ut_anything('state')
114     || $self->ut_textn('county')
115     || $self->ut_text('country')
116     || $self->ut_float('tax')
117     || $self->ut_textn('taxclass') # ...
118     || $self->ut_money('exempt_amount')
119     || $self->ut_textn('taxname')
120     || $self->ut_enum('setuptax', [ '', 'Y' ] )
121     || $self->ut_enum('recurtax', [ '', 'Y' ] )
122     || $self->SUPER::check
123     ;
124
125 }
126
127 sub taxname {
128   my $self = shift;
129   if ( $self->dbdef_table->column('taxname') ) {
130     return $self->setfield('taxname', $_[0]) if @_;
131     return $self->getfield('taxname');
132   }  
133   return '';
134 }
135
136 sub setuptax {
137   my $self = shift;
138   if ( $self->dbdef_table->column('setuptax') ) {
139     return $self->setfield('setuptax', $_[0]) if @_;
140     return $self->getfield('setuptax');
141   }  
142   return '';
143 }
144
145 sub recurtax {
146   my $self = shift;
147   if ( $self->dbdef_table->column('recurtax') ) {
148     return $self->setfield('recurtax', $_[0]) if @_;
149     return $self->getfield('recurtax');
150   }  
151   return '';
152 }
153
154 =item sql_taxclass_sameregion
155
156 Returns an SQL WHERE fragment or the empty string to search for entries
157 with different tax classes.
158
159 =cut
160
161 #hmm, description above could be better...
162
163 sub sql_taxclass_sameregion {
164   my $self = shift;
165
166   my $same_query = 'SELECT taxclass FROM cust_main_county '.
167                    ' WHERE taxnum != ? AND country = ?';
168   my @same_param = ( 'taxnum', 'country' );
169   foreach my $opt_field (qw( state county )) {
170     if ( $self->$opt_field() ) {
171       $same_query .= " AND $opt_field = ?";
172       push @same_param, $opt_field;
173     } else {
174       $same_query .= " AND $opt_field IS NULL";
175     }
176   }
177
178   my @taxclasses = $self->_list_sql( \@same_param, $same_query );
179
180   return '' unless scalar(@taxclasses);
181
182   '( taxclass IS NULL OR ( '.  #only if !$self->taxclass ??
183      join(' AND ', map { 'taxclass != '.dbh->quote($_) } @taxclasses ). 
184   ' ) ) ';
185 }
186
187 sub _list_sql {
188   my( $self, $param, $sql ) = @_;
189   my $sth = dbh->prepare($sql) or die dbh->errstr;
190   $sth->execute( map $self->$_(), @$param )
191     or die "Unexpected error executing statement $sql: ". $sth->errstr;
192   map $_->[0], @{ $sth->fetchall_arrayref };
193 }
194
195 =back
196
197 =head1 SUBROUTINES
198
199 =over 4
200
201 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
202
203 =cut
204
205 sub regionselector {
206   my ( $selected_county, $selected_state, $selected_country,
207        $prefix, $onchange, $disabled ) = @_;
208
209   $prefix = '' unless defined $prefix;
210
211   $countyflag = 0;
212
213 #  unless ( @cust_main_county ) { #cache 
214     @cust_main_county = qsearch('cust_main_county', {} );
215     foreach my $c ( @cust_main_county ) {
216       $countyflag=1 if $c->county;
217       #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
218       $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
219     }
220 #  }
221   $countyflag=1 if $selected_county;
222
223   my $script_html = <<END;
224     <SCRIPT>
225     function opt(what,value,text) {
226       var optionName = new Option(text, value, false, false);
227       var length = what.length;
228       what.options[length] = optionName;
229     }
230     function ${prefix}country_changed(what) {
231       country = what.options[what.selectedIndex].text;
232       for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
233           what.form.${prefix}state.options[i] = null;
234 END
235       #what.form.${prefix}state.options[0] = new Option('', '', false, true);
236
237   foreach my $country ( sort keys %cust_main_county ) {
238     $script_html .= "\nif ( country == \"$country\" ) {\n";
239     foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
240       ( my $dstate = $state ) =~ s/[\n\r]//g;
241       my $text = $dstate || '(n/a)';
242       $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
243     }
244     $script_html .= "}\n";
245   }
246
247   $script_html .= <<END;
248     }
249     function ${prefix}state_changed(what) {
250 END
251
252   if ( $countyflag ) {
253     $script_html .= <<END;
254       state = what.options[what.selectedIndex].text;
255       country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
256       for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
257           what.form.${prefix}county.options[i] = null;
258 END
259
260     foreach my $country ( sort keys %cust_main_county ) {
261       $script_html .= "\nif ( country == \"$country\" ) {\n";
262       foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
263         $script_html .= "\nif ( state == \"$state\" ) {\n";
264           #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
265           foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
266             my $text = $county || '(n/a)';
267             $script_html .=
268               qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
269           }
270         $script_html .= "}\n";
271       }
272       $script_html .= "}\n";
273     }
274   }
275
276   $script_html .= <<END;
277     }
278     </SCRIPT>
279 END
280
281   my $county_html = $script_html;
282   if ( $countyflag ) {
283     $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
284     $county_html .= '</SELECT>';
285   } else {
286     $county_html .=
287       qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
288   }
289
290   my $state_html = qq!<SELECT NAME="${prefix}state" !.
291                    qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
292   foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
293     my $text = $state || '(n/a)';
294     my $selected = $state eq $selected_state ? 'SELECTED' : '';
295     $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
296   }
297   $state_html .= '</SELECT>';
298
299   $state_html .= '</SELECT>';
300
301   my $country_html = qq!<SELECT NAME="${prefix}country" !.
302                      qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
303   my $countrydefault = $conf->config('countrydefault') || 'US';
304   foreach my $country (
305     sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
306       keys %cust_main_county
307   ) {
308     my $selected = $country eq $selected_country ? ' SELECTED' : '';
309     $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
310   }
311   $country_html .= '</SELECT>';
312
313   ($county_html, $state_html, $country_html);
314
315 }
316
317 =back
318
319 =head1 BUGS
320
321 regionselector?  putting web ui components in here?  they should probably live
322 somewhere else...
323
324 =head1 SEE ALSO
325
326 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
327 documentation.
328
329 =cut
330
331 1;
332