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 TAXABLES_ARRAYREF, [ OPTION => VALUE ... ]
203 Returns a listref of a name and an amount of tax calculated for the list of
204 packages or amounts referenced by TAXABLES_ARRAYREF. Returns a scalar error
207 Options include custnum and invoice_date and are hints to this method
212 my( $self, $taxables, %opt ) = @_;
215 push @exemptions, @{ $_->_cust_tax_exempt_pkg }
216 for grep { ref($_) } @$taxables;
218 local $SIG{HUP} = 'IGNORE';
219 local $SIG{INT} = 'IGNORE';
220 local $SIG{QUIT} = 'IGNORE';
221 local $SIG{TERM} = 'IGNORE';
222 local $SIG{TSTP} = 'IGNORE';
223 local $SIG{PIPE} = 'IGNORE';
225 my $oldAutoCommit = $FS::UID::AutoCommit;
226 local $FS::UID::AutoCommit = 0;
229 my $name = $self->taxname || 'Tax';
232 foreach my $cust_bill_pkg (@$taxables) {
234 my $cust_pkg = $cust_bill_pkg->cust_pkg;
235 my $cust_bill = $cust_pkg->cust_bill if $cust_pkg;
236 my $custnum = $cust_pkg ? $cust_pkg->custnum : $opt{custnum};
237 my $part_pkg = $cust_bill_pkg->part_pkg;
238 my $invoice_date = $cust_bill ? $cust_bill->_date : $opt{invoice_date};
240 my $taxable_charged = 0;
241 $taxable_charged += $cust_bill_pkg->setup
242 unless $part_pkg->setuptax =~ /^Y$/i
243 || $self->setuptax =~ /^Y$/i;
244 $taxable_charged += $cust_bill_pkg->recur
245 unless $part_pkg->recurtax =~ /^Y$/i
246 || $self->recurtax =~ /^Y$/i;
248 next unless $taxable_charged;
250 if ( $self->exempt_amount && $self->exempt_amount > 0 ) {
251 #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
253 (localtime( $cust_bill_pkg->sdate || $invoice_date ) )[4,5];
255 my $freq = $part_pkg->freq || 1;
256 if ( $freq !~ /(\d+)$/ ) {
257 $dbh->rollback if $oldAutoCommit;
258 return "daily/weekly package definitions not (yet?)".
259 " compatible with monthly tax exemptions";
261 my $taxable_per_month =
262 sprintf("%.2f", $taxable_charged / $freq );
264 #call the whole thing off if this customer has any old
265 #exemption records...
266 my @cust_tax_exempt =
267 qsearch( 'cust_tax_exempt' => { custnum=> $custnum } );
268 if ( @cust_tax_exempt ) {
269 $dbh->rollback if $oldAutoCommit;
271 'this customer still has old-style tax exemption records; '.
272 'run bin/fs-migrate-cust_tax_exempt?';
275 foreach my $which_month ( 1 .. $freq ) {
277 #maintain the new exemption table now
280 FROM cust_tax_exempt_pkg
281 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
282 LEFT JOIN cust_bill USING ( invnum )
288 my $sth = dbh->prepare($sql) or do {
289 $dbh->rollback if $oldAutoCommit;
290 return "fatal: can't lookup exising exemption: ". dbh->errstr;
298 $dbh->rollback if $oldAutoCommit;
299 return "fatal: can't lookup exising exemption: ". dbh->errstr;
301 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
303 foreach ( grep { $_->taxnum == $self->taxnum &&
305 $_->year == 1900+$year
309 $existing_exemption += $_->amount;
312 my $remaining_exemption =
313 $self->exempt_amount - $existing_exemption;
314 if ( $remaining_exemption > 0 ) {
315 my $addl = $remaining_exemption > $taxable_per_month
317 : $remaining_exemption;
318 $taxable_charged -= $addl;
320 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
321 'taxnum' => $self->taxnum,
322 'year' => 1900+$year,
324 'amount' => sprintf("%.2f", $addl ),
326 if ($cust_bill_pkg->billpkgnum) {
327 $cust_tax_exempt_pkg->billpkgnum($cust_bill_pkg->billpkgnum);
328 my $error = $cust_tax_exempt_pkg->insert;
330 $dbh->rollback if $oldAutoCommit;
331 return "fatal: can't insert cust_tax_exempt_pkg: $error";
334 push @exemptions, $cust_tax_exempt_pkg;
335 push @{ $cust_bill_pkg->_cust_tax_exempt_pkg }, $cust_tax_exempt_pkg;
336 } # if $cust_bill_pkg->billpkgnum
337 } # if $remaining_exemption > 0
341 #until ( $mon < 12 ) { $mon -= 12; $year++; }
342 until ( $mon < 13 ) { $mon -= 12; $year++; }
344 } #foreach $which_month
346 } #if $tax->exempt_amount
348 $taxable_charged = sprintf( "%.2f", $taxable_charged);
350 $amount += $taxable_charged * $self->tax / 100
353 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
368 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
373 my ( $selected_county, $selected_state, $selected_country,
374 $prefix, $onchange, $disabled ) = @_;
376 $prefix = '' unless defined $prefix;
380 # unless ( @cust_main_county ) { #cache
381 @cust_main_county = qsearch('cust_main_county', {} );
382 foreach my $c ( @cust_main_county ) {
383 $countyflag=1 if $c->county;
384 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
385 $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
388 $countyflag=1 if $selected_county;
390 my $script_html = <<END;
392 function opt(what,value,text) {
393 var optionName = new Option(text, value, false, false);
394 var length = what.length;
395 what.options[length] = optionName;
397 function ${prefix}country_changed(what) {
398 country = what.options[what.selectedIndex].text;
399 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
400 what.form.${prefix}state.options[i] = null;
402 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
404 foreach my $country ( sort keys %cust_main_county ) {
405 $script_html .= "\nif ( country == \"$country\" ) {\n";
406 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
407 ( my $dstate = $state ) =~ s/[\n\r]//g;
408 my $text = $dstate || '(n/a)';
409 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
411 $script_html .= "}\n";
414 $script_html .= <<END;
416 function ${prefix}state_changed(what) {
420 $script_html .= <<END;
421 state = what.options[what.selectedIndex].text;
422 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
423 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
424 what.form.${prefix}county.options[i] = null;
427 foreach my $country ( sort keys %cust_main_county ) {
428 $script_html .= "\nif ( country == \"$country\" ) {\n";
429 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
430 $script_html .= "\nif ( state == \"$state\" ) {\n";
431 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
432 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
433 my $text = $county || '(n/a)';
435 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
437 $script_html .= "}\n";
439 $script_html .= "}\n";
443 $script_html .= <<END;
448 my $county_html = $script_html;
450 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
451 $county_html .= '</SELECT>';
454 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
457 my $state_html = qq!<SELECT NAME="${prefix}state" !.
458 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
459 foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
460 my $text = $state || '(n/a)';
461 my $selected = $state eq $selected_state ? 'SELECTED' : '';
462 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
464 $state_html .= '</SELECT>';
466 $state_html .= '</SELECT>';
468 my $country_html = qq!<SELECT NAME="${prefix}country" !.
469 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
470 my $countrydefault = $conf->config('countrydefault') || 'US';
471 foreach my $country (
472 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
473 keys %cust_main_county
475 my $selected = $country eq $selected_country ? ' SELECTED' : '';
476 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
478 $country_html .= '</SELECT>';
480 ($county_html, $state_html, $country_html);
488 regionselector? putting web ui components in here? they should probably live
493 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base