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;
239 unless $taxable_charged;
241 if ( $self->exempt_amount && $self->exempt_amount > 0 ) {
242 #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
244 (localtime( $cust_bill_pkg->sdate || $cust_bill->_date ) )[4,5];
246 my $freq = $part_pkg->freq || 1;
247 if ( $freq !~ /(\d+)$/ ) {
248 $dbh->rollback if $oldAutoCommit;
249 return "daily/weekly package definitions not (yet?)".
250 " compatible with monthly tax exemptions";
252 my $taxable_per_month =
253 sprintf("%.2f", $taxable_charged / $freq );
255 #call the whole thing off if this customer has any old
256 #exemption records...
257 my @cust_tax_exempt =
258 qsearch( 'cust_tax_exempt' => { custnum=> $cust_bill->custnum } );
259 if ( @cust_tax_exempt ) {
260 $dbh->rollback if $oldAutoCommit;
262 'this customer still has old-style tax exemption records; '.
263 'run bin/fs-migrate-cust_tax_exempt?';
266 foreach my $which_month ( 1 .. $freq ) {
268 #maintain the new exemption table now
271 FROM cust_tax_exempt_pkg
272 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
273 LEFT JOIN cust_bill USING ( invnum )
279 my $sth = dbh->prepare($sql) or do {
280 $dbh->rollback if $oldAutoCommit;
281 return "fatal: can't lookup exising exemption: ". dbh->errstr;
289 $dbh->rollback if $oldAutoCommit;
290 return "fatal: can't lookup exising exemption: ". dbh->errstr;
292 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
294 my $remaining_exemption =
295 $self->exempt_amount - $existing_exemption;
296 if ( $remaining_exemption > 0 ) {
297 my $addl = $remaining_exemption > $taxable_per_month
299 : $remaining_exemption;
300 $taxable_charged -= $addl;
302 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
303 'billpkgnum' => $cust_bill_pkg->billpkgnum,
304 'taxnum' => $self->taxnum,
305 'year' => 1900+$year,
307 'amount' => sprintf("%.2f", $addl ),
309 my $error = $cust_tax_exempt_pkg->insert;
311 $dbh->rollback if $oldAutoCommit;
312 return "fatal: can't insert cust_tax_exempt_pkg: $error";
314 } # if $remaining_exemption > 0
318 #until ( $mon < 12 ) { $mon -= 12; $year++; }
319 until ( $mon < 13 ) { $mon -= 12; $year++; }
321 } #foreach $which_month
323 } #if $tax->exempt_amount
325 $taxable_charged = sprintf( "%.2f", $taxable_charged);
327 $amount += $taxable_charged * $self->tax / 100
330 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
331 return [ $name, $amount ]
340 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
345 my ( $selected_county, $selected_state, $selected_country,
346 $prefix, $onchange, $disabled ) = @_;
348 $prefix = '' unless defined $prefix;
352 # unless ( @cust_main_county ) { #cache
353 @cust_main_county = qsearch('cust_main_county', {} );
354 foreach my $c ( @cust_main_county ) {
355 $countyflag=1 if $c->county;
356 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
357 $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
360 $countyflag=1 if $selected_county;
362 my $script_html = <<END;
364 function opt(what,value,text) {
365 var optionName = new Option(text, value, false, false);
366 var length = what.length;
367 what.options[length] = optionName;
369 function ${prefix}country_changed(what) {
370 country = what.options[what.selectedIndex].text;
371 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
372 what.form.${prefix}state.options[i] = null;
374 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
376 foreach my $country ( sort keys %cust_main_county ) {
377 $script_html .= "\nif ( country == \"$country\" ) {\n";
378 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
379 ( my $dstate = $state ) =~ s/[\n\r]//g;
380 my $text = $dstate || '(n/a)';
381 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
383 $script_html .= "}\n";
386 $script_html .= <<END;
388 function ${prefix}state_changed(what) {
392 $script_html .= <<END;
393 state = what.options[what.selectedIndex].text;
394 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
395 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
396 what.form.${prefix}county.options[i] = null;
399 foreach my $country ( sort keys %cust_main_county ) {
400 $script_html .= "\nif ( country == \"$country\" ) {\n";
401 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
402 $script_html .= "\nif ( state == \"$state\" ) {\n";
403 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
404 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
405 my $text = $county || '(n/a)';
407 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
409 $script_html .= "}\n";
411 $script_html .= "}\n";
415 $script_html .= <<END;
420 my $county_html = $script_html;
422 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
423 $county_html .= '</SELECT>';
426 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
429 my $state_html = qq!<SELECT NAME="${prefix}state" !.
430 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
431 foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
432 my $text = $state || '(n/a)';
433 my $selected = $state eq $selected_state ? 'SELECTED' : '';
434 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
436 $state_html .= '</SELECT>';
438 $state_html .= '</SELECT>';
440 my $country_html = qq!<SELECT NAME="${prefix}country" !.
441 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
442 my $countrydefault = $conf->config('countrydefault') || 'US';
443 foreach my $country (
444 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
445 keys %cust_main_county
447 my $selected = $country eq $selected_country ? ' SELECTED' : '';
448 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
450 $country_html .= '</SELECT>';
452 ($county_html, $state_html, $country_html);
460 regionselector? putting web ui components in here? they should probably live
465 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base