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 taxline CUST_BILL_PKG, ...
162 Returns a listref of a name and an amount of tax calculated for the list of
163 packages. Returns a scalar error message on error.
170 local $SIG{HUP} = 'IGNORE';
171 local $SIG{INT} = 'IGNORE';
172 local $SIG{QUIT} = 'IGNORE';
173 local $SIG{TERM} = 'IGNORE';
174 local $SIG{TSTP} = 'IGNORE';
175 local $SIG{PIPE} = 'IGNORE';
177 my $oldAutoCommit = $FS::UID::AutoCommit;
178 local $FS::UID::AutoCommit = 0;
181 my $name = $self->taxname || 'Tax';
184 foreach my $cust_bill_pkg (@_) {
186 my $cust_bill = $cust_bill_pkg->cust_pkg->cust_bill;
187 my $part_pkg = $cust_bill_pkg->part_pkg;
189 my $taxable_charged = 0;
190 $taxable_charged += $cust_bill_pkg->setup
191 unless $part_pkg->setuptax =~ /^Y$/i
192 || $self->setuptax =~ /^Y$/i;
193 $taxable_charged += $cust_bill_pkg->recur
194 unless $part_pkg->recurtax =~ /^Y$/i
195 || $self->recurtax =~ /^Y$/i;
198 unless $taxable_charged;
200 if ( $self->exempt_amount && $self->exempt_amount > 0 ) {
201 #my ($mon,$year) = (localtime($cust_bill_pkg->sdate) )[4,5];
203 (localtime( $cust_bill_pkg->sdate || $cust_bill->_date ) )[4,5];
205 my $freq = $part_pkg->freq || 1;
206 if ( $freq !~ /(\d+)$/ ) {
207 $dbh->rollback if $oldAutoCommit;
208 return "daily/weekly package definitions not (yet?)".
209 " compatible with monthly tax exemptions";
211 my $taxable_per_month =
212 sprintf("%.2f", $taxable_charged / $freq );
214 #call the whole thing off if this customer has any old
215 #exemption records...
216 my @cust_tax_exempt =
217 qsearch( 'cust_tax_exempt' => { custnum=> $cust_bill->custnum } );
218 if ( @cust_tax_exempt ) {
219 $dbh->rollback if $oldAutoCommit;
221 'this customer still has old-style tax exemption records; '.
222 'run bin/fs-migrate-cust_tax_exempt?';
225 foreach my $which_month ( 1 .. $freq ) {
227 #maintain the new exemption table now
230 FROM cust_tax_exempt_pkg
231 LEFT JOIN cust_bill_pkg USING ( billpkgnum )
232 LEFT JOIN cust_bill USING ( invnum )
238 my $sth = dbh->prepare($sql) or do {
239 $dbh->rollback if $oldAutoCommit;
240 return "fatal: can't lookup exising exemption: ". dbh->errstr;
248 $dbh->rollback if $oldAutoCommit;
249 return "fatal: can't lookup exising exemption: ". dbh->errstr;
251 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
253 my $remaining_exemption =
254 $self->exempt_amount - $existing_exemption;
255 if ( $remaining_exemption > 0 ) {
256 my $addl = $remaining_exemption > $taxable_per_month
258 : $remaining_exemption;
259 $taxable_charged -= $addl;
261 my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
262 'billpkgnum' => $cust_bill_pkg->billpkgnum,
263 'taxnum' => $self->taxnum,
264 'year' => 1900+$year,
266 'amount' => sprintf("%.2f", $addl ),
268 my $error = $cust_tax_exempt_pkg->insert;
270 $dbh->rollback if $oldAutoCommit;
271 return "fatal: can't insert cust_tax_exempt_pkg: $error";
273 } # if $remaining_exemption > 0
277 #until ( $mon < 12 ) { $mon -= 12; $year++; }
278 until ( $mon < 13 ) { $mon -= 12; $year++; }
280 } #foreach $which_month
282 } #if $tax->exempt_amount
284 $taxable_charged = sprintf( "%.2f", $taxable_charged);
286 $amount += $taxable_charged * $self->tax / 100
289 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
290 return [ $name, $amount ]
299 =item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE [ DISABLED ] ] ] ]
304 my ( $selected_county, $selected_state, $selected_country,
305 $prefix, $onchange, $disabled ) = @_;
307 $prefix = '' unless defined $prefix;
311 # unless ( @cust_main_county ) { #cache
312 @cust_main_county = qsearch('cust_main_county', {} );
313 foreach my $c ( @cust_main_county ) {
314 $countyflag=1 if $c->county;
315 #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
316 $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
319 $countyflag=1 if $selected_county;
321 my $script_html = <<END;
323 function opt(what,value,text) {
324 var optionName = new Option(text, value, false, false);
325 var length = what.length;
326 what.options[length] = optionName;
328 function ${prefix}country_changed(what) {
329 country = what.options[what.selectedIndex].text;
330 for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
331 what.form.${prefix}state.options[i] = null;
333 #what.form.${prefix}state.options[0] = new Option('', '', false, true);
335 foreach my $country ( sort keys %cust_main_county ) {
336 $script_html .= "\nif ( country == \"$country\" ) {\n";
337 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
338 ( my $dstate = $state ) =~ s/[\n\r]//g;
339 my $text = $dstate || '(n/a)';
340 $script_html .= qq!opt(what.form.${prefix}state, "$dstate", "$text");\n!;
342 $script_html .= "}\n";
345 $script_html .= <<END;
347 function ${prefix}state_changed(what) {
351 $script_html .= <<END;
352 state = what.options[what.selectedIndex].text;
353 country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
354 for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
355 what.form.${prefix}county.options[i] = null;
358 foreach my $country ( sort keys %cust_main_county ) {
359 $script_html .= "\nif ( country == \"$country\" ) {\n";
360 foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
361 $script_html .= "\nif ( state == \"$state\" ) {\n";
362 #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
363 foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
364 my $text = $county || '(n/a)';
366 qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
368 $script_html .= "}\n";
370 $script_html .= "}\n";
374 $script_html .= <<END;
379 my $county_html = $script_html;
381 $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange" $disabled>!;
382 $county_html .= '</SELECT>';
385 qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
388 my $state_html = qq!<SELECT NAME="${prefix}state" !.
389 qq!onChange="${prefix}state_changed(this); $onchange" $disabled>!;
390 foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
391 my $text = $state || '(n/a)';
392 my $selected = $state eq $selected_state ? 'SELECTED' : '';
393 $state_html .= qq(\n<OPTION $selected VALUE="$state">$text</OPTION>);
395 $state_html .= '</SELECT>';
397 $state_html .= '</SELECT>';
399 my $country_html = qq!<SELECT NAME="${prefix}country" !.
400 qq!onChange="${prefix}country_changed(this); $onchange" $disabled>!;
401 my $countrydefault = $conf->config('countrydefault') || 'US';
402 foreach my $country (
403 sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
404 keys %cust_main_county
406 my $selected = $country eq $selected_country ? ' SELECTED' : '';
407 $country_html .= qq(\n<OPTION$selected VALUE="$country">$country</OPTION>");
409 $country_html .= '</SELECT>';
411 ($county_html, $state_html, $country_html);
419 regionselector? putting web ui components in here? they should probably live
424 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base