fix tax calculation on bundled packages, fallout from #25899
[freeside.git] / FS / FS / cust_bill_pkg.pm
index 826569b..ef9c01a 100644 (file)
@@ -8,10 +8,10 @@ use List::Util qw( sum min );
 use Text::CSV_XS;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
-use FS::cust_bill;
 use FS::cust_bill_pkg_detail;
 use FS::cust_bill_pkg_display;
 use FS::cust_bill_pkg_discount;
+use FS::cust_bill_pkg_fee;
 use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
 use FS::cust_tax_exempt_pkg;
@@ -47,8 +47,8 @@ FS::cust_bill_pkg - Object methods for cust_bill_pkg records
 =head1 DESCRIPTION
 
 An FS::cust_bill_pkg object represents an invoice line item.
-FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
-supported:
+FS::cust_bill_pkg inherits from FS::Record.  The following fields are
+currently supported:
 
 =over 4
 
@@ -201,16 +201,49 @@ sub insert {
 
   my $tax_location = $self->get('cust_bill_pkg_tax_location');
   if ( $tax_location ) {
-    foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
-      $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
-      $error = $cust_bill_pkg_tax_location->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error inserting cust_bill_pkg_tax_location: $error";
+    foreach my $link ( @$tax_location ) {
+      next if $link->billpkgtaxlocationnum; # don't try to double-insert
+      # This cust_bill_pkg can be linked on either side (i.e. it can be the
+      # tax or the taxed item).  If the other side is already inserted, 
+      # then set billpkgnum to ours, and insert the link.  Otherwise,
+      # set billpkgnum to ours and pass the link off to the cust_bill_pkg
+      # on the other side, to be inserted later.
+
+      my $tax_cust_bill_pkg = $link->get('tax_cust_bill_pkg');
+      if ( $tax_cust_bill_pkg && $tax_cust_bill_pkg->billpkgnum ) {
+        $link->set('billpkgnum', $tax_cust_bill_pkg->billpkgnum);
+        # break circular links when doing this
+        $link->set('tax_cust_bill_pkg', '');
       }
-    }
+      my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
+      if ( $taxable_cust_bill_pkg && $taxable_cust_bill_pkg->billpkgnum ) {
+        $link->set('taxable_billpkgnum', $taxable_cust_bill_pkg->billpkgnum);
+        # XXX if we ever do tax-on-tax for these, this will have to change
+        # since pkgnum will be zero
+        $link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
+        $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
+        $link->set('taxable_cust_bill_pkg', '');
+      }
+
+      if ( $link->billpkgnum and $link->taxable_billpkgnum ) {
+        $error = $link->insert;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "error inserting cust_bill_pkg_tax_location: $error";
+        }
+      } else { # handoff
+        my $other;
+        $other = $link->billpkgnum ? $link->get('taxable_cust_bill_pkg')
+                                   : $link->get('tax_cust_bill_pkg');
+        my $link_array = $other->get('cust_bill_pkg_tax_location') || [];
+        push @$link_array, $link;
+        $other->set('cust_bill_pkg_tax_location' => $link_array);
+      }
+    } #foreach my $link
   }
 
+  # someday you will be as awesome as cust_bill_pkg_tax_location...
+  # but not today
   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
   if ( $tax_rate_location ) {
     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
@@ -223,6 +256,52 @@ sub insert {
     }
   }
 
