X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2FTaxEngine.pm;h=45601429a0a39dd6ad53cd3dfd83d8fa62975010;hp=a146c54d1af6ebcf4761d702aac3d862486a8b14;hb=389b6f1116c3309c2ee57a6c295ed1a793503095;hpb=7516e3da0f17eeecba27219ef96a8b5f46af2083 diff --git a/FS/FS/TaxEngine.pm b/FS/FS/TaxEngine.pm index a146c54d1..45601429a 100644 --- a/FS/FS/TaxEngine.pm +++ b/FS/FS/TaxEngine.pm @@ -14,49 +14,67 @@ FS::TaxEngine - Base class for tax calculation engines. =head1 USAGE 1. At the start of creating an invoice, create an FS::TaxEngine object. -2. Each time a sale item is added to the invoice, call C on the +2. Each time a sale item is added to the invoice, call L on the TaxEngine. - -- If the TaxEngine is "batch" style (Billsoft): 3. Set the "pending" flag on the invoice. 4. Insert the invoice and its line items. + +- If the TaxEngine is "batch" style (Billsoft): 5. After creating all invoices for the day, call FS::TaxEngine::process_tax_batch. This will create the tax items for all of the pending invoices, clear the "pending" flag, and call - C on each of the billed customers. + L on each of the billed customers. - If not (the internal tax system, CCH): -3. After adding all sale items, call C on the TaxEngine to +5. After adding all sale items, call L on the TaxEngine to produce a list of tax line items. -4. Append the tax line items to the invoice. -5. Insert the invoice. +6. Append the tax line items to the invoice. +7. Update the invoice with the new charged amount and clear the pending flag. =head1 CLASS METHODS =over 4 +=item class + +Returns the class name for tax engines, according to the 'tax_data_vendor' +configuration setting. + +=cut + +sub class { + my $conf = FS::Conf->new; + my $subclass = $conf->config('tax_data_vendor') || 'internal'; + my $class = "FS::TaxEngine::$subclass"; + local $@; + eval "use $class"; + die "couldn't load $class: $@\n" if $@; + + $class; +} + =item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS... Creates an L object. The subclass will be chosen by the -'enable_taxproducts' configuration setting. +'tax_data_vendor' configuration setting. -CUST_MAIN and TIME are required. OPTIONS can include "cancel" => 1 to -indicate that the package is being billed on cancellation. +CUST_MAIN and TIME are required. OPTIONS can include: + +"cancel" => 1 to indicate that the package is being billed on cancellation. + +"estimate" => 1 to indicate that this calculation is for tax estimation, +and isn't an actual sale invoice, in case that matters. =cut sub new { my $class = shift; my %opt = @_; + my $conf = FS::Conf->new; if ($class eq 'FS::TaxEngine') { - my $conf = FS::Conf->new; - my $subclass = $conf->config('enable_taxproducts') || 'internal'; - $class .= "::$subclass"; - local $@; - eval "use $class"; - die "couldn't load $class: $@\n" if $@; + $class = $class->class; } - my $self = { items => [], taxes => {}, %opt }; + my $self = { items => [], taxes => {}, conf => $conf, %opt }; bless $self, $class; } @@ -84,33 +102,47 @@ Returns a hashref of metadata about this tax method, including: Adds the CUST_BILL_PKG object as a taxable sale on this invoice. -=item calculate_taxes CUST_BILL +=item calculate_taxes INVOICE Calculates the taxes on the taxable sales and returns a list of -L objects to add to the invoice. There is a base -implementation of this, which calls the C method to calculate -each individual tax. +L objects to add to the invoice. The base implementation +is to call L to produce a list of "raw" tax line items, +then L to combine those with the same itemdesc. + +If this fails, it will throw an exception. (Accordingly it should not trap +exceptions from internal methods that it calls, except to translate error +messages into a more meaningful form.) If it succeeds, it MUST return an +arrayref (even if the arrayref is empty). =cut sub calculate_taxes { my $self = shift; - my $conf = FS::Conf->new; - my $cust_bill = shift; - my @tax_line_items; - # keys are tax names (as printed on invoices / itemdesc ) - # values are arrayrefs of taxlines - my %taxname; + my @raw_taxlines = $self->make_taxlines($cust_bill); + if ( !@raw_taxlines ) { + return; + } elsif ( !ref $raw_taxlines[0] ) { # error message + return $raw_taxlines[0]; + } - # keys are taxnums - # values are (cumulative) amounts - my %tax_amount; + my @real_taxlines = $self->consolidate_taxlines(@raw_taxlines); - # keys are taxnums - # values are arrayrefs of cust_tax_exempt_pkg objects - my %tax_exemption; + if ( $cust_bill and $cust_bill->get('invnum') ) { + $_->set('invnum', $cust_bill->get('invnum')) foreach @real_taxlines; + } + return \@real_taxlines; +} + +sub make_taxlines { + # only used by FS::TaxEngine::internal; should just move there + my $self = shift; + my $conf = $self->{conf}; + + my $cust_bill = shift; + + my @raw_taxlines; # For each distinct tax rate definition, calculate the tax and exemptions. foreach my $taxnum ( keys %{ $self->{taxes} } ) { @@ -122,43 +154,86 @@ sub calculate_taxes { # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects # (setup, recurring, usage classes) - my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables); - # taxline methods are now required to return real line items - # with their link records - die $taxline unless ref($taxline); + my @taxlines = $self->taxline('tax' => $tax_object, 'sales' => $taxables); + # taxline methods are now required to return the link records alone. + # Consolidation will take care of the rest. + next if !@taxlines; + die $taxlines[0] unless ref($taxlines[0]); - push @{ $taxname{ $taxline->itemdesc } }, $taxline; + push @raw_taxlines, @taxlines; } #foreach $taxnum - my $link_table = $self->info->{link_table}; - # For each distinct tax name (the values set as $taxline->itemdesc), - # create a consolidated tax item with the total amount and all the links - # of all tax items that share that name. - foreach my $taxname ( keys %taxname ) { - my @tax_links; - my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({ - 'invnum' => $cust_bill->invnum, + return @raw_taxlines; +} + +sub consolidate_taxlines { + + my $self = shift; + my $conf = $self->{conf}; + + my @raw_taxlines = @_; + return if !@raw_taxlines; # shouldn't even be here + + my @tax_line_items; + + # keys are tax names (as printed on invoices / itemdesc ) + # values are arrayrefs of tax links ("raw taxlines") + my %taxname; + # collate these by itemdesc + foreach my $taxline (@raw_taxlines) { + my $taxname = $taxline->taxname; + $taxname{$taxname} ||= []; + push @{ $taxname{$taxname} }, $taxline; + } + + # keys are taxnums + # values are (cumulative) amounts + my %tax_amount; + + my $link_table = $raw_taxlines[0]->table; + + # Preconstruct cust_bill_pkg objects that will become the "final" + # taxlines for each name, so that we can reference them. + # (keys are taxnames) + my %real_taxline_named = map { + $_ => FS::cust_bill_pkg->new({ 'pkgnum' => 0, 'recur' => 0, 'sdate' => '', 'edate' => '', - 'itemdesc' => $taxname, - $link_table => \@tax_links, - }); + 'itemdesc' => $_ + }) + } keys %taxname; + + # For each distinct tax name (the values set as $taxline->itemdesc), + # create a consolidated tax item with the total amount and all the links + # of all tax items that share that name. + foreach my $taxname ( keys %taxname ) { + my $tax_links = $taxname{$taxname}; + my $tax_cust_bill_pkg = $real_taxline_named{$taxname}; + $tax_cust_bill_pkg->set( $link_table => $tax_links ); my $tax_total = 0; warn "adding $taxname\n" if $DEBUG > 1; - foreach my $taxitem ( @{ $taxname{$taxname} } ) { + foreach my $link ( @$tax_links ) { # then we need to transfer the amount and the links from the # line item to the new one we're creating. - $tax_total += $taxitem->setup; - foreach my $link ( @{ $taxitem->get($link_table) } ) { - $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg); - push @tax_links, $link; + $tax_total += $link->amount; + $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg); + + # if the link represents tax on tax, also fix its taxable pointer + # to point to the "final" taxline + my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg'); + if ( $taxable_cust_bill_pkg and + my $other_taxname = $taxable_cust_bill_pkg->itemdesc) { + $link->set('taxable_cust_bill_pkg', + $real_taxline_named{$other_taxname} + ); } - } # foreach $taxitem + + } # foreach $link next unless $tax_total; # we should really neverround this up...I guess it's okay if taxline @@ -185,7 +260,7 @@ sub calculate_taxes { push @tax_line_items, $tax_cust_bill_pkg; } - \@tax_line_items; + @tax_line_items; } =head1 CLASS METHODS