when bill_usage_on_cancel config is set, fix billing setup on cancellation of unbille...
[freeside.git] / FS / FS / cust_main / Billing.pm
index 4fd8643..44181eb 100644 (file)
@@ -12,7 +12,6 @@ use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
 use FS::cust_bill_pay;
 use FS::cust_credit_bill;
-use FS::cust_pkg;
 use FS::cust_tax_adjustment;
 use FS::tax_rate;
 use FS::tax_rate_location;
@@ -20,6 +19,7 @@ use FS::cust_bill_pkg_tax_location;
 use FS::cust_bill_pkg_tax_rate_location;
 use FS::part_event;
 use FS::part_event_condition;
+use FS::pkg_category;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -547,7 +547,7 @@ sub bill {
     #create the new invoice
     my $cust_bill = new FS::cust_bill ( {
       'custnum'             => $self->custnum,
-      '_date'               => ( $invoice_time ),
+      '_date'               => $invoice_time,
       'charged'             => $charged,
       'billing_balance'     => $balance,
       'previous_balance'    => $previous_balance,
@@ -584,17 +584,23 @@ sub _omit_zero_value_bundles {
   my @cust_bill_pkg = ();
   my @cust_bill_pkg_bundle = ();
   my $sum = 0;
+  my $discount_show_always = 0;
 
   foreach my $cust_bill_pkg ( @_ ) {
+    $discount_show_always = ($cust_bill_pkg->get('discounts')
+                               && scalar(@{$cust_bill_pkg->get('discounts')})
+                               && $conf->exists('discount-show-always'));
     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
-      push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+      push @cust_bill_pkg, @cust_bill_pkg_bundle 
+                       if ($sum > 0 || ($sum == 0 && $discount_show_always));
       @cust_bill_pkg_bundle = ();
       $sum = 0;
     }
     $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
     push @cust_bill_pkg_bundle, $cust_bill_pkg;
   }
-  push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+  push @cust_bill_pkg, @cust_bill_pkg_bundle
+                       if ($sum > 0 || ($sum == 0 && $discount_show_always));
 
   (@cust_bill_pkg);
 
@@ -720,13 +726,6 @@ sub calculate_taxes {
   foreach my $tax ( keys %$taxlisthash ) {
     foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
       next unless ref($_) eq 'FS::cust_bill_pkg';
-
-      unless ( $packagemap{$_->pkgnum} ) {
-        warn "WARNING: can't move cust_tax_exempt_pkg records for pkgnum".
-             $_->pkgnum. " - not in our line item list";
-        next;
-      }
-
       push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg }, 
         splice( @{ $_->_cust_tax_exempt_pkg } );
     }
@@ -821,19 +820,21 @@ sub _make_lines {
 
   my $setup = 0;
   my $unitsetup = 0;
-  if ( $options{'resetup'}
-       || ( ! $cust_pkg->setup
-            && ( ! $cust_pkg->start_date
-                 || $cust_pkg->start_date <= $time
-               )
-            && ( ! $conf->exists('disable_setup_suspended_pkgs')
-                 || ( $conf->exists('disable_setup_suspended_pkgs') &&
-                      ! $cust_pkg->getfield('susp')
-                    )
-               )
-          )
-        and !$options{recurring_only}
-    )
+  if (     ! $options{recurring_only}
+       and ! $options{cancel}
+       and ( $options{'resetup'}
+             || ( ! $cust_pkg->setup
+                  && ( ! $cust_pkg->start_date
+                       || $cust_pkg->start_date <= $time
+                     )
+                  && ( ! $conf->exists('disable_setup_suspended_pkgs')
+                       || ( $conf->exists('disable_setup_suspended_pkgs') &&
+                            ! $cust_pkg->getfield('susp')
+                          )
+                     )
+                )
+           )
+     )
   {
     
     warn "    bill setup\n" if $DEBUG > 1;
@@ -863,15 +864,14 @@ sub _make_lines {
   my $recur = 0;
   my $unitrecur = 0;
   my $sdate;
-  if (     ! $cust_pkg->get('susp')
-       and ! $cust_pkg->get('start_date')
-       and ( $part_pkg->getfield('freq') ne '0'
-             && ( $cust_pkg->getfield('bill') || 0 ) <= $time
-           )
-        || ( $part_pkg->plan eq 'voip_cdr'
-              && $part_pkg->option('bill_every_call')
-           )
-        || ( $options{cancel} )
+  if (     ! $cust_pkg->start_date
+       and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) )
+       and
+            ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $time )
+         || ( $part_pkg->plan eq 'voip_cdr'
+               && $part_pkg->option('bill_every_call')
+            )
+         || $options{cancel}
   ) {
 
     # XXX should this be a package event?  probably.  events are called
@@ -899,12 +899,14 @@ sub _make_lines {
                   'discounts'           => \@discounts,
                   'real_pkgpart'        => $real_pkgpart,
                   'freq_override'      => $options{freq_override} || '',
+                  'setup_fee'           => 0,
                 );
 
     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
 
     # There may be some part_pkg for which this is wrong.  Only those
     # which can_discount are supported.
+    # (the UI should prevent adding discounts to these at the moment)
 
     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
     return "$@ running $method for $cust_pkg\n"
@@ -927,6 +929,14 @@ sub _make_lines {
 
     }
 
+    if ( $param{'setup_fee'} ) {
+      # Add an additional setup fee at the billing stage.
+      # Used for prorate_defer_bill.
+      $setup += $param{'setup_fee'};
+      $unitsetup += $param{'setup_fee'};
+      $lineitems++;
+    }
+
   }
 
   warn "\$setup is undefined" unless defined($setup);
@@ -938,7 +948,7 @@ sub _make_lines {
   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
   ###
 
-  if ( $lineitems || $options{has_hidden} ) {
+  if ( $lineitems ) {
 
     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
       # hmm.. and if just the options are modified in some weird price plan?
@@ -947,6 +957,7 @@ sub _make_lines {
         if $DEBUG >1;
   
       my $error = $cust_pkg->replace( $old_cust_pkg,
+                                      'depend_jobnum'=>$options{depend_jobnum},
                                       'options' => { $cust_pkg->options },
                                     )
         unless $options{no_commit};
@@ -963,9 +974,13 @@ sub _make_lines {
       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
     }
 
+    my $discount_show_always = ($recur == 0 && scalar(@discounts) 
+                               && $conf->exists('discount-show-always'));
+
     if ( $setup != 0 ||
          $recur != 0 ||
-         !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
+         (!$part_pkg->hidden && $options{has_hidden}) || #include some $0 lines
+        $discount_show_always ) 
     {
 
       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
@@ -1011,9 +1026,11 @@ sub _make_lines {
       # handle taxes
       ###
 
-      my $error = 
-        $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
-      return $error if $error;
+      unless ( $discount_show_always ) {
+         my $error = 
+           $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
+         return $error if $error;
+      }
 
       push @$cust_bill_pkgs, $cust_bill_pkg;
 
@@ -1055,18 +1072,14 @@ sub _handle_taxes {
        )
     {
 
-      if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
-        return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
-      }
-
       foreach my $class (@classes) {
-        my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
+        my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
         return $err_or_ref unless ref($err_or_ref);
         $taxes{$class} = $err_or_ref;
       }
 
       unless (exists $taxes{''}) {
-        my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
+        my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
         return $err_or_ref unless ref($err_or_ref);
         $taxes{''} = $err_or_ref;
       }
@@ -1233,11 +1246,18 @@ sub _gather_taxes {
   my $self = shift;
   my $part_pkg = shift;
   my $class = shift;
+  my $cust_pkg = shift;
 
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
 
+  my $geocode;
+  if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
+    $geocode = $cust_pkg->cust_location->geocode('cch');
+  } else {
+    $geocode = $self->geocode('cch');
+  }
+
   my @taxes = ();
-  my $geocode = $self->geocode('cch');
 
   my @taxclassnums = map { $_->taxclassnum }
                      $part_pkg->part_pkg_taxoverride($class);
@@ -1631,174 +1651,6 @@ Explicitly pass the objects to be tested (typically used with eventtable).
 Set to true to return the objects, but not actually insert them into the
 database.
 
-=item discount_terms
-
-Returns a list of lengths for term discounts
-
-=cut
-
-sub _discount_pkgs_and_bill {
-my $self = shift;
-
-  my @cust_bill = $self->cust_bill;
-  my $cust_bill = pop @cust_bill;
-  return () unless $cust_bill && $cust_bill->owed;
-
-  my @where = ();
-  push @where, "cust_bill_pkg.invnum = ". $cust_bill->invnum;
-  push @where, "cust_bill_pkg.pkgpart_override IS NULL";
-  push @where, "part_pkg.freq = '1'";
-  push @where, "(cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0)";
-  push @where, "(cust_pkg.susp   IS NULL OR cust_pkg.susp   = 0)";
-  push @where, "0<(SELECT count(*) FROM part_pkg_discount
-                  WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart)";
-  push @where,
-    "0=(SELECT count(*) FROM cust_bill_pkg_discount
-         WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum)";
-
-  my $extra_sql = 'WHERE '. join(' AND ', @where);
-
-  my @cust_pkg = 
-    qsearch({
-      'table' => 'cust_pkg',
-      'select' => "DISTINCT cust_pkg.*",
-      'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '.
-                     'JOIN part_pkg USING(pkgpart)',
-      'hashref' => {},
-      'extra_sql' => $extra_sql,
-    }); 
-
-  ($cust_bill, @cust_pkg);
-}
-
-sub _discountable_pkgs_at_term {
-  my ($term, @pkgs) = @_;
-  my $part_pkg = new FS::part_pkg { freq => $term - 1 };
-  grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) && 
-         ( !$_->expire  || $_->expire  > $part_pkg->add_freq($_->bill) )
-       }
-    @pkgs;
-}
-
-=item discount_terms
-
-Returns a list of lengths for term discounts
-
-=cut
-
-sub discount_terms {
-my $self = shift;
-
-  my %terms = ();
-
-  my @discount_pkgs = $self->_discount_pkgs_and_bill;
-  shift @discount_pkgs; #discard bill;
-  
-  map { $terms{$_->months} = 1 }
-    grep { $_->months && $_->months > 1 }
-    map { $_->discount }
-    map { $_->part_pkg->part_pkg_discount }
-    @discount_pkgs;
-
-  return sort { $a <=> $b } keys %terms;
-
-}
-
-=back
-
-=item discount_term_values MONTHS
-
-Returns a list with credit, dollar amount saved, and total bill acheived
-by prepaying the most recent invoice for MONTHS.
-
-=cut
-
-sub discount_term_values {
-  my $self = shift;
-  my $term = shift;
-
-  local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
-
-  warn "$me discount_term_values called with $term\n" if $DEBUG;
-
-  my %result = ();
-
-  my @packages = $self->_discount_pkgs_and_bill;
-  my $cust_bill = shift(@packages);
-  @packages = _discountable_pkgs_at_term( $term, @packages );
-  return () unless scalar(@packages);
-
-  $_->bill($_->last_bill) foreach @packages;
-  my @final = map { new FS::cust_pkg { $_->hash } } @packages;
-
-  my %options = (
-                  'recurring_only' => 1,
-                  'no_usage_reset' => 1,
-                  'no_commit'      => 1,
-                );
-
-  my %params =  (
-                  'return_bill'    => [],
-                  'pkg_list'       => \@packages,
-                  'time'           => $cust_bill->_date,
-                );
-
-  my $error = $self->bill(%options, %params);
-  die $error if $error; # XXX think about this a bit more
-
-  my $credit = 0;
-  $credit += $_->charged foreach @{$params{return_bill}};
-  $credit = sprintf('%.2f', $credit);
-  warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
-
-  %params =  (
-               'return_bill'    => [],
-               'pkg_list'       => \@packages,
-               'time'           => $packages[0]->part_pkg->add_freq($cust_bill->_date)
-             );
-
-  $error = $self->bill(%options, %params);
-  die $error if $error; # XXX think about this a bit more
-
-  my $next = 0;
-  $next += $_->charged foreach @{$params{return_bill}};
-  warn "$me discount_term_values $term next: $next\n" if $DEBUG;
-  
-  %params =  ( 
-               'return_bill'    => [],
-               'pkg_list'       => \@final,
-               'time'           => $cust_bill->_date,
-               'freq_override'  => $term,
-             );
-
-  $error = $self->bill(%options, %params);
-  die $error if $error; # XXX think about this a bit more
-
-  my $final = $self->balance - $credit;
-  $final += $_->charged foreach @{$params{return_bill}};
-  $final = sprintf('%.2f', $final);
-  warn "$me discount_term_values $term final: $final\n" if $DEBUG;
-
-  my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
-
-  ( $credit, $savings, $final );
-
-}
-
-sub discount_terms_hash {
-  my $self = shift;
-
-  my %result = ();
-  my @terms = $self->discount_terms;
-  foreach my $term (@terms) {
-    my @result = $self->discount_term_values($term);
-    $result{$term} = [ @result ] if scalar(@result);
-  }
-
-  return %result;
-
-}
-
 =back
 
 =cut
@@ -2250,6 +2102,28 @@ sub apply_payments {
   return $total_unapplied_payments;
 }
 
+=back
+
+=head1 FLOW
+
+  bill_and_collect
+
+    cancel_expired_pkgs
+    suspend_adjourned_pkgs
+
+    bill
+      (do_cust_event pre-bill)
+      _make_lines
+        _handle_taxes
+          (vendor-only) _gather_taxes
+      _omit_zero_value_bundles
+      calculate_taxes
+
+    apply_payments_and_credits
+    collect
+      do_cust_event
+        due_cust_event
+
 =head1 BUGS
 
 =head1 SEE ALSO