+  my $fee_links = $self->get('cust_bill_pkg_fee');
+  if ( $fee_links ) {
+    foreach my $link ( @$fee_links ) {
+      # very similar to cust_bill_pkg_tax_location, for obvious reasons
+      next if $link->billpkgfeenum; # don't try to double-insert
+
+      my $target = $link->get('cust_bill_pkg'); # the line item of the fee
+      my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
+
+      if ( $target and $target->billpkgnum ) {
+        $link->set('billpkgnum', $target->billpkgnum);
+        # base_invnum => null indicates that the fee is based on its own
+        # invoice
+        $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
+        $link->set('cust_bill_pkg', '');
+      }
+
+      if ( $base and $base->billpkgnum ) {
+        $link->set('base_billpkgnum', $base->billpkgnum);
+        $link->set('base_cust_bill_pkg', '');
+      } elsif ( $base ) {
+        # it's based on a line item that's not yet inserted
+        my $link_array = $base->get('cust_bill_pkg_fee') || [];
+        push @$link_array, $link;
+        $base->set('cust_bill_pkg_fee' => $link_array);
+        next; # don't insert the link yet
+      }
+
+      $error = $link->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_fee: $error";
+      }
+    } # foreach my $link
+  }
+
+  my $cust_event_fee = $self->get('cust_event_fee');
+  if ( $cust_event_fee ) {
+    $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
+    $error = $cust_event_fee->replace;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error updating cust_event_fee: $error";
+    }
+  }
+
   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
   if ( $cust_tax_adjustment ) {
     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
@@ -400,7 +479,13 @@ sub check {
       || $self->ut_snumber('pkgnum')
       || $self->ut_number('invnum')
       || $self->ut_money('setup')
+      || $self->ut_moneyn('unitsetup')
+      || $self->ut_currencyn('setup_billed_currency')
+      || $self->ut_moneyn('setup_billed_amount')
       || $self->ut_money('recur')
+      || $self->ut_moneyn('unitrecur')
+      || $self->ut_currencyn('recur_billed_currency')
+      || $self->ut_moneyn('recur_billed_amount')
       || $self->ut_numbern('sdate')
       || $self->ut_numbern('edate')
       || $self->ut_textn('itemdesc')
@@ -471,11 +556,19 @@ sub regularize_details {
 
 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
 
+=item cust_main
+
+Returns the customer (L<FS::cust_main> object) for this line item.
+
 =cut
 
-sub cust_bill {
+sub cust_main {
+  # required for cust_main_Mixin equivalence
+  # and use cust_bill instead of cust_pkg because this might not have a 
+  # cust_pkg
   my $self = shift;
-  qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+  my $cust_bill = $self->cust_bill or return '';
+  $cust_bill->cust_main;
 }
 
 =item previous_cust_bill_pkg
@@ -665,8 +758,9 @@ sub set_display {
 
 =item disintegrate
 
-Returns a list of cust_bill_pkg objects each with no more than a single class
-(including setup or recur) of charge.
+Returns a hash: keys are "setup", "recur" or usage classnum, values are
+FS::cust_bill_pkg objects, each with no more than a single class (setup or
+recur) of charge.
 
 =cut
 
@@ -843,6 +937,62 @@ sub _X_show_zero {
   $self->cust_pkg->_X_show_zero($what);
 }
 
+=item credited [ BEFORE, AFTER, OPTIONS ]
+
+Returns the sum of credits applied to this item.  Arguments are the same as
+owed_sql/paid_sql/credited_sql.
+
+=cut
+
+sub credited {
+  my $self = shift;
+  $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
+}
+
+=item tax_locationnum
+
+Returns the L<FS::cust_location> number that this line item is in for tax
+purposes.  For package sales, it's the package tax location; for fees, 
+it's the customer's default service location.
+
+=cut
+
+sub tax_locationnum {
+  my $self = shift;
+  if ( $self->pkgnum ) { # normal sales
+    return $self->cust_pkg->tax_locationnum;
+  } elsif ( $self->feepart ) { # fees
+    return $self->cust_bill->cust_main->ship_locationnum;
+  } else { # taxes
+    return '';
+  }
+}
+
+sub tax_location {
+  my $self = shift;
+  FS::cust_location->by_key($self->tax_locationnum);
+}
+
+=item part_X
+
+Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
+charge.  If called on a tax line, returns nothing.
+
+=cut
+
+sub part_X {
+  my $self = shift;
+  if ( $self->pkgpart_override ) {
+    return FS::part_pkg->by_key($self->pkgpart_override);
+  } elsif ( $self->pkgnum ) {
+    return $self->cust_pkg->part_pkg;
+  } elsif ( $self->feepart ) {
+    return $self->part_fee;
+  } else {
+    return;
+  }
+}
+
 =back
 
 =head1 CLASS METHODS
@@ -866,9 +1016,10 @@ sub usage_sql { $usage_sql }
 # this makes owed_sql, etc. much more concise
 sub charged_sql {
   my ($class, $start, $end, %opt) = @_;
+  my $setuprecur = $opt{setuprecur} || '';
   my $charged = 
-    $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' :
-    $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' :
+    $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
+    $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
     'cust_bill_pkg.setup + cust_bill_pkg.recur';
 
   if ($opt{no_usage} and $charged =~ /recur/) { 
@@ -902,16 +1053,16 @@ Returns an SQL expression for the sum of payments applied to this item.
 
 sub paid_sql {
   my ($class, $start, $end, %opt) = @_;
-  my $s = $start ? "AND cust_bill_pay._date <= $start" : '';
-  my $e = $end   ? "AND cust_bill_pay._date >  $end"   : '';
-  my $setuprecur = 
-    $opt{setuprecur} =~ /^s/ ? 'setup' :
-    $opt{setuprecur} =~ /^r/ ? 'recur' :
-    '';
+  my $s = $start ? "AND cust_pay._date <= $start" : '';
+  my $e = $end   ? "AND cust_pay._date >  $end"   : '';
+  my $setuprecur = $opt{setuprecur} || '';
+  $setuprecur = 'setup' if $setuprecur =~ /^s/;
+  $setuprecur = 'recur' if $setuprecur =~ /^r/;
   $setuprecur &&= "AND setuprecur = '$setuprecur'";
 
   my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
      FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
+                            JOIN cust_pay      USING (paynum)
      WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
            $s $e $setuprecur )";
 
@@ -930,16 +1081,16 @@ sub paid_sql {
 
 sub credited_sql {
   my ($class, $start, $end, %opt) = @_;
-  my $s = $start ? "AND cust_credit_bill._date <= $start" : '';
-  my $e = $end   ? "AND cust_credit_bill._date >  $end"   : '';
-  my $setuprecur = 
-    $opt{setuprecur} =~ /^s/ ? 'setup' :
-    $opt{setuprecur} =~ /^r/ ? 'recur' :
-    '';
+  my $s = $start ? "AND cust_credit._date <= $start" : '';
+  my $e = $end   ? "AND cust_credit._date >  $end"   : '';
+  my $setuprecur = $opt{setuprecur} || '';
+  $setuprecur = 'setup' if $setuprecur =~ /^s/;
+  $setuprecur = 'recur' if $setuprecur =~ /^r/;
   $setuprecur &&= "AND setuprecur = '$setuprecur'";
 
   my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
      FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
+                               JOIN cust_credit      USING (crednum)
      WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
            $s $e $setuprecur )";
 
@@ -1057,16 +1208,12 @@ sub upgrade_tax_location {
     delete @hash{qw(censustract censusyear latitude longitude coord_auto)};
 
     $hash{custnum} = $h_cust_main->custnum;
-    my $tax_loc = qsearchs('cust_location', \%hash) # unlikely
-                  || FS::cust_location->new({ %hash });
-    if ( !$tax_loc->locationnum ) {
-      $tax_loc->disabled('Y');
-      my $error = $tax_loc->insert;
-      if ( $error ) {
-        warn "couldn't create historical location record for cust#".
-        $h_cust_main->custnum.": $error\n";
-        next INVOICE;
-      }
+    my $tax_loc = FS::cust_location->new(\%hash);
+    my $error = $tax_loc->find_or_insert || $tax_loc->disable_if_unused;
+    if ( $error ) {
+      warn "couldn't create historical location record for cust#".
+      $h_cust_main->custnum.": $error\n";
+      next INVOICE;
     }
     my $exempt_cust = 1 if $h_cust_main->tax;
 
@@ -1297,9 +1444,10 @@ sub upgrade_tax_location {
                        );
           $cents_remaining -= $part;
           push @tax_links, {
-            taxnum => $taxdef->taxnum,
-            pkgnum => $nontax->pkgnum,
-            cents  => $part,
+            taxnum      => $taxdef->taxnum,
+            pkgnum      => $nontax->pkgnum,
+            billpkgnum  => $nontax->billpkgnum,
+            cents       => $part,
           };
         } #foreach $nontax
       } #foreach $taxclass
@@ -1342,6 +1490,7 @@ sub upgrade_tax_location {
             taxnum      => $_->{taxnum},
             pkgnum      => $_->{pkgnum},
             amount      => sprintf('%.2f', $_->{cents} / 100),
+            taxable_billpkgnum => $_->{billpkgnum},
         });
         my $error = $link->insert;
         if ( $error ) {
@@ -1430,6 +1579,9 @@ sub _upgrade_data {
   # Then mark the upgrade as done, so that we don't queue the job twice
   # and somehow run two of them concurrently.
   FS::upgrade_journal->set_done($upgrade);
+  # This upgrade now does the job of assigning taxable_billpkgnums to 
+  # cust_bill_pkg_tax_location, so set that task done also.
+  FS::upgrade_journal->set_done('tax_location_taxable_billpkgnum');
 }
 
 =back