use FS::cust_tax_location;
use FS::part_pkg_taxrate;
use FS::cust_main;
+use FS::Misc qw( csv_from_fixed );
@ISA = qw( FS::Record );
|| $self->ut_textn('data_vendor')
|| $self->ut_textn('location')
|| $self->ut_foreign_key('taxclassnum', 'tax_class', 'taxclassnum')
- || $self->ut_numbern('effective_date')
+ || $self->ut_snumbern('effective_date')
|| $self->ut_float('tax')
|| $self->ut_floatn('excessrate')
|| $self->ut_money('taxbase')
'11' => 'gross profits',
'12' => 'tariff rate',
'14' => 'account',
+ '15' => 'prior year gross receipts',
);
sub basetype_name {
$tax_passtypes{$self->passtype};
}
-=item taxline CUST_BILL_PKG|AMOUNT, ...
+=item taxline TAXABLES, [ OPTIONSHASH ]
Returns a listref of a name and an amount of tax calculated for the list
-of packages/amounts. If an error occurs, a message is returned as a scalar.
+of packages/amounts referenced by TAXABLES. If an error occurs, a message
+is returned as a scalar.
=cut
sub taxline {
my $self = shift;
+ my $taxables;
+ my %opt = ();
+
+ if (ref($_[0]) eq 'ARRAY') {
+ $taxables = shift;
+ %opt = @_;
+ }else{
+ $taxables = [ @_ ];
+ #exemptions would be broken in this case
+ }
+
my $name = $self->taxname;
$name = 'Other surcharges'
if ($self->passtype == 2);
my $amount = 0;
- return [$name, $amount] # we always know how to handle disabled taxes
- if $self->disabled;
+ if ( $self->disabled ) { # we always know how to handle disabled taxes
+ return {
+ 'name' => $name,
+ 'amount' => $amount,
+ };
+ }
my $taxable_charged = 0;
- my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; } @_;
+ my @cust_bill_pkg = grep { $taxable_charged += $_ unless ref; ref; }
+ @$taxables;
warn "calculating taxes for ". $self->taxnum. " on ".
join (",", map { $_->pkgnum } @cust_bill_pkg)
if $DEBUG;
if ($self->passflag eq 'N') {
- return "fatal: can't (yet) handle taxes not passed to the customer";
+ # return "fatal: can't (yet) handle taxes not passed to the customer";
+ # until someone needs to track these in freeside
+ return {
+ 'name' => $name,
+ 'amount' => 0,
+ };
}
if ($self->maxtype != 0 && $self->maxtype != 9) {
- return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
- '" threshold';
+ return $self->_fatal_or_null( 'tax with "'.
+ $self->maxtype_name. '" threshold'
+ );
}
if ($self->maxtype == 9) {
- return qq!fatal: can't (yet) handle tax with "!. $self->maxtype_name.
- '" threshold'; # "texas" tax
+ return
+ $self->_fatal_or_null( 'tax with "'. $self->maxtype_name. '" threshold' );
+ # "texas" tax
}
+ # we treat gross revenue as gross receipts and expect the tax data
+ # to DTRT (i.e. tax on tax rules)
if ($self->basetype != 0 && $self->basetype != 1 &&
- $self->basetype != 6 && $self->basetype != 7 &&
+ $self->basetype != 5 && $self->basetype != 6 &&
+ $self->basetype != 7 && $self->basetype != 8 &&
$self->basetype != 14
) {
- return qq!fatal: can't (yet) handle tax with "!. $self->basetype_name.
- '" basis';
+ return
+ $self->_fatal_or_null( 'tax with "'. $self->basetype_name. '" basis' );
}
unless ($self->setuptax =~ /^Y$/i) {
my $taxable_units = 0;
unless ($self->recurtax =~ /^Y$/i) {
if ($self->unittype == 0) {
- $taxable_units += $_->units foreach @cust_bill_pkg;
+ my %seen = ();
+ foreach (@cust_bill_pkg) {
+ $taxable_units += $_->units
+ unless $seen{$_->pkgnum};
+ $seen{$_->pkgnum}++;
+ }
}elsif ($self->unittype == 1) {
- return qq!fatal: can't (yet) handle fee with minute unit type!;
+ return $self->_fatal_or_null( 'fee with minute unit type' );
}elsif ($self->unittype == 2) {
$taxable_units = 1;
}else {
- return qq!fatal: can't (yet) handle unknown unit type in tax!.
- $self->taxnum;
+ return $self->_fatal_or_null( 'unknown unit type in tax'. $self->taxnum );
}
}
warn "calculated taxes as [ $name, $amount ]\n"
if $DEBUG;
- return [$name, $amount];
+ return {
+ 'name' => $name,
+ 'amount' => $amount,
+ };
}
+sub _fatal_or_null {
+ my ($self, $error) = @_;
+
+ my $conf = new FS::Conf;
+
+ $error = "fatal: can't yet handle ". $error;
+ my $name = $self->taxname;
+ $name = 'Other surcharges'
+ if ($self->passtype == 2);
+
+ if ($conf->exists('ignore_incalculable_taxes')) {
+ warn $error;
+ return { name => $name, amount => 0 };
+ } else {
+ return $error;
+ }
+}
+
=item tax_on_tax CUST_MAIN
Returns a list of taxes which are candidates for taxing taxes for the
my @fields;
my $hook;
+ my @column_lengths = ();
+ my @column_callbacks = ();
+ if ( $format eq 'cch-fixed' || $format eq 'cch-fixed-update' ) {
+ $format =~ s/-fixed//;
+ my $date_format = sub { my $r='';
+ /^(\d{4})(\d{2})(\d{2})$/ && ($r="$1/$2/$3");
+ $r;
+ };
+ my $trim = sub { my $r = shift; $r =~ s/^\s*//; $r =~ s/\s*$//; $r };
+ push @column_lengths, qw( 10 1 1 8 8 5 8 8 8 1 2 2 30 8 8 10 2 8 2 1 2 2 );
+ push @column_lengths, 1 if $format eq 'cch-update';
+ push @column_callbacks, $trim foreach (@column_lengths); # 5, 6, 15, 17 esp
+ $column_callbacks[8] = $date_format;
+ }
+
my $line;
my ( $count, $last, $min_sec ) = (0, time, 5); #progressbar
- if ( $job ) {
- $count++
- while ( defined($line=<$fh>) );
- seek $fh, 0, 0;
+ if ( $job || scalar(@column_callbacks) ) {
+ my $error =
+ csv_from_fixed(\$fh, \$count, \@column_lengths, \@column_callbacks);
+ return $error if $error;
}
$count *=2;
}
my $actionflag = delete($hash->{'actionflag'});
+
+ $hash->{'taxname'} =~ s/`/'/g;
+ $hash->{'taxname'} =~ s|\\|/|g;
+
+ return '' if $format eq 'cch'; # but not cch-update
+
if ($actionflag eq 'I') {
- $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+ $insert{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
}elsif ($actionflag eq 'D') {
- $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = $hash;
+ $delete{ $hash->{'geocode'}. ':'. $hash->{'taxclassnum'} } = { %$hash };
}else{
return "Unexpected action flag: ". $hash->{'actionflag'};
}
+ delete($hash->{$_}) for keys %$hash;
+
'';
};
return $error;
}
+ if (scalar(keys %tax_rate)) { #inserts only, not updates for cch
+
+ my $tax_rate = new FS::tax_rate( \%tax_rate );
+ $error = $tax_rate->insert;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert tax_rate for $line: $error";
+ }
+
+ }
+
$imported++;
}
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
+ my $hashref = $insert{$_};
+ $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
return "can't insert tax_rate for $line: $error";
}
#join(" ", map { "$_ => ". $old->{$_} } @fields);
join(" ", map { "$_ => ". $old->{$_} } keys(%$old) );
}
- my $new = new FS::tax_rate( $insert{$_} );
+ 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;
- return "can't insert tax_rate for $line: $error";
+ my $hashref = $insert{$_};
+ $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+ return "can't replace tax_rate for $line: $error";
}
$imported++;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "can't insert tax_rate for $line: $error";
+ my $hashref = $delete{$_};
+ $line = join(", ", map { "$_ => ". $hashref->{$_} } keys(%$hashref) );
+ return "can't delete tax_rate for $line: $error";
}
$imported++;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return "Empty file!" unless $imported;
+ return "Empty file!" unless ($imported || $format eq 'cch-update');
''; #no error
}
-=item process_batch
+=item process_batch_import
Load a batch import as a queued JSRPC job
=cut
-sub process_batch {
+sub process_batch_import {
my $job = shift;
my $param = thaw(decode_base64(shift));
my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
- if ($format eq 'cch') {
+ if ($format eq 'cch' || $format eq 'cch-fixed') {
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
my $error = '';
+ my $have_location = 0;
my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
+ 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
'DETAIL', 'detail', \&FS::tax_rate::batch_import,
);
while( scalar(@list) ) {
my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
unless ($files{$file}) {
+ next if $name eq 'PLUS4';
$error = "No $name supplied";
+ $error = "Neither PLUS4 nor ZIP supplied"
+ if ($name eq 'ZIP' && !$have_location);
next;
}
+ $have_location = 1 if $name eq 'PLUS4';
+ my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
my $filename = "$dir/". $files{$file};
open my $fh, "< $filename" or $error ||= "Can't open $name file: $!";
- $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+ $error ||= &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
close $fh;
unlink $filename or warn "Can't delete $filename: $!";
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
}
- }elsif ($format eq 'cch-update') {
+ }elsif ($format eq 'cch-update' || $format eq 'cch-fixed-update') {
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my @list = ( 'CODE', 'codefile', \&FS::tax_class::batch_import,
'PLUS4', 'plus4file', \&FS::cust_tax_location::batch_import,
+ 'ZIP', 'zipfile', \&FS::cust_tax_location::batch_import,
'TXMATRIX', 'txmatrix', \&FS::part_pkg_taxrate::batch_import,
);
my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc;
while( scalar(@list) ) {
my ($name, $file, $import_sub) = (shift @list, shift @list, shift @list);
unless ($files{$file}) {
+ my $vendor = $name eq 'ZIP' ? 'cch' : 'cch-zip';
+ next # update expected only for previously installed location data
+ if ( ($name eq 'PLUS4' || $name eq 'ZIP')
+ && !scalar( qsearch( { table => 'cust_tax_location',
+ hashref => { data_vendor => $vendor },
+ select => 'DISTINCT data_vendor',
+ } )
+ )
+ );
+
$error = "No $name supplied";
next;
}
UNLINK => 0, #meh
) or die "can't open temp file: $!\n";
+ my $insert_pattern = ($format eq 'cch-update') ? qr/"I"\s*$/ : qr/I\s*$/;
+ my $delete_pattern = ($format eq 'cch-update') ? qr/"D"\s*$/ : qr/D\s*$/;
while(<$fh>) {
my $handle = '';
- $handle = $ifh if $_ =~ /"I"\s*$/;
- $handle = $dfh if $_ =~ /"D"\s*$/;
+ $handle = $ifh if $_ =~ /$insert_pattern/;
+ $handle = $dfh if $_ =~ /$delete_pattern/;
unless ($handle) {
$error = "bad input line: $_" unless $handle;
last;
my ($name, $file, $import_sub) =
(shift @insert_list, shift @insert_list, shift @insert_list);
+ my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
$error ||=
- &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+ &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
close $fh;
unlink $file or warn "Can't delete $file: $!";
}
- $error = "No DETAIL supplied"
+ $error ||= "No DETAIL supplied"
unless ($files{detail});
open my $fh, "< $dir/". $files{detail}
or $error ||= "Can't open DETAIL file: $!";
my ($name, $file, $import_sub) =
(shift @delete_list, shift @delete_list, shift @delete_list);
+ my $fmt = $format. ( $name eq 'ZIP' ? '-zip' : '' );
open my $fh, "< $file" or $error ||= "Can't open $name file $file: $!";
$error ||=
- &{$import_sub}({ 'filehandle' => $fh, 'format' => $format }, $job);
+ &{$import_sub}({ 'filehandle' => $fh, 'format' => $fmt }, $job);
close $fh;
unlink $file or warn "Can't delete $file: $!";
}