X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling.pm;h=df7e17f81f5340eb8d0c45a88dd2c3d9916e45e1;hb=4b695753d2456060e6a16808120cbb488a19c584;hp=9bfab96ef622ec979cbdb90feed0129bc4ce3518;hpb=92b6628c08e4478e48b6f250320a3e3e93262ec2;p=freeside.git diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 9bfab96ef..df7e17f81 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -8,6 +8,7 @@ use List::Util qw( min ); use FS::UID qw( dbh ); use FS::Record qw( qsearch qsearchs dbdef ); use FS::Misc::DateTime qw( day_end ); +use Tie::RefHash; use FS::cust_bill; use FS::cust_bill_pkg; use FS::cust_bill_pkg_display; @@ -378,6 +379,12 @@ Do not save the generated bill in the database. Useful with return_bill A list reference on which the generated bill(s) will be returned. +=item estimate + +Boolean value; indicates that this is an estimate rather than a "tax invoice". +This will be passed through to the tax engine, as online tax services +sometimes need to know it for reporting purposes. Otherwise it has no effect. + =item invoice_terms Optional terms to be printed on this invoice. Otherwise, customer-specific @@ -473,7 +480,8 @@ sub bill { foreach (@passes) { $tax_engines{$_} = FS::TaxEngine->new(cust_main => $self, invoice_time => $invoice_time, - cancel => $options{cancel} + cancel => $options{cancel}, + estimate => $options{estimate}, ); $tax_is_batch ||= $tax_engines{$_}->info->{batch}; } @@ -541,7 +549,8 @@ sub bill { $tax_engines{$pass} = FS::TaxEngine->new( cust_main => $self, invoice_time => $invoice_time, - cancel => $options{cancel} + cancel => $options{cancel}, + estimate => $options{estimate}, ); $cust_bill_pkg{$pass} = []; } @@ -809,16 +818,17 @@ sub bill { # calculate and append taxes if ( ! $tax_is_batch) { - my $arrayref_or_error = $tax_engines{$pass}->calculate_taxes($cust_bill); + local $@; + my $arrayref = eval { $tax_engines{$pass}->calculate_taxes($cust_bill) }; - unless ( ref( $arrayref_or_error ) ) { + if ( $@ ) { $dbh->rollback if $oldAutoCommit && !$options{no_commit}; - return $arrayref_or_error; + return $@; } # or should this be in TaxEngine? my $total_tax = 0; - foreach my $taxline ( @$arrayref_or_error ) { + foreach my $taxline ( @$arrayref ) { $total_tax += $taxline->setup; $taxline->set('invnum' => $cust_bill->invnum); # just to be sure push @cust_bill_pkg, $taxline; # for return_bill @@ -1043,6 +1053,7 @@ sub _make_lines { ) ) ) + || $cust_pkg->is_status_delay_cancel ) and ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time ) @@ -1096,6 +1107,14 @@ sub _make_lines { return "$@ running $method for $cust_pkg\n" if ( $@ ); + if ($recur eq 'NOTHING') { + # then calc_cancel (or calc_recur but that's not used) has declined to + # generate a recurring lineitem at all. treat this as zero, but also + # try not to generate a lineitem. + $recur = 0; + $lineitems--; + } + #base_cancel??? $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better @@ -1389,6 +1408,11 @@ If not supplied, part_item will be inferred from the pkgnum or feepart of the cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and the customer's default service location). +This method will also calculate exemptions for any taxes that apply to the +line item (using the C method of L) and +attach them. This is the only place C is called in normal +invoice processing. + =cut sub _handle_taxes { @@ -1406,7 +1430,7 @@ sub _handle_taxes { return if ( $self->payby eq 'COMP' ); #dubious - if ( $conf->exists('enable_taxproducts') + if ( $conf->config('enable_taxproducts') && ( scalar($part_item->part_pkg_taxoverride) || $part_item->has_taxproduct ) @@ -1418,85 +1442,73 @@ sub _handle_taxes { my %taxes = (); my @classes; - push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage; + my $usage = $cust_bill_pkg->usage || 0; + push @classes, $cust_bill_pkg->usage_classes if $usage; push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel}; - push @classes, 'recur' if $cust_bill_pkg->recur and !$options{cancel}; - - my $exempt = $conf->exists('cust_class-tax_exempt') - ? ( $self->cust_class ? $self->cust_class->tax : '' ) - : $self->tax; + push @classes, 'recur' if ($cust_bill_pkg->recur - $usage) + and !$options{cancel}; + # that's better--probably don't even need $options{cancel} now + # but leave it for now, just to be safe + # + # About $options{cancel}: This protects against charging per-line or + # per-customer or other flat-rate surcharges on a package that's being + # billed on cancellation (which is an out-of-cycle bill and should only + # have usage charges). See RT#29443. + + # customer exemption is now handled in the 'taxline' method + #my $exempt = $conf->exists('cust_class-tax_exempt') + # ? ( $self->cust_class ? $self->cust_class->tax : '' ) + # : $self->tax; # standardize this just to be sure - $exempt = ($exempt eq 'Y') ? 'Y' : ''; - - if ( !$exempt ) { + #$exempt = ($exempt eq 'Y') ? 'Y' : ''; + # + #if ( !$exempt ) { + + unless (exists $taxes{''}) { + # unsure what purpose this serves, but last time I deleted something + # from here just because I didn't see the point, it actually did + # something important. + my $err_or_ref = $self->_gather_taxes($part_item, '', $location); + return $err_or_ref unless ref($err_or_ref); + $taxes{''} = $err_or_ref; + } - foreach my $class (@classes) { - my $err_or_ref = $self->_gather_taxes($part_item, $class, $location); - return $err_or_ref unless ref($err_or_ref); - $taxes{$class} = $err_or_ref; - } + # NO DISINTEGRATIONS. + # my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; + # + # do not call taxline() with any argument except the entire set of + # cust_bill_pkgs on an invoice that are eligible for the tax. - unless (exists $taxes{''}) { - my $err_or_ref = $self->_gather_taxes($part_item, '', $location); - return $err_or_ref unless ref($err_or_ref); - $taxes{''} = $err_or_ref; - } + # only calculate exemptions once for each tax rate, even if it's used + # for multiple classes + my %tax_seen = (); + + foreach my $class (@classes) { + my $err_or_ref = $self->_gather_taxes($part_item, $class, $location); + return $err_or_ref unless ref($err_or_ref); + my @taxes = @$err_or_ref; - } + next if !@taxes; - my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr - foreach my $key (keys %tax_cust_bill_pkg) { - # $key is "setup", "recur", or a usage class name. ('' is a usage class.) - # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of - # the line item. - # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that - # apply to $key-class charges. - my @taxes = @{ $taxes{$key} || [] }; - my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key}; - - my %localtaxlisthash = (); foreach my $tax ( @taxes ) { - # this is the tax identifier, not the taxname - my $taxname = ref( $tax ). ' '. $tax->taxnum; - # $taxlisthash: keys are "setup", "recur", and usage classes. + my $tax_id = ref( $tax ). ' '. $tax->taxnum; + # $taxlisthash: keys are tax identifiers ('FS::tax_rate 123456'). # Values are arrayrefs, first the tax object (cust_main_county - # or tax_rate) and then any cust_bill_pkg objects that the - # tax applies to. - $taxlisthash->{ $taxname } ||= [ $tax ]; - push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg; - - $localtaxlisthash{ $taxname } ||= [ $tax ]; - push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg; - - } + # or tax_rate), then the cust_bill_pkg object that the + # tax applies to, then the tax class (setup, recur, usage classnum). + $taxlisthash->{ $tax_id } ||= [ $tax ]; + push @{ $taxlisthash->{ $tax_id } }, $cust_bill_pkg, $class; + + # determine any exemptions that apply + if (!$tax_seen{$tax_id}) { + $cust_bill_pkg->set_exemptions( $tax, custnum => $self->custnum ); + $tax_seen{$tax_id} = 1; + } - warn "finding taxed taxes...\n" if $DEBUG > 2; - foreach my $tax ( keys %localtaxlisthash ) { - my $tax_object = shift @{ $localtaxlisthash{$tax} }; - warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n" - if $DEBUG > 2; - next unless $tax_object->can('tax_on_tax'); - - foreach my $tot ( $tax_object->tax_on_tax( $location ) ) { - my $totname = ref( $tot ). ' '. $tot->taxnum; - - warn "checking $totname which we call ". $tot->taxname. " as applicable\n" - if $DEBUG > 2; - next unless exists( $localtaxlisthash{ $totname } ); # only increase - # existing taxes - warn "adding $totname to taxed taxes\n" if $DEBUG > 2; - # calculate the tax amount that the tax_on_tax will apply to - my $hashref_or_error = - $tax_object->taxline( $localtaxlisthash{$tax} ); - return $hashref_or_error - unless ref($hashref_or_error); - - # and append it to the list of taxable items - $taxlisthash->{ $totname } ||= [ $tot ]; - push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount}; + # tax on tax will be done later, when we actually create the tax + # line items - } } } @@ -1536,6 +1548,7 @@ sub _handle_taxes { foreach (@taxes) { my $tax_id = 'cust_main_county '.$_->taxnum; $taxlisthash->{$tax_id} ||= [ $_ ]; + $cust_bill_pkg->set_exemptions($_, custnum => $self->custnum); push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg; } @@ -2164,6 +2177,7 @@ sub due_cust_event { =item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits. +Payments with the no_auto_apply flag set will not be applied. In most cases, this new method should be used in place of sequential apply_payments and apply_credits methods. @@ -2306,6 +2320,7 @@ sub apply_credits { Applies (see L) unapplied payments (see L) to outstanding invoice balances in chronological order. +Payments with the no_auto_apply flag set will not be applied. #and returns the value of any remaining unapplied payments. @@ -2335,7 +2350,7 @@ sub apply_payments { #return 0 unless - my @payments = $self->unapplied_cust_pay; + my @payments = grep { !$_->no_auto_apply } $self->unapplied_cust_pay; my @invoices = $self->open_cust_bill;