X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=ceefeaf69bb8a316028b45d392414075db25d29d;hb=6626dc2a13c809092aa539c5a72bc72a0c56afdc;hp=de4d38de9615bbebcce8d9afe4c1300c1d8abedd;hpb=34f91089a0d54fa39d622ec5e03d9e09a50e9f79;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index de4d38de9..ceefeaf69 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -45,8 +45,6 @@ use FS::part_pkg; use FS::part_event; use FS::part_event_condition; #use FS::cust_event; -use FS::cust_tax_exempt; -use FS::cust_tax_exempt_pkg; use FS::type_pkgs; use FS::payment_gateway; use FS::agent_payment_gateway; @@ -2078,6 +2076,7 @@ sub bill { my( $total_setup, $total_recur ) = ( 0, 0 ); my %tax; + my %taxlisthash; my @precommit_hooks = (); foreach my $cust_pkg ( @@ -2166,8 +2165,7 @@ sub bill { # only for figuring next bill date, nothing else, so, reset $sdate again # here $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; - $cust_pkg->last_bill($sdate) - if $cust_pkg->dbdef_table->column('last_bill'); + $cust_pkg->last_bill($sdate); if ( $part_pkg->freq =~ /^\d+$/ ) { $mon += $part_pkg->freq; @@ -2248,140 +2246,94 @@ sub bill { unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { + my @taxes = (); + my @taxoverrides = $part_pkg->part_pkg_taxoverride; + my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) ) ? 'ship_' : ''; - my %taxhash = map { $_ => $self->get("$prefix$_") } - qw( state county country ); - $taxhash{'taxclass'} = $part_pkg->taxclass; + if ( $conf->exists('enable_taxproducts') + && (scalar(@taxoverrides) || $part_pkg->taxproductnum ) + ) + { - my @taxes = qsearch( 'cust_main_county', \%taxhash ); + my @taxclassnums = (); + my $geocode = $self->geocode('cch'); - unless ( @taxes ) { - $taxhash{'taxclass'} = ''; - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } + if ( scalar( @taxoverrides ) ) { + @taxclassnums = map { $_->taxclassnum } @taxoverrides; + }elsif ( $part_pkg->taxproductnum ) { + @taxclassnums = map { $_->taxclassnum } + $part_pkg->part_pkg_taxrate('cch', $geocode); + } - #one more try at a whole-country tax rate - unless ( @taxes ) { - $taxhash{$_} = '' foreach qw( state county ); - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } + my $extra_sql = + "AND (". + join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")"; + + @taxes = qsearch({ 'table' => 'tax_rate', + 'hashref' => { 'geocode' => $geocode, }, + 'extra_sql' => $extra_sql, + }) + if scalar(@taxclassnums); + + + }else{ + + my %taxhash = map { $_ => $self->get("$prefix$_") } + qw( state county country ); + + $taxhash{'taxclass'} = $part_pkg->taxclass; + + @taxes = qsearch( 'cust_main_county', \%taxhash ); + + unless ( @taxes ) { + $taxhash{'taxclass'} = ''; + @taxes = qsearch( 'cust_main_county', \%taxhash ); + } + + #one more try at a whole-country tax rate + unless ( @taxes ) { + $taxhash{$_} = '' foreach qw( state county ); + @taxes = qsearch( 'cust_main_county', \%taxhash ); + } + + } #if $conf->exists('enable_taxproducts') # maybe eliminate this entirely, along with all the 0% records unless ( @taxes ) { $dbh->rollback if $oldAutoCommit; - return - "fatal: can't find tax rate for state/county/country/taxclass ". - join('/', ( map $self->get("$prefix$_"), - qw(state county country) - ), - $part_pkg->taxclass ). "\n"; + my $error; + if ( $conf->exists('enable_taxproducts') ) { + $error = + "fatal: can't find tax rate for zip/taxproduct/pkgpart ". + join('/', ( map $self->get("$prefix$_"), + qw(zip) + ), + $part_pkg->taxproduct_description, + $part_pkg->pkgpart ). "\n"; + }else{ + $error = + "fatal: can't find tax rate for state/county/country/taxclass ". + join('/', ( map $self->get("$prefix$_"), + qw(state county country) + ), + $part_pkg->taxclass ). "\n"; + } + return $error; } foreach my $tax ( @taxes ) { + my $taxname = ref( $tax ). ' '. $tax->taxnum; + if ( exists( $taxlisthash{ $taxname } ) ) { + push @{ $taxlisthash{ $taxname } }, $cust_bill_pkg; + }else{ + $taxlisthash{ $taxname } = [ $tax, $cust_bill_pkg ]; + } + } - my $taxable_charged = 0; - $taxable_charged += $setup - unless $part_pkg->setuptax =~ /^Y$/i - || $tax->setuptax =~ /^Y$/i; - $taxable_charged += $recur - unless $part_pkg->recurtax =~ /^Y$/i - || $tax->recurtax =~ /^Y$/i; - next unless $taxable_charged; - - if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) { - #my ($mon,$year) = (localtime($sdate) )[4,5]; - my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5]; - $mon++; - my $freq = $part_pkg->freq || 1; - if ( $freq !~ /(\d+)$/ ) { - $dbh->rollback if $oldAutoCommit; - return "daily/weekly package definitions not (yet?)". - " compatible with monthly tax exemptions"; - } - my $taxable_per_month = - sprintf("%.2f", $taxable_charged / $freq ); - - #call the whole thing off if this customer has any old - #exemption records... - my @cust_tax_exempt = - qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } ); - if ( @cust_tax_exempt ) { - $dbh->rollback if $oldAutoCommit; - return - 'this customer still has old-style tax exemption records; '. - 'run bin/fs-migrate-cust_tax_exempt?'; - } - - foreach my $which_month ( 1 .. $freq ) { - - #maintain the new exemption table now - my $sql = " - SELECT SUM(amount) - FROM cust_tax_exempt_pkg - LEFT JOIN cust_bill_pkg USING ( billpkgnum ) - LEFT JOIN cust_bill USING ( invnum ) - WHERE custnum = ? - AND taxnum = ? - AND year = ? - AND month = ? - "; - my $sth = dbh->prepare($sql) or do { - $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; - }; - $sth->execute( - $self->custnum, - $tax->taxnum, - 1900+$year, - $mon, - ) or do { - $dbh->rollback if $oldAutoCommit; - return "fatal: can't lookup exising exemption: ". dbh->errstr; - }; - my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0; - - my $remaining_exemption = - $tax->exempt_amount - $existing_exemption; - if ( $remaining_exemption > 0 ) { - my $addl = $remaining_exemption > $taxable_per_month - ? $taxable_per_month - : $remaining_exemption; - $taxable_charged -= $addl; - - my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( { - 'billpkgnum' => $cust_bill_pkg->billpkgnum, - 'taxnum' => $tax->taxnum, - 'year' => 1900+$year, - 'month' => $mon, - 'amount' => sprintf("%.2f", $addl ), - } ); - $error = $cust_tax_exempt_pkg->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "fatal: can't insert cust_tax_exempt_pkg: $error"; - } - } # if $remaining_exemption > 0 - - #++ - $mon++; - #until ( $mon < 12 ) { $mon -= 12; $year++; } - until ( $mon < 13 ) { $mon -= 12; $year++; } - - } #foreach $which_month - - } #if $tax->exempt_amount - - $taxable_charged = sprintf( "%.2f", $taxable_charged); - - #$tax += $taxable_charged * $cust_main_county->tax / 100 - $tax{ $tax->taxname || 'Tax' } += - $taxable_charged * $tax->tax / 100 - - } #foreach my $tax ( @taxes ) } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' @@ -2411,6 +2363,18 @@ sub bill { my $charged = sprintf( "%.2f", $total_setup + $total_recur ); + foreach my $tax ( keys %taxlisthash ) { + my $tax_object = shift @{ $taxlisthash{$tax} }; + my $listref_or_error = $tax_object->taxline( @{ $taxlisthash{$tax} } ); + unless (ref($listref_or_error)) { + $dbh->rollback if $oldAutoCommit; + return $listref_or_error; + } + + $tax{ $listref_or_error->[0] } += $listref_or_error->[1]; + + } + foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) { my $tax = sprintf("%.2f", $tax{$taxname} ); $charged = sprintf( "%.2f", $charged+$tax ); @@ -3170,7 +3134,7 @@ sub realtime_bop { 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ), }; $cust_pay_pending->payunique( $options{payunique} ) - if length($options{payunique}); + if defined($options{payunique}) && length($options{payunique}); my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted return $cpp_new_err if $cpp_new_err; @@ -3323,7 +3287,8 @@ sub realtime_bop { 'paydate' => $paydate, } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now - $cust_pay->payunique( $options{payunique} ) if length($options{payunique}); + $cust_pay->payunique( $options{payunique} ) + if defined($options{payunique}) && length($options{payunique}); my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; @@ -4633,13 +4598,14 @@ the error, otherwise returns false. sub charge { my $self = shift; - my ( $amount, $pkg, $comment, $taxclass, $additional ); + my ( $amount, $pkg, $comment, $taxclass, $additional, $classnum ); if ( ref( $_[0] ) ) { $amount = $_[0]->{amount}; $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge'; $comment = exists($_[0]->{comment}) ? $_[0]->{comment} : '$'. sprintf("%.2f",$amount); $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : ''; + $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : ''; $additional = $_[0]->{additional}; }else{ $amount = shift; @@ -4666,6 +4632,7 @@ sub charge { 'plan' => 'flat', 'freq' => 0, 'disabled' => 'Y', + 'classnum' => $classnum ? $classnum : '', 'taxclass' => $taxclass, } ); @@ -4860,6 +4827,40 @@ sub country_full { code2country($self->country); } +=item geocode DATA_PROVIDER + +Returns a value for the customer location as encoded by DATA_PROVIDER. +Currently this only makes sense for "CCH" as DATA_PROVIDER. + +=cut + +sub geocode { + my ($self, $data_provider) = (shift, shift); #always cch for now + + my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) ) + ? 'ship_' + : ''; + + my ($zip,$plus4) = split /-/, $self->get("${prefix}zip") + if $self->country eq 'US'; + + #CCH specific location stuff + my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'"; + + my $geocode = ''; + my $cust_tax_location = + qsearchs( { + 'table' => 'cust_tax_location', + 'hashref' => { 'zip' => $zip, 'data_provider' => $data_provider }, + 'extra_sql' => $extra_sql, + } + ); + $geocode = $cust_tax_location->geocode + if $cust_tax_location; + + $geocode; +} + =item cust_status =item status @@ -5191,8 +5192,8 @@ sub _money_table_where { my @where = (); push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'}; if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) { - push @where, "$table._date <= $start" if length($start); - push @where, "$table._date > $end" if length($end); + push @where, "$table._date <= $start" if defined($start) && length($start); + push @where, "$table._date > $end" if defined($end) && length($end); } push @where, @{$opt{'where'}} if $opt{'where'}; my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : ''; @@ -5329,10 +5330,14 @@ sub smart_search { } - } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search - # (also try agent_custid) - # (regex needs tweaking if your - # legacy cust numbers have letters) + # custnum search (also try agent_custid), with some tweaking options if your + # legacy cust "numbers" have letters + } elsif ( $search =~ /^\s*(\d+)\s*$/ + || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+' + && $search =~ /^\s*(\w\w?\d+)\s*$/ + ) + ) + { push @cust_main, qsearch( { 'table' => 'cust_main', @@ -5509,6 +5514,72 @@ sub smart_search { } +=item email_search + +Accepts the following options: I, the email address to search for. The +email address will be searched for as an email invoice destination and as an +svc_acct account. + +#Any additional options are treated as an additional qualifier on the search +#(i.e. I). + +Returns a (possibly empty) array of FS::cust_main objects (but usually just +none or one). + +=cut + +sub email_search { + my %options = @_; + + local($DEBUG) = 1; + + my $email = delete $options{'email'}; + + #we're only being used by RT at the moment... no agent virtualization yet + #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql; + + my @cust_main = (); + + if ( $email =~ /([^@]+)\@([^@]+)/ ) { + + my ( $user, $domain ) = ( $1, $2 ); + + warn "$me smart_search: searching for $user in domain $domain" + if $DEBUG; + + push @cust_main, + map $_->cust_main, + qsearch( { + 'table' => 'cust_main_invoice', + 'hashref' => { 'dest' => $email }, + } + ); + + push @cust_main, + map $_->cust_main, + grep $_, + map $_->cust_svc->cust_pkg, + qsearch( { + 'table' => 'svc_acct', + 'hashref' => { 'username' => $user, }, + 'extra_sql' => + 'AND ( SELECT domain FROM svc_domain + WHERE svc_acct.domsvc = svc_domain.svcnum + ) = '. dbh->quote($domain), + } + ); + } + + my %saw = (); + @cust_main = grep { !$saw{$_->custnum}++ } @cust_main; + + warn "$me smart_search: found ". scalar(@cust_main). " unique customers" + if $DEBUG; + + @cust_main; + +} + =item check_and_rebuild_fuzzyfiles =cut