correct display of prepay discount info with non-monthly packages, #15040
[freeside.git] / FS / FS / cust_main / Billing.pm
index 5647ef0..eb5bd0b 100644 (file)
@@ -7,6 +7,7 @@ use Data::Dumper;
 use List::Util qw( min );
 use FS::UID qw( dbh );
 use FS::Record qw( qsearch qsearchs dbdef );
+use FS::Misc::DateTime qw( day_end );
 use FS::cust_bill;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
@@ -20,7 +21,6 @@ use FS::cust_bill_pkg_tax_rate_location;
 use FS::part_event;
 use FS::part_event_condition;
 use FS::pkg_category;
-use POSIX;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -113,7 +113,7 @@ sub bill_and_collect {
   my $job = $options{'job'};
 
   $job->update_statustext('0,cleaning expired packages') if $job;
-  $error = $self->cancel_expired_pkgs( $self->day_end( $options{actual_time} ) );
+  $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) );
   if ( $error ) {
     $error = "Error expiring custnum ". $self->custnum. ": $error";
     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
@@ -121,7 +121,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
-  $error = $self->suspend_adjourned_pkgs( $self->day_end( $options{actual_time} ) );
+  $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) );
   if ( $error ) {
     $error = "Error adjourning custnum ". $self->custnum. ": $error";
     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
@@ -165,19 +165,6 @@ sub bill_and_collect {
 
 }
 
-sub day_end {
-    # XXX: sometimes "incorrect" if crossing DST boundaries?
-
-    my $self = shift;
-    my $time = shift;
-
-    return $time unless $conf->exists('next-bill-ignore-time');
-
-    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
-        localtime($time);
-    mktime(59,59,23,$mday,$mon,$year,$wday,$yday,$isdst);
-}
-
 sub cancel_expired_pkgs {
   my ( $self, $time, %options ) = @_;
   
@@ -266,7 +253,8 @@ charges, etc.
 =item freq_override
 
 If set, then override the normal frequency and look for a part_pkg_discount
-to take at that frequency.
+to take at that frequency.  This will exclude any packages that aren't billed
+on a monthly cycle.
 
 =item time
 
@@ -416,18 +404,27 @@ sub bill {
 
       my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
 
-      my $error =
-        $self->_make_lines( 'part_pkg'            => $part_pkg,
-                            'cust_pkg'            => $cust_pkg,
-                            'precommit_hooks'     => \@precommit_hooks,
-                            'line_items'          => $cust_bill_pkg{$pass},
-                            'setup'               => $total_setup{$pass},
-                            'recur'               => $total_recur{$pass},
-                            'tax_matrix'          => $taxlisthash{$pass},
-                            'time'                => $time,
-                            'real_pkgpart'        => $real_pkgpart,
-                            'options'             => \%options,
-                          );
+      my $next_bill = $cust_pkg->getfield('bill') || 0;
+      my $error;
+      while ( $next_bill <= $time ) {
+        $error =
+          $self->_make_lines( 'part_pkg'            => $part_pkg,
+                              'cust_pkg'            => $cust_pkg,
+                              'precommit_hooks'     => \@precommit_hooks,
+                              'line_items'          => $cust_bill_pkg{$pass},
+                              'setup'               => $total_setup{$pass},
+                              'recur'               => $total_recur{$pass},
+                              'tax_matrix'          => $taxlisthash{$pass},
+                              'time'                => $time,
+                              'real_pkgpart'        => $real_pkgpart,
+                              'options'             => \%options,
+                            );
+        # Stop if anything goes wrong, or if we're not incrementing 
+        # the bill date.
+        last if $error;
+        last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
+        $next_bill = $cust_pkg->getfield('bill') || 0;
+      }
       if ($error) {
         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
         return $error;
@@ -594,27 +591,52 @@ sub bill {
 
 #discard bundled packages of 0 value
 sub _omit_zero_value_bundles {
+  my @in = @_;
 
   my @cust_bill_pkg = ();
   my @cust_bill_pkg_bundle = ();
   my $sum = 0;
   my $discount_show_always = 0;
 
-  foreach my $cust_bill_pkg ( @_ ) {
+  foreach my $cust_bill_pkg ( @in ) {
+
     $discount_show_always = ($cust_bill_pkg->get('discounts')
                                && scalar(@{$cust_bill_pkg->get('discounts')})
                                && $conf->exists('discount-show-always'));
+
+    warn "  pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
+         "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
+         "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
+      if $DEBUG > 0;
+
     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
       push @cust_bill_pkg, @cust_bill_pkg_bundle 
-                       if ($sum > 0 || ($sum == 0 && $discount_show_always));
+        if $sum > 0
+        || ($sum == 0 && (    $discount_show_always
+                           || grep {$_->recur_show_zero || $_->setup_show_zero}
+                                   @cust_bill_pkg_bundle
+                         )
+           );
       @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 || ($sum == 0 && $discount_show_always));
+    if $sum > 0
+    || ($sum == 0 && (    $discount_show_always
+                       || grep {$_->recur_show_zero || $_->setup_show_zero}
+                               @cust_bill_pkg_bundle
+                     )
+       );
+
+  warn "  _omit_zero_value_bundles: ". scalar(@in).
+       '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
+    if $DEBUG > 2;
 
   (@cust_bill_pkg);
 
@@ -825,13 +847,18 @@ sub _make_lines {
   my $time = $params{'time'} or die "no time specified";
   my (%options) = %{$params{options}};
 
+  if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
+    # this should never happen
+    die 'freq_override billing attempted on non-monthly package '.
+      $cust_pkg->pkgnum;
+  }
+
   my $dbh = dbh;
   my $real_pkgpart = $params{real_pkgpart};
   my %hash = $cust_pkg->hash;
   my $old_cust_pkg = new FS::cust_pkg \%hash;
 
   my @details = ();
-  my @discounts = ();
   my $lineitems = 0;
 
   $cust_pkg->pkgpart($part_pkg->pkgpart);
@@ -842,12 +869,14 @@ sub _make_lines {
 
   my $setup = 0;
   my $unitsetup = 0;
+  my @setup_discounts = ();
+  my %setup_param = ( 'discounts' => \@setup_discounts );
   if (     ! $options{recurring_only}
        and ! $options{cancel}
        and ( $options{'resetup'}
              || ( ! $cust_pkg->setup
                   && ( ! $cust_pkg->start_date
-                       || $cust_pkg->start_date <= $self->day_end($time)
+                       || $cust_pkg->start_date <= day_end($time)
                      )
                   && ( ! $conf->exists('disable_setup_suspended_pkgs')
                        || ( $conf->exists('disable_setup_suspended_pkgs') &&
@@ -864,7 +893,7 @@ sub _make_lines {
     unless ( $cust_pkg->waive_setup ) {
         $lineitems++;
 
-        $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
+        $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
         return "$@ running calc_setup for $cust_pkg\n"
           if $@;
 
@@ -888,11 +917,12 @@ sub _make_lines {
   #XXX unit stuff here too
   my $recur = 0;
   my $unitrecur = 0;
+  my @recur_discounts = ();
   my $sdate;
   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 ) <= $self->day_end($time) )
+            ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
          || ( $part_pkg->plan eq 'voip_cdr'
                && $part_pkg->option('bill_every_call')
             )
@@ -916,12 +946,13 @@ sub _make_lines {
 
     #over two params!  lets at least switch to a hashref for the rest...
     my $increment_next_bill = ( $part_pkg->freq ne '0'
-                                && ( $cust_pkg->getfield('bill') || 0 ) <= $self->day_end($time)
+                                && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time)
                                 && !$options{cancel}
                               );
-    my %param = ( 'precommit_hooks'     => $precommit_hooks,
+    my %param = ( %setup_param,
+                  'precommit_hooks'     => $precommit_hooks,
                   'increment_next_bill' => $increment_next_bill,
-                  'discounts'           => \@discounts,
+                  'discounts'           => \@recur_discounts,
                   'real_pkgpart'        => $real_pkgpart,
                   'freq_override'      => $options{freq_override} || '',
                   'setup_fee'           => 0,
@@ -967,6 +998,12 @@ sub _make_lines {
       $lineitems++;
     }
 
+    if ( defined $param{'discount_left_setup'} ) {
+        foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
+            $setup -= $discount_setup;
+        }
+    }
+
   }
 
   warn "\$setup is undefined" unless defined($setup);
@@ -1004,13 +1041,18 @@ 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'));
+    my $discount_show_always = $conf->exists('discount-show-always')
+                               && (    ($setup == 0 && scalar(@setup_discounts))
+                                    || ($recur == 0 && scalar(@recur_discounts))
+                                  );
 
-    if ( $setup != 0 ||
-         $recur != 0 ||
-         (!$part_pkg->hidden && $options{has_hidden}) || #include some $0 lines
-        $discount_show_always ) 
+    if (    $setup != 0
+         || $recur != 0
+         || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
+         || $discount_show_always
+         || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
+         || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
+       ) 
     {
 
       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
@@ -1031,7 +1073,7 @@ sub _make_lines {
         'unitrecur' => $unitrecur,
         'quantity'  => $cust_pkg->quantity,
         'details'   => \@details,
-        'discounts' => \@discounts,
+        'discounts' => [ @setup_discounts, @recur_discounts ],
         'hidden'    => $part_pkg->hidden,
         'freq'      => $part_pkg->freq,
       };
@@ -1176,46 +1218,12 @@ sub _handle_taxes {
     } #if $conf->exists('enable_taxproducts') ...
 
   }
-  my @display = ();
-  my $separate = $conf->exists('separate_usage');
-  my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
-  my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
-  my $section = $temp_pkg->part_pkg->categoryname;
-  if ( $separate || $section || $usage_mandate ) {
-
-    my %hash = ( 'section' => $section );
-
-    $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
-    my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
-    if ( $separate ) {
-      push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
-      push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
-    } else {
-      push @display, new FS::cust_bill_pkg_display
-                       { type => '',
-                         %hash,
-                         ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
-                       };
-    }
 
-    if ($separate && $section && $summary) {
-      push @display, new FS::cust_bill_pkg_display { type    => 'U',
-                                                     summary => 'Y',
-                                                     %hash,
-                                                   };
-    }
-    if ($usage_mandate || $section && $summary) {
-      $hash{post_total} = 'Y';
-    }
-
-    if ($separate || $usage_mandate) {
-      $hash{section} = $section if ($separate || $usage_mandate);
-      push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
-    }
-
-  }
-  $cust_bill_pkg->set('display', \@display);
+  #what's this doing in the middle of _handle_taxes?  probably should split
+  #this into three parts above in _make_lines
+  $cust_bill_pkg->set_display(   part_pkg     => $part_pkg,
+                                 real_pkgpart => $real_pkgpart,
+                             );
 
   my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
   foreach my $key (keys %tax_cust_bill_pkg) {
@@ -1624,7 +1632,7 @@ sub do_cust_event {
         if $DEBUG > 1;
 
       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
-      if ( my $error = $cust_event->do_event() ) {
+      if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
         #XXX wtf is this?  figure out a proper dealio with return value
         #from do_event
         return $error;
@@ -2108,11 +2116,14 @@ sub apply_payments {
 
     my $amount = min( $payment->unapplied, $owed );
 
-    my $cust_bill_pay = new FS::cust_bill_pay ( {
+    my $cbp = {
       'paynum' => $payment->paynum,
       'invnum' => $cust_bill->invnum,
       'amount' => $amount,
-    } );
+    };
+    $cbp->{_date} = $payment->_date 
+        if $options{'manual'} && $options{'backdate_application'};
+    my $cust_bill_pay = new FS::cust_bill_pay($cbp);
     $cust_bill_pay->pkgnum( $payment->pkgnum )
       if $conf->exists('pkg-balances') && $payment->pkgnum;
     my $error = $cust_bill_pay->insert(%options);