-sub taxline {
- # FS::tax_rate::taxline() ridiculously returns a description and amount
- # instead of a real line item. Fix that here.
- #
- # XXX eventually move the code from tax_rate to here
- # but that's not necessary yet
- my ($self, %opt) = @_;
- my $tax_object = $opt{tax};
- my $taxables = $opt{sales};
- my $hashref = $tax_object->taxline_cch($taxables);
- return $hashref unless ref $hashref; # it's an error message
-
- my $tax_amount = sprintf('%.2f', $hashref->{amount});
- my $tax_item = FS::cust_bill_pkg->new({
- 'itemdesc' => $hashref->{name},
- 'pkgnum' => 0,
- 'recur' => 0,
- 'sdate' => '',
- 'edate' => '',
- 'setup' => $tax_amount,
- });
- my $tax_link = FS::cust_bill_pkg_tax_rate_location->new({
- 'taxnum' => $tax_object->taxnum,
- 'taxtype' => ref($tax_object), #redundant
- 'amount' => $tax_amount,
- 'locationtaxid' => $tax_object->location,
- 'taxratelocationnum' =>
- $tax_object->tax_rate_location->taxratelocationnum,
- 'tax_cust_bill_pkg' => $tax_item,
- # XXX still need to get taxable_cust_bill_pkg in here
- # but that requires messing around in the taxline code
- });
- $tax_item->set('cust_bill_pkg_tax_rate_location', [ $tax_link ]);
-
- return $tax_item;
+# differs from stock make_taxlines because we need another pass to do
+# tax on tax
+sub make_taxlines {
+ my $self = shift;
+ my $cust_bill = shift;
+
+ my @raw_taxlines;
+ my %taxable_location; # taxable billpkgnum => cust_location
+ my %item_has_tax; # taxable billpkgnum => taxnum
+ foreach my $taxnum ( keys %{ $self->{taxes} } ) {
+ my $tax_rate = FS::tax_rate->by_key($taxnum);
+ my $taxables = $self->{taxes}{$taxnum};
+ my $charge_classes = $self->{taxclass}{$taxnum};
+ foreach (@$taxables) {
+ $taxable_location{ $_->billpkgnum } ||= $_->tax_location;
+ }
+
+ my @taxlines = $tax_rate->taxline_cch( $taxables, $charge_classes );
+
+ next if !@taxlines;
+ if (!ref $taxlines[0]) {
+ # it's an error string
+ warn "error evaluating tax#$taxnum\n";
+ return $taxlines[0];
+ }
+
+ my $billpkgnum = -1; # the current one
+ my $fragments; # $item_has_tax{$billpkgnum}{taxnum}
+
+ foreach my $taxline (@taxlines) {
+ next if $taxline->setup == 0;
+
+ my $link = $taxline->get('cust_bill_pkg_tax_rate_location')->[0];
+ # store this tax fragment, indexed by taxable item, then by taxnum
+ if ( $billpkgnum != $link->taxable_billpkgnum ) {
+ $billpkgnum = $link->taxable_billpkgnum;
+ $item_has_tax{$billpkgnum} ||= {};
+ $fragments = $item_has_tax{$billpkgnum}{$taxnum} ||= [];
+ }
+
+ $taxline->set('invnum', $cust_bill->invnum);
+ push @$fragments, $taxline; # so we can ToT it
+ push @raw_taxlines, $taxline; # so we actually bill it
+ }
+ } # foreach $taxnum
+
+ # all first-tier taxes are calculated. now for tax on tax
+ # (has to be done on a per-taxable-item basis)
+ foreach my $billpkgnum (keys %item_has_tax) {
+ # taxes that apply to this item
+ my $this_has_tax = $item_has_tax{$billpkgnum};
+ my $location = $taxable_location{$billpkgnum};
+ foreach my $taxnum (keys %$this_has_tax) {
+ my $tax_rate = FS::tax_rate->by_key($taxnum);
+ # find all taxes that apply to it in this location
+ my @tot = $tax_rate->tax_on_tax( $location );
+ next if !@tot;
+
+ warn "found possible taxed taxnum $taxnum\n"
+ if $DEBUG > 2;
+ # Calculate ToT separately for each taxable item, and only if _that
+ # item_ is already taxed under the ToT. This is counterintuitive.
+ # See RT#5243.
+ foreach my $tot (@tot) {
+ my $totnum = $tot->taxnum;
+ warn "checking taxnum ".$tot->taxnum.
+ " which we call ". $tot->taxname ."\n"
+ if $DEBUG > 2;
+ if ( exists $this_has_tax->{ $totnum } ) {
+ warn "calculating tax on tax: taxnum ".$tot->taxnum." on $taxnum\n"
+ if $DEBUG;
+ my @taxlines = $tot->taxline_cch(
+ $this_has_tax->{ $taxnum }, # the first-stage tax (in an arrayref)
+ );
+ next if (!@taxlines); # it didn't apply after all
+ if (!ref($taxlines[0])) {
+ warn "error evaluating TOT ($totnum on $taxnum)\n";
+ return $taxlines[0];
+ }
+ # add these to the taxline queue
+ push @raw_taxlines, @taxlines;
+ } # if $this_has_tax->{$totnum}
+ } # foreach my $tot (tax-on-tax rate definition)
+ } # foreach $taxnum (first-tier rate definition)
+ } # foreach $taxable_item
+
+ return @raw_taxlines;