+=item calculate_tax_adjustment PARAMS
+
+Calculate the amount of tax that needs to be credited as part of a lineitem
+credit.
+
+PARAMS must include:
+
+- billpkgnums: arrayref identifying the line items to credit
+- setuprecurs: arrayref of 'setup' or 'recur', indicating which part of
+ the lineitem charge is being credited
+- amounts: arrayref of the amounts to credit on each line item
+- custnum: the customer all of these invoices belong to, for error checking
+
+Returns a hash containing:
+- subtotal: the total non-tax amount to be credited (the sum of the 'amounts')
+- taxtotal: the total tax amount to be credited
+- taxlines: an arrayref of hashrefs for each tax line to be credited, each with:
+ - table: "cust_bill_pkg_tax_location" or "cust_bill_pkg_tax_rate_location"
+ - num: the key within that table
+ - credit: the credit amount to apply to that line
+
+=cut
+
+sub calculate_tax_adjustment {
+ my ($class, %arg) = @_;
+
+ my $error;
+ my @taxlines;
+ my $subtotal = 0;
+ my $taxtotal = 0;
+
+ my (%cust_bill_pkg, %cust_bill);
+
+ for (my $i = 0; ; $i++) {
+ my $billpkgnum = $arg{billpkgnums}[$i]
+ or last;
+ my $setuprecur = $arg{setuprecurs}[$i];
+ my $amount = $arg{amounts}[$i];
+ next if $amount == 0;
+ $subtotal += $amount;
+ my $cust_bill_pkg = $cust_bill_pkg{$billpkgnum}
+ ||= FS::cust_bill_pkg->by_key($billpkgnum)
+ or die "lineitem #$billpkgnum not found\n";
+
+ my $invnum = $cust_bill_pkg->invnum;
+ $cust_bill{ $invnum } ||= FS::cust_bill->by_key($invnum);
+ $cust_bill{ $invnum}->custnum == $arg{custnum}
+ or die "lineitem #$billpkgnum not found\n";
+
+ # tax_Xlocation records don't distinguish setup and recur, so calculate
+ # the fraction of setup+recur (after deducting credits) that's setup. This
+ # will also be the fraction of tax (after deducting credits) that's tax on
+ # setup.
+ my ($setup, $recur);
+ $setup = $cust_bill_pkg->get('setup') || 0;
+ if ($setup) {
+ $setup -= $cust_bill_pkg->credited('', '', setuprecur => 'setup') || 0;
+ }
+ $recur = $cust_bill_pkg->get('recur') || 0;
+ if ($recur) {
+ $recur -= $cust_bill_pkg->credited('', '', setuprecur => 'recur') || 0;
+ }
+ my $setup_ratio = $setup / ($setup + $recur);
+
+ # Calculate the fraction of tax to credit: it's the fraction of this charge
+ # (either setup or recur) that's being credited.
+ my $charged = ($setuprecur eq 'setup') ? $setup : $recur;
+ next if $charged == 0; # shouldn't happen, but still...
+
+ if ($charged < $amount) {
+ $error = "invoice #$invnum: tried to credit $amount, but only $charged was charged";
+ last;
+ }
+ my $credit_ratio = $amount / $charged;
+
+ # gather taxes that apply to the selected item
+ foreach my $table (
+ qw(cust_bill_pkg_tax_location cust_bill_pkg_tax_rate_location)
+ ) {
+ foreach my $tax_link (
+ qsearch($table, { taxable_billpkgnum => $billpkgnum })
+ ) {
+ my $tax_amount = $tax_link->amount;
+ # deduct existing credits applied to the tax, for the same reason as
+ # above
+ foreach ($tax_link->cust_credit_bill_pkg) {
+ $tax_amount -= $_->amount;
+ }
+ # split tax amount based on setuprecur
+ # (this method ensures that, if you credit both setup and recur tax,
+ # it always equals the entire tax despite any rounding)
+ my $setup_tax = sprintf('%.2f', $tax_amount * $setup_ratio);
+ if ( $setuprecur eq 'setup' ) {
+ $tax_amount = $setup_tax;
+ } else {
+ $tax_amount = $tax_amount - $setup_tax;
+ }
+ my $tax_credit = sprintf('%.2f', $tax_amount * $credit_ratio);
+ my $pkey = $tax_link->get($tax_link->primary_key);
+ push @taxlines, {
+ table => $table,
+ num => $pkey,
+ credit => $tax_credit,
+ };
+ $taxtotal += $tax_credit;
+
+ } #foreach cust_bill_pkg_tax_(rate_)?location
+ }
+ } # foreach $billpkgnum
+
+ return (
+ subtotal => sprintf('%.2f', $subtotal),
+ taxtotal => sprintf('%.2f', $taxtotal),
+ taxlines => \@taxlines,
+ );
+}
+