fix package balance conditions, RT#11834
[freeside.git] / FS / FS / cust_bill.pm
index 84ef089..dd8fab2 100644 (file)
@@ -5,6 +5,7 @@ use vars qw( @ISA $DEBUG $me $conf
              $money_char $date_format $rdate_format $date_format_long );
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
+use Cwd;
 use List::Util qw(min max);
 use Date::Format;
 use Text::Template 1.20;
@@ -38,7 +39,8 @@ use FS::part_bill_event;
 use FS::payby;
 use FS::bill_batch;
 use FS::cust_bill_batch;
-use Cwd;
+use FS::cust_bill_pay_pkg;
+use FS::cust_credit_bill_pkg;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
@@ -649,44 +651,86 @@ sub cust_credit_bill {
   shift->cust_credited(@_);
 }
 
-=item cust_bill_pay_pkgnum PKGNUM
+#=item cust_bill_pay_pkgnum PKGNUM
+#
+#Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_bill_pay_pkgnum {
+#  my( $self, $pkgnum ) = @_;
+#  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+#                                'pkgnum' => $pkgnum,
+#                              }
+#           );
+#}
+
+=item cust_bill_pay_pkg PKGNUM
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
-with matching pkgnum.
+applied against the matching pkgnum.
 
 =cut
 
-sub cust_bill_pay_pkgnum {
+sub cust_bill_pay_pkg {
   my( $self, $pkgnum ) = @_;
-  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
-  sort { $a->_date <=> $b->_date }
-    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
-                                'pkgnum' => $pkgnum,
-                              }
-           );
+
+  qsearch({
+    'select'    => 'cust_bill_pay_pkg.*',
+    'table'     => 'cust_bill_pay_pkg',
+    'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
+                   ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
+    'extra_sql' => ' WHERE invnum = '. $self->invnum.
+                   "   AND pkgnum = $pkgnum",
+  });
+
 }
 
-=item cust_credited_pkgnum PKGNUM
+#=item cust_credited_pkgnum PKGNUM
+#
+#=item cust_credit_bill_pkgnum PKGNUM
+#
+#Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_credited_pkgnum {
+#  my( $self, $pkgnum ) = @_;
+#  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+#                                   'pkgnum' => $pkgnum,
+#                                 }
+#           );
+#}
+#
+#sub cust_credit_bill_pkgnum {
+#  shift->cust_credited_pkgnum(@_);
+#}
 
-=item cust_credit_bill_pkgnum PKGNUM
+=item cust_credit_bill_pkg PKGNUM
 
-Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
-with matching pkgnum.
+Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
+applied against the matching pkgnum.
 
 =cut
 
-sub cust_credited_pkgnum {
+sub cust_credit_bill_pkg {
   my( $self, $pkgnum ) = @_;
-  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
-  sort { $a->_date <=> $b->_date }
-    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
-                                   'pkgnum' => $pkgnum,
-                                 }
-           );
-}
 
-sub cust_credit_bill_pkgnum {
-  shift->cust_credited_pkgnum(@_);
+  qsearch({
+    'select'    => 'cust_credit_bill_pkg.*',
+    'table'     => 'cust_credit_bill_pkg',
+    'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
+                   ' LEFT JOIN cust_bill_pkg    USING ( billpkgnum    ) ',
+    'extra_sql' => ' WHERE invnum = '. $self->invnum.
+                   "   AND pkgnum = $pkgnum",
+  });
+
 }
 
 =item tax
