per-agent disable_previous_balance, #15863
[freeside.git] / FS / FS / cust_bill.pm
index d9a5e42..35ce48c 100644 (file)
@@ -43,6 +43,7 @@ use FS::bill_batch;
 use FS::cust_bill_batch;
 use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
+use FS::discount_plan;
 use FS::L10N;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
@@ -143,6 +144,8 @@ Specific use cases
 
 =item agent_invid - legacy invoice number
 
+=item promised_date - customer promised payment date, for collection
+
 =back
 
 =head1 METHODS
@@ -748,6 +751,18 @@ sub cust_bill_batch {
   qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
 }
 
+=item discount_plans
+
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
+hash keyed by term length.
+
+=cut
+
+sub discount_plans {
+  my $self = shift;
+  FS::discount_plan->all($self);
+}
+
 =item tax
 
 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
@@ -796,6 +811,23 @@ sub owed_pkgnum {
   $balance;
 }
 
+=item hide
+
+Returns true if this invoice should be hidden.  See the
+selfservice-hide_invoices-taxclass configuraiton setting.
+
+=cut
+
+sub hide {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
+    or return '';
+  my @cust_bill_pkg = $self->cust_bill_pkg;
+  my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
+  ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
+}
+
 =item apply_payments_and_credits [ OPTION => VALUE ... ]
 
 Applies unapplied payments and credits to this invoice.
@@ -1028,41 +1060,54 @@ sub generate_email {
       'Disposition' => 'inline',
     );
 
-    $args{'from'} =~ /\@([\w\.\-]+)/;
-    my $from = $1 || 'example.com';
-    my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
 
