X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Ftax_rate.pm;h=4516004321039a72e0620a5c371d0f2ce100b335;hp=550dca53c8440799e946eea8866f1a7b3fedf878;hb=e9e0cf0989259b94d9758eceff448666a2e5a5cc;hpb=4e13a8a470a20ee8b59d95bdf4c6b033a4c10a8d diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm index 550dca53c..451600432 100644 --- a/FS/FS/tax_rate.pm +++ b/FS/FS/tax_rate.pm @@ -1,7 +1,8 @@ package FS::tax_rate; +use base qw( FS::Record ); use strict; -use vars qw( @ISA $DEBUG $me +use vars qw( $DEBUG $me %tax_unittypes %tax_maxtypes %tax_basetypes %tax_authorities %tax_passtypes %GetInfoType $keep_cch_files ); use Date::Parse; @@ -10,6 +11,8 @@ use DateTime::Format::Strptime; use Storable qw( thaw nfreeze ); use IO::File; use File::Temp; +use Text::CSV_XS; +use URI::Escape; use LWP::UserAgent; use HTTP::Request; use HTTP::Response; @@ -18,6 +21,7 @@ use DBIx::DBSchema; use DBIx::DBSchema::Table; use DBIx::DBSchema::Column; use FS::Record qw( qsearch qsearchs dbh dbdef ); +use FS::Conf; use FS::tax_class; use FS::cust_bill_pkg; use FS::cust_tax_location; @@ -27,12 +31,6 @@ use FS::part_pkg_taxproduct; use FS::cust_main; use FS::Misc qw( csv_from_fixed ); -#i'd like to dump these -use FS::CGI qw(rooturl popurl); -use URI::Escape; - -@ISA = qw( FS::Record ); - $DEBUG = 0; $me = '[FS::tax_rate]'; $keep_cch_files = 0; @@ -215,7 +213,7 @@ sub check { $self->ut_numbern('taxnum') || $self->ut_text('geocode') || $self->ut_textn('data_vendor') - || $self->ut_textn('location') + || $self->ut_cch_textn('location') || $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum') || $self->ut_snumbern('effective_date') || $self->ut_float('tax') @@ -245,6 +243,18 @@ sub check { } +#ut_text / ut_textn w/ ` added cause now that's in the data +sub ut_cch_textn { + my($self,$field)=@_; + $self->getfield($field) + =~ /^([\wô \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=\[\]\<\>\`]*)$/ + or return gettext('illegal_or_empty_text'). " $field: ". + $self->getfield($field); + $self->setfield($field,$1); + ''; + +} + =item taxclass_description Returns the human understandable value associated with the related @@ -361,7 +371,7 @@ sub passtype_name { $tax_passtypes{$self->passtype}; } -=item taxline TAXABLES, [ OPTIONSHASH ] +=item taxline TAXABLES Returns a listref of a name and an amount of tax calculated for the list of packages/amounts referenced by TAXABLES. If an error occurs, a message @@ -371,13 +381,13 @@ is returned as a scalar. sub taxline { my $self = shift; + # this used to accept a hash of options but none of them did anything + # so it's been removed. my $taxables; - my %opt = (); if (ref($_[0]) eq 'ARRAY') { $taxables = shift; - %opt = @_; }else{ $taxables = [ @_ ]; #exemptions would be broken in this case @@ -413,7 +423,7 @@ sub taxline { } my $maxtype = $self->maxtype || 0; - if ($maxtype != 0 && $maxtype != 9) { + if ($maxtype != 0 && $maxtype != 1 && $maxtype != 9) { return $self->_fatal_or_null( 'tax with "'. $self->maxtype_name. '" threshold' ); @@ -445,28 +455,43 @@ sub taxline { my $taxable_units = 0; unless ($self->recurtax =~ /^Y$/i) { - if (( $self->unittype || 0 ) == 0) { + + if (( $self->unittype || 0 ) == 0) { #access line my %seen = (); foreach (@cust_bill_pkg) { $taxable_units += $_->units - unless $seen{$_->pkgnum}; - $seen{$_->pkgnum}++; + unless $seen{$_->pkgnum}++; } - }elsif ($self->unittype == 1) { + + } elsif ($self->unittype == 1) { #minute return $self->_fatal_or_null( 'fee with minute unit type' ); - }elsif ($self->unittype == 2) { - $taxable_units = 1; - }else { + + } elsif ($self->unittype == 2) { #account + + my $conf = new FS::Conf; + if ( $conf->exists('tax-pkg_address') ) { + #number of distinct locations + my %seen = (); + foreach (@cust_bill_pkg) { + $taxable_units++ + unless $seen{$_->cust_pkg->locationnum}++; + } + } else { + $taxable_units = 1; + } + + } else { return $self->_fatal_or_null( 'unknown unit type in tax'. $self->taxnum ); } + } - # - # XXX insert exemption handling here + # XXX handle excessrate (use_excessrate) / excessfee / + # taxbase/feebase / taxmax/feemax + # and eventually exemptions # # the tax or fee is applied to taxbase or feebase and then # the excessrate or excess fee is applied to taxmax or feemax - # $amount += $taxable_charged * $self->tax; $amount += $taxable_units * $self->fee; @@ -499,10 +524,10 @@ sub _fatal_or_null { } } -=item tax_on_tax CUST_MAIN +=item tax_on_tax CUST_LOCATION Returns a list of taxes which are candidates for taxing taxes for the -given customer (see L) +given service location (see L) =cut @@ -510,13 +535,13 @@ given customer (see L) sub tax_on_tax { #akshun my $self = shift; - my $cust_main = shift; + my $cust_location = shift; warn "looking up taxes on tax ". $self->taxnum. " for customer ". - $cust_main->custnum + $cust_location->custnum if $DEBUG; - my $geocode = $cust_main->geocode($self->data_vendor); + my $geocode = $cust_location->geocode($self->data_vendor); # CCH oddness in m2m my $dbh = dbh; @@ -623,6 +648,7 @@ sub batch_import { $count *=2; if ( $format eq 'cch' || $format eq 'cch-update' ) { + #false laziness w/below (sub _perform_cch_diff) @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax excessrate effective_date taxauth taxtype taxcat taxname usetax useexcessrate fee unittype feemax maxtype passflag @@ -701,9 +727,6 @@ sub batch_import { die "unknown format $format"; } - eval "use Text::CSV_XS;"; - die $@ if $@; - my $csv = new Text::CSV_XS; my $imported = 0; @@ -744,9 +767,10 @@ sub batch_import { foreach my $field ( @fields ) { $tax_rate{$field} = shift @columns; } + if ( scalar( @columns ) ) { $dbh->rollback if $oldAutoCommit; - return "Unexpected trailing columns in line (wrong format?): $line"; + return "Unexpected trailing columns in line (wrong format?) importing tax_rate: $line"; } my $error = &{$hook}(\%tax_rate); @@ -771,7 +795,8 @@ sub batch_import { } - for (grep { !exists($delete{$_}) } keys %insert) { + my @replace = grep { exists($delete{$_}) } keys %insert; + for (@replace) { if ( $job ) { # progress bar if ( time - $min_sec > $last ) { my $error = $job->update_statustext( @@ -785,20 +810,35 @@ sub batch_import { } } - my $tax_rate = new FS::tax_rate( $insert{$_} ); - my $error = $tax_rate->insert; + my $old = qsearchs( 'tax_rate', $delete{$_} ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - my $hashref = $insert{$_}; - $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) ); - return "can't insert tax_rate for $line: $error"; + if ( $old ) { + + my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' }); + $new->taxnum($old->taxnum); + my $error = $new->replace($old); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + my $hashref = $insert{$_}; + $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) ); + return "can't replace tax_rate for $line: $error"; + } + + $imported++; + + } else { + + $old = delete $delete{$_}; + warn "WARNING: can't find tax_rate to replace (inserting instead and continuing) for: ". + #join(" ", map { "$_ => ". $old->{$_} } @fields); + join(" ", map { "$_ => ". $old->{$_} } keys(%$old) ); } $imported++; } - for (grep { exists($delete{$_}) } keys %insert) { + for (grep { !exists($delete{$_}) } keys %insert) { if ( $job ) { # progress bar if ( time - $min_sec > $last ) { my $error = $job->update_statustext( @@ -812,27 +852,17 @@ sub batch_import { } } - my $old = qsearchs( 'tax_rate', $delete{$_} ); - unless ($old) { - $dbh->rollback if $oldAutoCommit; - $old = $delete{$_}; - return "can't find tax_rate to replace for: ". - #join(" ", map { "$_ => ". $old->{$_} } @fields); - join(" ", map { "$_ => ". $old->{$_} } keys(%$old) ); - } - my $new = new FS::tax_rate({ $old->hash, %{$insert{$_}}, 'manual' => '' }); - $new->taxnum($old->taxnum); - my $error = $new->replace($old); + my $tax_rate = new FS::tax_rate( $insert{$_} ); + my $error = $tax_rate->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; my $hashref = $insert{$_}; $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) ); - return "can't replace tax_rate for $line: $error"; + return "can't insert tax_rate for $line: $error"; } $imported++; - $imported++; } for (grep { !exists($insert{$_}) } keys %delete) { @@ -947,7 +977,7 @@ sub _perform_batch_import { my $file = lc($name). 'file'; unless ($files{$file}) { - $error = "No $name supplied"; + #$error = "No $name supplied"; next; } next if $name eq 'DETAIL' && $format =~ /update/; @@ -964,7 +994,7 @@ sub _perform_batch_import { unlink $filename or warn "Can't delete $filename: $!" unless $keep_cch_files; push @insert_list, $name, $insertname, $import_sub, $format; - if ( $name eq 'GEOCODE' ) { #handle this whole ordering issue better + if ( $name eq 'GEOCODE' || $name eq 'CODE' ) { #handle this whole ordering issue better unshift @predelete_list, $name, $deletename, $import_sub, $format; } else { unshift @delete_list, $name, $deletename, $import_sub, $format; @@ -979,13 +1009,20 @@ sub _perform_batch_import { } push @insert_list, - 'DETAIL', "$dir/".$files{detail}, \&FS::tax_rate::batch_import, $format + 'DETAIL', "$dir/".$files{detailfile}, \&FS::tax_rate::batch_import, $format if $format =~ /update/; + my %addl_param = (); + if ( $param->{'delete_only'} ) { + $addl_param{'delete_only'} = $param->{'delete_only'}; + @insert_list = () + } + $error ||= _perform_cch_tax_import( $job, [ @predelete_list ], [ @insert_list ], [ @delete_list ], + \%addl_param, ); @@ -1010,7 +1047,8 @@ sub _perform_batch_import { sub _perform_cch_tax_import { - my ( $job, $predelete_list, $insert_list, $delete_list ) = @_; + my ( $job, $predelete_list, $insert_list, $delete_list, $addl_param ) = @_; + $addl_param ||= {}; my $error = ''; foreach my $list ($predelete_list, $insert_list, $delete_list) { @@ -1019,7 +1057,11 @@ sub _perform_cch_tax_import { my $fmt = "$format-update"; $fmt = $format. ( lc($name) eq 'zip' ? '-zip' : '' ); open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!"; - $error ||= &{$method}({ 'filehandle' => $fh, 'format' => $fmt }, $job); + my $param = { 'filehandle' => $fh, + 'format' => $fmt, + %$addl_param, + }; + $error ||= &{$method}($param, $job); close $fh; } } @@ -1101,8 +1143,26 @@ sub _perform_cch_diff { } close $newcsvfh; - for (keys %oldlines) { - print $dfh $_, ',"D"', "\n" if $oldlines{$_}; + #false laziness w/above (sub batch_import) + my @fields = qw( geocode inoutcity inoutlocal tax location taxbase taxmax + excessrate effective_date taxauth taxtype taxcat taxname + usetax useexcessrate fee unittype feemax maxtype passflag + passtype basetype ); + my $numfields = scalar(@fields); + + my $csv = new Text::CSV_XS { 'always_quote' => 1 }; + + for my $line (grep $oldlines{$_}, keys %oldlines) { + + $csv->parse($line) or do { + #$dbh->rollback if $oldAutoCommit; + die "can't parse: ". $csv->error_input(); + }; + my @columns = $csv->fields(); + + $csv->combine( splice(@columns, 0, $numfields) ); + + print $dfh $csv->string, ',"D"', "\n"; } close $dfh; @@ -1156,9 +1216,6 @@ sub _cch_fetch_and_unzip { sub _cch_extract_csv_from_dbf { my ( $job, $dir, $name ) = @_; - eval "use Text::CSV_XS;"; - die $@ if $@; - eval "use XBase;"; die $@ if $@; @@ -1182,9 +1239,14 @@ sub _cch_extract_csv_from_dbf { $date; }; while (my $row = $cursor->fetch_hashref) { - $csv->combine( map { ($table->field_type($_) eq 'D') - ? &{$format_date}($row->{$_}) - : $row->{$_} + $csv->combine( map { my $type = $table->field_type($_); + if ($type eq 'D') { + &{$format_date}($row->{$_}) ; + } elsif ($type eq 'N' && $row->{$_} =~ /e-/i ) { + sprintf('%.8f', $row->{$_}); #db row is numeric(14,8) + } else { + $row->{$_}; + } } @fields ); @@ -1318,7 +1380,7 @@ sub _restore_remembered_tax_products { unless ( $part_pkg_taxproduct ) { return "failed to find part_pkg_taxproduct (". - $taxproduct->{pkgpart}->{$class}. ") for pkgpart $pkgpart\n"; + $taxproduct->{$pkgpart}->{$class}. ") for pkgpart $pkgpart\n"; } if ( $class eq '' ) { @@ -1616,16 +1678,16 @@ sub process_download_and_update { if (-d $dir) { - if (-d "$dir.4") { - opendir(my $dirh, "$dir.4") or die "failed to open $dir.4: $!\n"; + if (-d "$dir.9") { + opendir(my $dirh, "$dir.9") or die "failed to open $dir.9: $!\n"; foreach my $file (readdir($dirh)) { - unlink "$dir.4/$file" if (-f "$dir.4/$file"); + unlink "$dir.9/$file" if (-f "$dir.9/$file"); } closedir($dirh); - rmdir "$dir.4"; + rmdir "$dir.9"; } - for (3, 2, 1) { + for (8, 7, 6, 5, 4, 3, 2, 1) { if ( -e "$dir.$_" ) { rename "$dir.$_", "$dir.". ($_+1) or die "can't rename $dir.$_: $!\n"; } @@ -1767,17 +1829,21 @@ Launches a tax liability report. =cut sub queue_liability_report { - my $cgi = shift; + my $job = shift; + my $param = thaw(decode_base64(shift)); + + my $cgi = new CGI; + $cgi->param('beginning', $param->{beginning}); + $cgi->param('ending', $param->{ending}); my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi); - my $agentnum = $cgi->param('agentnum'); - $agentnum =~ /^(\d+)$/ ? $agentnum = $1 : $agentnum = ''; - my $job = new FS::queue { job => 'FS::tax_rate::generate_liability_report' }; - $job->insert( + my $agentnum = $param->{agentnum}; + if ($agentnum =~ /^(\d+)$/) { $agentnum = $1; } else { $agentnum = ''; }; + generate_liability_report( 'beginning' => $beginning, 'ending' => $ending, 'agentnum' => $agentnum, - 'p' => popurl(2), - 'rooturl' => rooturl, + 'p' => $param->{RootURL}, + 'job' => $job, ); } @@ -1788,9 +1854,12 @@ agentnum, beginning, and ending =cut +#shit, all sorts of false laxiness w/report_newtax.cgi sub generate_liability_report { my %args = @_; + my ( $count, $last, $min_sec ) = _progressbar_foo(); + #let us open the temp file early my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc; my $report = new File::Temp( TEMPLATE => 'report.tax.liability.XXXXXXXX', @@ -1822,12 +1891,9 @@ sub generate_liability_report { $where .= ' AND cust_main.agentnum = '. $agent->agentnum; } - # my ( $location_sql, @location_param ) = FS::cust_pkg->location_sql; - # $where .= " AND $location_sql"; - #my @taxparam = ( 'itemdesc', @location_param ); - # now something along the lines of geocode matching ? - #$where .= FS::cust_pkg->_location_sql_where('cust_tax_location');; - my @taxparam = ( 'itemdesc', 'tax_rate_location.state', 'tax_rate_location.county', 'tax_rate_location.city', 'cust_bill_pkg_tax_rate_location.locationtaxid' ); + #my @taxparam = ( 'itemdesc', 'tax_rate_location.state', 'tax_rate_location.county', 'tax_rate_location.city', 'cust_bill_pkg_tax_rate_location.locationtaxid' ); + my @taxparams = qw( city county state locationtaxid ); + my @params = ('itemdesc', @taxparams); my $select = 'DISTINCT itemdesc,locationtaxid,tax_rate_location.state,tax_rate_location.county,tax_rate_location.city'; @@ -1845,15 +1911,26 @@ sub generate_liability_report { my $credit = 0; my %taxes = (); my %basetaxes = (); - foreach my $t (qsearch({ table => 'cust_bill_pkg', - select => $select, - hashref => { pkgpart => 0 }, - addl_from => $addl_from, - extra_sql => $where, - }) - ) - { - my @params = map { my $f = $_; $f =~ s/.*\.//; $f } @taxparam; + my $calculated = 0; + my @tax_and_location = qsearch({ table => 'cust_bill_pkg', + select => $select, + hashref => { pkgpart => 0 }, + addl_from => $addl_from, + extra_sql => $where, + }); + $count = scalar(@tax_and_location); + foreach my $t ( @tax_and_location ) { + + if ( $args{job} ) { + if ( time - $min_sec > $last ) { + $args{job}->update_statustext( int( 100 * $calculated / $count ). + ",Calculating" + ); + $last = time; + } + } + + #my @params = map { my $f = $_; $f =~ s/.*\.//; $f } @taxparam; my $label = join('~', map { $t->$_ } @params); $label = 'Tax'. $label if $label =~ /^~/; unless ( exists( $taxes{$label} ) ) { @@ -1863,25 +1940,31 @@ sub generate_liability_report { $taxes{$label}->{'url_param'} = join(';', map { "$_=". uri_escape($t->$_) } @params); - my $taxwhere = "FROM cust_bill_pkg $addl_from $where AND payby != 'COMP' ". - "AND ". join( ' AND ', map { "( $_ = ? OR ? = '' AND $_ IS NULL)" } @taxparam ); + my $payby_itemdesc_loc = + " payby != 'COMP' ". + "AND ( itemdesc = ? OR ? = '' AND itemdesc IS NULL ) ". + "AND ". FS::tax_rate_location->location_sql( map { $_ => $t->$_ } + @taxparams + ); - my $sql = "SELECT SUM(cust_bill_pkg.setup+cust_bill_pkg.recur) ". - " $taxwhere AND cust_bill_pkg.pkgnum = 0"; + my $taxwhere = + "FROM cust_bill_pkg $addl_from $where AND $payby_itemdesc_loc"; - my $x = &{$scalar_sql}($t, [ map { $_, $_ } @params ], $sql ); + my $sql = "SELECT SUM(amount) $taxwhere AND cust_bill_pkg.pkgnum = 0"; + + my $x = &{$scalar_sql}($t, [ 'itemdesc', 'itemdesc' ], $sql ); $tax += $x; $taxes{$label}->{'tax'} += $x; - my $creditfrom = " JOIN cust_credit_bill_pkg USING (billpkgnum,billpkgtaxratelocationnum) "; - my $creditwhere = "FROM cust_bill_pkg $addl_from $creditfrom $where ". - "AND payby != 'COMP' ". - "AND ". join( ' AND ', map { "( $_ = ? OR ? = '' AND $_ IS NULL)" } @taxparam ); + my $creditfrom = + "JOIN cust_credit_bill_pkg USING (billpkgnum,billpkgtaxratelocationnum)"; + my $creditwhere = + "FROM cust_bill_pkg $addl_from $creditfrom $where AND $payby_itemdesc_loc"; $sql = "SELECT SUM(cust_credit_bill_pkg.amount) ". " $creditwhere AND cust_bill_pkg.pkgnum = 0"; - my $y = &{$scalar_sql}($t, [ map { $_, $_ } @params ], $sql ); + my $y = &{$scalar_sql}($t, [ 'itemdesc', 'itemdesc' ], $sql ); $credit += $y; $taxes{$label}->{'credit'} += $y; @@ -1907,6 +1990,12 @@ sub generate_liability_report { #ordering + + if ( $args{job} ) { + $args{job}->update_statustext( "0,Sorted" ); + $last = time; + } + my @taxes = (); foreach my $tax ( sort { $a cmp $b } keys %taxes ) { @@ -1935,7 +2024,7 @@ sub generate_liability_report { $dateagentlink .= ';agentnum='. $args{agentnum} if length($agentname); my $baselink = $args{p}. "search/cust_bill_pkg.cgi?$dateagentlink"; - + my $creditlink = $args{p}. "search/cust_credit_bill_pkg.html?$dateagentlink"; print $report < - Tax collected + Tax invoiced      Tax credited @@ -1968,8 +2057,19 @@ EOF my $bgcolor2 = '#ffffff'; my $bgcolor = ''; + $count = scalar(@taxes); + $calculated = 0; foreach my $tax ( @taxes ) { + if ( $args{job} ) { + if ( time - $min_sec > $last ) { + $args{job}->update_statustext( int( 100 * $calculated / $count ). + ",Generated" + ); + $last = time; + } + } + if ( $bgcolor eq $bgcolor1 ) { $bgcolor = $bgcolor2; } else { @@ -1992,7 +2092,7 @@ EOF <% ($tax->{base}) ? qq!! : '' %> - <% '$money_char' %><% sprintf('%.2f', $tax->{'credit'} ) %> + <% '$money_char' %><% sprintf('%.2f', $tax->{'credit'} ) %> <% !($tax->{base}) ? qq!! : '' %> @@ -2012,7 +2112,7 @@ EOF my $dropstring = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/report.'; $reportname =~ s/^$dropstring//; - my $reporturl = $args{rooturl}. "/misc/queued_report?report=$reportname"; + my $reporturl = "%%%ROOTURL%%%/misc/queued_report?report=$reportname"; die "view\n"; } @@ -2030,8 +2130,7 @@ EOF =head1 SEE ALSO -L, L, L, schema.html from the base -documentation. +L, L, L =cut