@@ -729,8 +773,8 @@ sub owed_pkgnum {
   my $balance = 0;
   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
 
-  $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
-  $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
+  $balance -= $_->amount            for $self->cust_bill_pay_pkg($pkgnum);
+  $balance -= $_->amount            for $self->cust_credit_bill_pkg($pkgnum);
 
   $balance = sprintf( "%.2f", $balance);
   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
@@ -911,6 +955,7 @@ sub generate_email {
     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
     'template'      => $args{'template'},
     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
+    'no_coupon'     => $args{'no_coupon'},
   );
 
   my $cust_main = $self->cust_main;
@@ -1259,11 +1304,12 @@ sub queueable_email {
   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
     or die "invalid invoice number: " . $opt{invnum};
 
-  my @args = ( $opt{template} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
+  my %args = ( 'template' => $opt{template} );
+  $args{$_} = $opt{$_}
+    foreach grep { exists($opt{$_}) && $opt{$_} }
+              qw( invoice_from notice_name no_coupon );
 
-  my $error = $self->email( @args );
+  my $error = $self->email( \%args );
   die $error if $error;
 
 }
@@ -1272,16 +1318,18 @@ sub queueable_email {
 sub email {
   my $self = shift;
 
-  my( $template, $invoice_from, $notice_name );
+  my( $template, $invoice_from, $notice_name, $no_coupon );
   if ( ref($_[0]) ) {
     my $opt = shift;
     $template = $opt->{'template'} || '';
     $invoice_from = $opt->{'invoice_from'};
     $notice_name = $opt->{'notice_name'} || 'Invoice';
+    $no_coupon = $opt->{'no_coupon'} || 0;
   } else {
     $template = scalar(@_) ? shift : '';
     $invoice_from = shift if scalar(@_);
     $notice_name = 'Invoice';
+    $no_coupon = 0;
   }
 
   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
@@ -1308,6 +1356,7 @@ sub email {
       'subject'     => $subject,
       'template'    => $template,
       'notice_name' => $notice_name,
+      'no_coupon'   => $no_coupon,
     )
   );
   die "can't email invoice: $error\n" if $error;
@@ -2295,7 +2344,7 @@ sub print_generic {
   my $template = $params{template} ? $params{template} : $self->_agent_template;
   my $templatefile = "invoice_$format";
   $templatefile .= "_$template"
-    if length($template);
+    if length($template) && $conf->exists($templatefile."_$template");
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
 
@@ -2433,6 +2482,12 @@ sub print_generic {
                            );
   my $embolden_function = $embolden_functions{$format};
 
+  my %newline_tokens = (  'latex'     => '\\\\',
+                          'html'      => '<br>',
+                          'template'  => "\n",
+                        );
+  my $newline_token = $newline_tokens{$format};
+
   warn "$me generating template variables\n"
     if $DEBUG > 1;
 
@@ -2546,6 +2601,21 @@ sub print_generic {
     'total_pages'     => 1,
 
   );
+  
+  my $min_sdate = 999999999999;
+  my $max_edate = 0;
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+    $min_sdate = $cust_bill_pkg->sdate
+      if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
+    $max_edate = $cust_bill_pkg->edate
+      if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
+  }
+
+  $invoice_data{'bill_period'} = '';
+  $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
+    . " to " . time2str('%e %h', $max_edate)
+    if ($max_edate != 0 && $min_sdate != 999999999999);
 
   $invoice_data{finance_section} = '';
   if ( $conf->config('finance_pkgclass') ) {
@@ -2622,7 +2692,9 @@ sub print_generic {
   warn "$me substituting variables in notes, footer, smallfooter\n"
     if $DEBUG > 1;
 
-  foreach my $include (qw( notes footer smallfooter coupon )) {
+  my @include = (qw( notes footer smallfooter ));
+  push @include, 'coupon' unless $params{'no_coupon'};
+  foreach my $include (@include) {
 
     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
     my @inc_src;
@@ -2700,7 +2772,7 @@ sub print_generic {
                                             sprintf('%.2f', $pr_total),
                            'summarized'  => $summarypage ? 'Y' : '',
                          };
-  $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
+  $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
     join(' / ', map { $cust_main->balance_date_range(@$_) }
                 $self->_prior_month30s
         )
@@ -3102,6 +3174,26 @@ sub print_generic {
       push @buf,[$self->balance_due_msg, $money_char. 
         sprintf("%10.2f", $balance_due ) ];
     }
+
+    if ( $conf->exists('previous_balance-show_credit')
+        and $cust_main->balance < 0 ) {
+      my $credit_total = {
+        'total_item'    => &$embolden_function($self->credit_balance_msg),
+        'total_amount'  => &$embolden_function(
+          $other_money_char. sprintf('%.2f', -$cust_main->balance)
+        ),
+      };
+      if ( $multisection ) {
+        $adjust_section->{'posttotal'} .= $newline_token .
+          $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+      }
+      else {
+        push @total_items, $credit_total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->credit_balance_msg, $money_char. 
+        sprintf("%10.2f", -$cust_main->balance ) ];
+    }
   }
 
   if ( $multisection ) {
@@ -3461,6 +3553,8 @@ sub balance_due_date {
   $duedate;
 }
 
+sub credit_balance_msg { 'Credit Balance Remaining' }
+
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
@@ -4174,6 +4268,85 @@ sub _items_svc_phone_sections {
       push @lines, $l;
     }
   }
+  
+  if($conf->exists('phone_usage_class_summary')) { 
+      # this only works with Latex
+      my @newlines;
+      my @newsections;
+
+      # after this, we'll have only two sections per DID:
+      # Calls Summary and Calls Detail
+      foreach my $section ( @sections ) {
+       if($section->{'post_total'}) {
+           $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+           $section->{'total_line_generator'} = sub { '' };
+           $section->{'total_generator'} = sub { '' };
+           $section->{'header_generator'} = sub { '' };
+           $section->{'description_generator'} = '';
+           push @newsections, $section;
+           my %calls_detail = %$section;
+           $calls_detail{'post_total'} = '';
+           $calls_detail{'sort_weight'} = '';
+           $calls_detail{'description_generator'} = sub { '' };
+           $calls_detail{'header_generator'} = sub {
+               return ' & Date/Time & Called Number & Duration & Price'
+                   if $format eq 'latex';
+               '';
+           };
+           $calls_detail{'description'} = 'Calls Detail: '
+                                                   . $section->{'phonenum'};
+           push @newsections, \%calls_detail;  
+       }
+      }
+
+      # after this, each usage class is collapsed/summarized into a single
+      # line under the Calls Summary section
+      foreach my $newsection ( @newsections ) {
+       if($newsection->{'post_total'}) { # this means Calls Summary
+           foreach my $section ( @sections ) {
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
+                               && !$section->{'post_total'});
+               my $newdesc = $section->{'description'};
+               my $tn = $section->{'phonenum'};
+               $newdesc =~ s/$tn//g;
+               my $line = {  ext_description => [],
+                             pkgnum => '',
+                             ref => '',
+                             quantity => '',
+                             calls => $section->{'calls'},
+                             section => $newsection,
+                             duration => $section->{'duration'},
+                             description => $newdesc,
+                             amount => sprintf("%.2f",$section->{'amount'}),
+                             product_code => 'N/A',
+                           };
+               push @newlines, $line;
+           }
+       }
+      }
+
+      # after this, Calls Details is populated with all CDRs
+      foreach my $newsection ( @newsections ) {
+       if(!$newsection->{'post_total'}) { # this means Calls Details
+           foreach my $line ( @lines ) {
+               next unless (scalar(@{$line->{'ext_description'}}) &&
+                       $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
+                           );
+               my @extdesc = @{$line->{'ext_description'}};
+               my @newextdesc;
+               foreach my $extdesc ( @extdesc ) {
+                   $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
+                   push @newextdesc, $extdesc;
+               }
+               $line->{'ext_description'} = \@newextdesc;
+               $line->{'section'} = $newsection;
+               push @newlines, $line;
+           }
+       }
+      }
+
+      return(\@newsections, \@newlines);
+  }
 
   return(\@sections, \@lines);
 
@@ -4456,11 +4629,17 @@ sub _items_cust_bill_pkg {
 
           }
 
-          warn "$me _items_cust_bill_pkg adding details\n"
-            if $DEBUG > 1;
+          unless ( $is_summary ) {
+            warn "$me _items_cust_bill_pkg adding details\n"
+              if $DEBUG > 1;
 
-          push @d, $cust_bill_pkg->details(%details_opt)
-            unless ($is_summary || $type && $type eq 'R');
+            #instead of omitting details entirely in this case (unwanted side
+            # effects), just omit CDRs
+            $details_opt{'format_function'} = sub { () }
+              if $type && $type eq 'R';
+
+            push @d, $cust_bill_pkg->details(%details_opt);
+          }
 
           warn "$me _items_cust_bill_pkg calculating amount\n"
             if $DEBUG > 1;
@@ -4468,9 +4647,9 @@ sub _items_cust_bill_pkg {
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
-          }elsif($type eq 'R') {
+          } elsif ($type eq 'R') {
             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
-          }elsif($type eq 'U') {
+          } elsif ($type eq 'U') {
             $amount = $cust_bill_pkg->usage;
           }