-    my $logo;
-    my $agentnum = $cust_main->agentnum;
-    if ( defined($args{'template'}) && length($args{'template'})
-         && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
-       )
-    {
-      $logo = 'logo_'. $args{'template'}. '.png';
+    my $htmldata;
+    my $image = '';
+    my $barcode = '';
+    if ( $conf->exists('invoice_email_pdf')
+         and scalar($conf->config('invoice_email_pdf_note')) ) {
+
+      $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
+
     } else {
-      $logo = "logo.png";
-    }
-    my $image_data = $conf->config_binary( $logo, $agentnum);
-
-    my $image = build MIME::Entity
-      'Type'       => 'image/png',
-      'Encoding'   => 'base64',
-      'Data'       => $image_data,
-      'Filename'   => 'logo.png',
-      'Content-ID' => "<$content_id>",
-    ;
+
+      $args{'from'} =~ /\@([\w\.\-]+)/;
+      my $from = $1 || 'example.com';
+      my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
+
+      my $logo;
+      my $agentnum = $cust_main->agentnum;
+      if ( defined($args{'template'}) && length($args{'template'})
+           && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
+         )
+      {
+        $logo = 'logo_'. $args{'template'}. '.png';
+      } else {
+        $logo = "logo.png";
+      }
+      my $image_data = $conf->config_binary( $logo, $agentnum);
+
+      $image = build MIME::Entity
+        'Type'       => 'image/png',
+        'Encoding'   => 'base64',
+        'Data'       => $image_data,
+        'Filename'   => 'logo.png',
+        'Content-ID' => "<$content_id>",
+      ;
    
-    my $barcode;
-    if($conf->exists('invoice-barcode')){
-       my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-       $barcode = build MIME::Entity
-         'Type'       => 'image/png',
-         'Encoding'   => 'base64',
-         'Data'       => $self->invoice_barcode(0),
-         'Filename'   => 'barcode.png',
-         'Content-ID' => "<$barcode_content_id>",
-       ;
-       $opt{'barcode_cid'} = $barcode_content_id;
+      if ($conf->exists('invoice-barcode')) {
+        my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
+        $barcode = build MIME::Entity
+          'Type'       => 'image/png',
+          'Encoding'   => 'base64',
+          'Data'       => $self->invoice_barcode(0),
+          'Filename'   => 'barcode.png',
+          'Content-ID' => "<$barcode_content_id>",
+        ;
+        $opt{'barcode_cid'} = $barcode_content_id;
+      }
+
+      $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
     }
 
     $alternative->attach(
@@ -1075,7 +1120,7 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $self->print_html({ 'cid'=>$content_id, %opt }),
+                         $htmldata,
                          '  </body>',
                          '</html>',
                        ],
@@ -1083,6 +1128,7 @@ sub generate_email {
       #'Filename'    => 'invoice.pdf',
     );
 
+
     my @otherparts = ();
     if ( $cust_main->email_csv_cdr ) {
 
@@ -1121,7 +1167,7 @@ sub generate_email {
 
       $related->add_part($alternative);
 
-      $related->add_part($image);
+      $related->add_part($image) if $image;
 
       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
 
@@ -1137,11 +1183,10 @@ sub generate_email {
       #   image/png
 
       $return{'content-type'} = 'multipart/related';
-      if($conf->exists('invoice-barcode')){
-         $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
-      }
-      else {
-         $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
+      if ($conf->exists('invoice-barcode') && $barcode) {
+        $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
+      } else {
+        $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
       }
       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
       #$return{'disposition'} = 'inline';
@@ -1335,6 +1380,7 @@ sub queueable_email {
 #sub email_invoice {
 sub email {
   my $self = shift;
+  return if $self->hide;
   my $conf = $self->conf;
 
   my( $template, $invoice_from, $notice_name, $no_coupon );
@@ -1453,7 +1499,9 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 #sub print_invoice {
 sub print {
   my $self = shift;
+  return if $self->hide;
   my $conf = $self->conf;
+
   my( $template, $notice_name );
   if ( ref($_[0]) ) {
     my $opt = shift;
@@ -1493,7 +1541,9 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 
 sub fax_invoice {
   my $self = shift;
+  return if $self->hide;
   my $conf = $self->conf;
+
   my( $template, $notice_name );
   if ( ref($_[0]) ) {
     my $opt = shift;
@@ -2943,7 +2993,7 @@ sub print_generic {
     }
   }
 
-  unless (    $conf->exists('disable_previous_balance')
+  unless (    $conf->exists('disable_previous_balance', $agentnum)
            || $conf->exists('previous_balance-summary_only')
          )
   {
@@ -2977,7 +3027,8 @@ sub print_generic {
 
   }
   
-  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
+  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) 
+    {
     push @buf, ['','-----------'];
     push @buf, [ $self->mt('Total Previous Balance'),
                  $money_char. sprintf("%10.2f", $pr_total) ];
@@ -3093,7 +3144,7 @@ sub print_generic {
   $invoice_data{current_less_finance} =
     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
 
-  if ( $multisection && !$conf->exists('disable_previous_balance')
+  if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
     || $conf->exists('previous_balance-summary_only') )
   {
     unshift @sections, $previous_section if $pr_total;
@@ -3157,7 +3208,7 @@ sub print_generic {
 
   push @buf,['','-----------'];
   push @buf,[$self->mt( 
-              $conf->exists('disable_previous_balance') 
+              $conf->exists('disable_previous_balance', $agentnum
                ? 'Total Charges'
                : 'Total New Charges'
              ),
@@ -3171,7 +3222,7 @@ sub print_generic {
          || 'Total New Charges'
       if $conf->exists('previous_balance-exclude_from_total');
     my $amount = $self->charged +
-                   ( $conf->exists('disable_previous_balance') ||
+                   ( $conf->exists('disable_previous_balance', $agentnum) ||
                      $conf->exists('previous_balance-exclude_from_total')
                      ? 0
                      : $pr_total
@@ -3198,7 +3249,7 @@ sub print_generic {
     push @buf,['',''];
   }
   
-  unless ( $conf->exists('disable_previous_balance') ) {
+  unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
   
     # credits
@@ -4936,11 +4987,20 @@ sub _items_cust_bill_pkg {
           my $description = ($is_summary && $type && $type eq 'U')
                             ? "Usage charges" : $desc;
 
-          $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
-                          " - ". time2str($date_format, $cust_bill_pkg->edate).
-                          ")"
-            unless $conf->exists('disable_line_item_date_ranges')
-                || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
+          unless (
+            $conf->exists('disable_line_item_date_ranges')
+              || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
+          ) {
+            my $time_period;
+            my $date_style = $conf->config('cust_bill-line_item-date_style');
+            if ( $date_style eq 'month_of' ) {
+              $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
+            } else {
+              $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
+                           " - ". time2str($date_format, $cust_bill_pkg->edate);
+            }
+            $description .= " ($time_period)";
+          }
 
           my @d = ();
           my @seconds = (); # for display of usage info
@@ -5183,108 +5243,34 @@ a setup fee if the discount is allowed to apply to setup fees.
 
 sub _items_discounts_avail {
   my $self = shift;
-  my %terms;
   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
-  my ($previous_balance) = $self->previous;
-
-  foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
-    $terms{$_->months} = {
-      pkgnums       => [],
-      base          => $previous_balance || 0, # pre-discount sum of charges
-      discounted    => $previous_balance || 0, # post-discount sum
-      list_pkgnums  => 0, # whether any packages are not discounted
-    }
-  }
-  foreach my $months (keys %terms) {
-    my $hash = $terms{$months};
 
-    # tricky, because packages may not all be eligible for the same discounts
-    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-      my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
-      my $part_pkg = $cust_pkg->part_pkg or next;
-      my $freq = $part_pkg->freq;
-      my $setup = $cust_bill_pkg->setup || 0;
-      my $recur = $cust_bill_pkg->recur || 0;
-
-      if ( $freq eq '1' ) { #monthly
-        my $permonth = $part_pkg->base_recur_permonth || 0;
-
-        my ($discount) = grep { $_->months == $months } 
-                         map { $_->discount } $part_pkg->part_pkg_discount;
+  my %plans = $self->discount_plans;
 
-        $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
+  $list_pkgnums = grep { $_->list_pkgnums } values %plans;
 
-        if ( $discount ) {
+  map {
+    my $months = $_;
+    my $plan = $plans{$months};
 
-          my $discountable;
-          if ( $discount->setup ) {
-            $discountable += $setup;
-          }
-          else {
-            $hash->{discounted} += $setup;
-          }
-
-          if ( $discount->percent ) {
-            $discountable += $months * $permonth;
-            $discountable -= ($discountable * $discount->percent / 100);
-            $discountable -= ($permonth - $recur); # correct for prorate
-            $hash->{discounted} += $discountable;
-          }
-          else {
-            $discountable += $recur;
-            $discountable -= $discount->amount * $recur/$permonth;
-
-            $discountable += ($months - 1) * max($permonth - $discount->amount,0);
-          }
-
-          $hash->{discounted} += $discountable;
-          push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
-        }
-        else { #no discount
-          $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
-          $hash->{list_pkgnums} = 1;
-        }
-      } #if $freq eq '1'
-      else { # all non-monthly packages: include current charges only
-        $hash->{discounted} += $setup + $recur;
-        $hash->{base} += $setup + $recur;
-        $hash->{list_pkgnums} = 1;
-      }
-    } #foreach $cust_bill_pkg
-
-    # don't show this line if no packages have discounts at this term
-    # or if there are no new charges to apply the discount to
-    delete $terms{$months} if $hash->{base} == $hash->{discounted}
-                           or $hash->{base} == 0;
-
-  }
-
-  $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
-
-  foreach my $months (keys %terms) {
-    my $hash = $terms{$months};
-    my $term_total = sprintf('%.2f', $hash->{discounted});
-    # possibly shouldn't include previous balance in these?
-    my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
+    my $term_total = sprintf('%.2f', $plan->discounted_total);
+    my $percent = sprintf('%.0f', 
+                          100 * (1 - $term_total / $plan->base_total) );
     my $permonth = sprintf('%.2f', $term_total / $months);
-
-    $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
-      $percent, $months
-    );
-    $hash->{amount} = $self->mt('[_1] ([_2] per month)', 
-      $term_total, $money_char.$permonth
-    );
-
-    my @detail;
-    if ( $list_pkgnums ) {
-      push @detail, $self->mt('discount on item'). ' '.
-                join(', ', map { "#$_" } @{ $hash->{pkgnums} });
+    my $detail = $self->mt('discount on item'). ' '.
+                 join(', ', map { "#$_" } $plan->pkgnums)
+      if $list_pkgnums;
+
+    +{
+      description => $self->mt('Save [_1]% by paying for [_2] months',
+                                $percent, $months),
+      amount      => $self->mt('[_1] ([_2] per month)', 
+                                $term_total, $money_char.$permonth),
+      ext_description => ($detail || ''),
     }
-    $hash->{ext_description} = join ', ', @detail;
-  }
+  } #map
+  sort { $b <=> $a } keys %plans;
 
-  map { $terms{$_} } sort {$b <=> $a} keys %terms;
 }
 
 =item call_details [ OPTION => VALUE ... ]
@@ -5648,6 +5634,15 @@ sub search_sql_where {
 
   }
 
+  #promised_date - also has an option to accept nulls
+  if ( $param->{promised_date} ) {
+    my($beginning, $ending, $null) = @{$param->{promised_date}};
+
+    push @search, "(( cust_bill.promised_date >= $beginning AND ".
+                    "cust_bill.promised_date <  $ending )" .
+                    ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
+  }
+
   #agent virtualization
   my $curuser = $FS::CurrentUser::CurrentUser;
   if ( $curuser->username eq 'fs_queue'