1 package FS::cust_main_county;
4 use vars qw( @ISA @EXPORT_OK $conf
5 @cust_main_county %cust_main_county $countyflag );
7 use FS::Record qw( qsearch dbh );
12 use FS::cust_tax_exempt;
13 use FS::cust_tax_exempt_pkg;
15 @ISA = qw( FS::Record );
16 @EXPORT_OK = qw( regionselector );
18 @cust_main_county = ();
21 #ask FS::UID to run this stuff for us later
22 $FS::UID::callback{'FS::cust_main_county'} = sub {
28 FS::cust_main_county - Object methods for cust_main_county objects
32 use FS::cust_main_county;
34 $record = new FS::cust_main_county \%hash;
35 $record = new FS::cust_main_county { 'column' => 'value' };
37 $error = $record->insert;
39 $error = $new_record->replace($old_record);
41 $error = $record->delete;
43 $error = $record->check;
45 ($county_html, $state_html, $country_html) =
46 FS::cust_main_county::regionselector( $county, $state, $country );
50 An FS::cust_main_county object represents a tax rate, defined by locale.
51 FS::cust_main_county inherits from FS::Record. The following fields are
56 =item taxnum - primary key (assigned automatically for new tax rates)
64 =item tax - percentage
70 =item taxname - if defined, printed on invoices instead of "Tax"
72 =item setuptax - if 'Y', this tax does not apply to setup fees
74 =item recurtax - if 'Y', this tax does not apply to recurring fees
84 Creates a new tax rate. To add the tax rate to the database, see L<"insert">.
88 sub table { 'cust_main_county'; }
92 Adds this tax rate to the database. If there is an error, returns the error,
93 otherwise returns false.
97 Deletes this tax rate from the database. If there is an error, returns the
98 error, otherwise returns false.
100 =item replace OLD_RECORD
102 Replaces the OLD_RECORD with this one in the database. If there is an error,
103 returns the error, otherwise returns false.
107 Checks all fields to make sure this is a valid tax rate. If there is an error,
108 returns the error, otherwise returns false. Called by the insert and replace
116 $self->exempt_amount(0) unless $self->exempt_amount;
118 $self->ut_numbern('taxnum')
119 || $self->ut_anything('state')
120 || $self->ut_textn('county')
121 || $self->ut_text('country')
122 || $self->ut_float('tax')
123 || $self->ut_textn('taxclass') # ...
124 || $self->ut_money('exempt_amount')
125 || $self->ut_textn('taxname')
126 || $self->ut_enum('setuptax', [ '', 'Y' ] )
127 || $self->ut_enum('recurtax', [ '', 'Y' ] )
128 || $self->SUPER::check
135 if ( $self->dbdef_table->column('taxname') ) {
136 return $self->setfield('taxname', $_[0]) if @_;
137 return $self->getfield('taxname');
144 if ( $self->dbdef_table->column('setuptax') ) {
145 return $self->setfield('setuptax', $_[0]) if @_;
146 return $self->getfield('setuptax');
153 if ( $self->dbdef_table->column('recurtax') ) {
154 return $self->setfield('recurtax', $_[0]) if @_;
155 return $self->getfield('recurtax');
160 =item sql_taxclass_sameregion
162 Returns an SQL WHERE fragment or the empty string to search for entries
163 with different tax classes.
167 #hmm, description above could be better...
169 sub sql_taxclass_sameregion {
172 my $same_query = 'SELECT taxclass FROM cust_main_county '.
173 ' WHERE taxnum != ? AND country = ?';
174 my @same_param = ( 'taxnum', 'country' );
175 foreach my $opt_field (qw( state county )) {
176 if ( $self->$opt_field() ) {
177 $same_query .= " AND $opt_field = ?";
178 push @same_param, $opt_field;
180 $same_query .= " AND $opt_field IS NULL";
184 my @taxclasses = $self->_list_sql( \@same_param, $same_query );
186 return '' unless scalar(@taxclasses);
188 '( taxclass IS NULL OR ( '. #only if !$self->taxclass ??
189 join(' AND ', map { 'taxclass != '.dbh->quote($_) } @taxclasses ).
194 my( $self, $param, $sql ) = @_;
195 my $sth = dbh->prepare($sql) or die dbh->errstr;
196 $sth->execute( map $self->$_(), @$param )
197 or die "Unexpected error executing statement $sql: ". $sth->errstr;
198 map $_->[0], @{ $sth->fetchall_arrayref };
201 =item taxline CUST_BILL_PKG, ...
203 Returns a listref of a name and an amount of tax calculated for the list of
204 packages. Returns a scalar error message on error.
211 local $SIG{HUP} = 'IGNORE';
212 local $SIG{INT} = 'IGNORE';
213 local $SIG{QUIT} = 'IGNORE';
214 local $SIG{TERM} = 'IGNORE';
215 local $SIG{TSTP} = 'IGNORE';
216 local $SIG{PIPE} = 'IGNORE';
218 my $oldAutoCommit = $FS::UID::AutoCommit;
219 local $FS::UID::AutoCommit = 0;
222 my $name = $self->taxname || 'Tax';
225 foreach my $cust_bill_pkg (@_) {
227 my $cust_bill = $cust_bill_pkg->cust_pkg->cust_bill;
228 my $part_pkg = $cust_bill_pkg->part_pkg;
230 my $taxable_charged = 0;
231 $taxable_charged += $cust_bill_pkg->setup
232 unless $part_pkg->setuptax =~ /^Y$/i
233 || $self->setuptax =~ /^Y$/i;
234 $taxable_charged += $cust_bill_pkg->recur
235 unless $part_pkg->recurtax =~ /^Y$/i
236 || $self->recurtax =~ /^Y$/i;
238 next unless $taxable_charged;
240 if ( $self->exempt_amount && $self->exempt_amount > 0 ) {
241 #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
243 (localtime( $cust_bill_pkg->sdate || $cust_bill->_date ) )[4,5];
245 my $freq = $part_pkg->freq || 1;
246 if ( $freq !~ /(\d+)$/ ) {
247 $dbh->rollback if $oldAutoCommit;
248 return "daily/weekly package definitions not (yet?)".
249 " compatible with monthly tax exemptions";
251 my $taxable_per_month =
252 sprintf("%.2f", $taxable_charged / $freq );
254 #call the whole thing off if this customer has any old
255 #exemption records...
256 my @cust_tax_exempt =
257 qsearch( 'cust_tax_exempt' => { custnum=> $cust_bill->custnum } );
258 if ( @cust_tax_exempt ) {
259 $dbh->rollback if $oldAutoCommit;
261 'this customer still has old-style tax exemption records; '.
262 'run bin/fs-migrate-cust_tax_exempt?';
265 foreach my $which_month ( 1 .. $freq ) {
267 #maintain the new exemption table now
270 FROM cust_tax_exempt_pkg
271 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
272 LEFT JOIN cust_bill USING ( invnum )
278 my $sth = dbh->prepare($sql) or do {
279 $dbh->rollback if $oldAutoCommit;
280 return "fatal: can't lookup exising exemption: ". dbh->errstr;
288 $dbh->rollback if $oldAutoCommit;
289 return "fatal: can't lookup exising exemption: ". dbh->errstr;
291 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
293 my $remaining_exemption =
294 $self->exempt_amount - $existing_exemption;
295 if ( $remaining_exemption > 0 ) {
296 my $addl = $remaining_exemption > $taxable_per_month
298 : $remaining_exemption;
299 $taxable_charged -= $addl;
301 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
302 'billpkgnum' => $cust_bill_pkg->billpkgnum,
303 'taxnum' => $self->taxnum,
304 'year' => 1900+$year,
306 'amount' => sprintf("%.2f", $addl ),
308 my $error = $cust_tax_exempt_pkg->insert;
310 $dbh->rollback if $oldAutoCommit;
311 return "fatal: can't insert cust_tax_exempt_pkg: $error";
313 } # if $remaining_exemption > 0
317 #until ( $mon < 12 ) { $mon -= 12; $year++; }
318 until ( $mon < 13 ) { $mon -= 12; $year++; }
320 } #foreach $which_month
322 } #if $tax->exempt_amount
324 $taxable_charged = sprintf( "%.2f", $taxable_charged);
326 $amount += $taxable_charged * $self->tax / 100
329 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
330 return [ $name, $amount ]
339 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
344 my ( $selected_county, $selected_state, $selected_country,
345 $prefix, $onchange, $disabled ) = @_;
347 $prefix = '' unless defined $prefix;
351 # unless ( @cust_main_county ) { #cache
352 @cust_main_county = qsearch('cust_main_county', {} );
353 foreach my $c ( @cust_main_county ) {
354 $countyflag=1 if $c->county;
355 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
356 $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
359 $countyflag=1 if $selected_county;
361 my $script_html = <<END;
363 function opt(what,value,text) {
364 var optionName = new Option(text, value, false, false);
365 var length = what.length;
366 what.options[length] = optionName;
368 function ${prefix}country_changed(what) {
369 country = what.options[what.selectedIndex].text;
370 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
371 what.form.${prefix}state.options[i] = null;
373 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
375 foreach my $country ( sort keys %cust_main_county ) {
376 $script_html .= "\nif ( country == \"$country\" ) {\n";
377 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
378 ( my $dstate = $state ) =~ s/[\n\r]//g;
379 my $text = $dstate || '(n/a)';
380 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
382 $script_html .= "}\n";
385 $script_html .= <<END;
387 function ${prefix}state_changed(what) {
391 $script_html .= <<END;
392 state = what.options[what.selectedIndex].text;
393 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
394 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
395 what.form.${prefix}county.options[i] = null;
398 foreach my $country ( sort keys %cust_main_county ) {
399 $script_html .= "\nif ( country == \"$country\" ) {\n";
400 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
401 $script_html .= "\nif ( state == \"$state\" ) {\n";
402 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
403 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
404 my $text = $county || '(n/a)';
406 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
408 $script_html .= "}\n";
410 $script_html .= "}\n";
414 $script_html .= <<END;
419 my $county_html = $script_html;
421 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
422 $county_html .= '</SELECT>';
425 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
428 my $state_html = qq!<SELECT NAME="${prefix}state" !.
429 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
430 foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
431 my $text = $state || '(n/a)';
432 my $selected = $state eq $selected_state ? 'SELECTED' : '';
433 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
435 $state_html .= '</SELECT>';
437 $state_html .= '</SELECT>';
439 my $country_html = qq!<SELECT NAME="${prefix}country" !.
440 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
441 my $countrydefault = $conf->config('countrydefault') || 'US';
442 foreach my $country (
443 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
444 keys %cust_main_county
446 my $selected = $country eq $selected_country ? ' SELECTED' : '';
447 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
449 $country_html .= '</SELECT>';
451 ($county_html, $state_html, $country_html);
459 regionselector? putting web ui components in here? they should probably live
464 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base