fix agent-specific logos migrated from 1.7, RT#4645
[freeside.git] / FS / FS / cust_bill.pm
index 3aed5a4..769839b 100644 (file)
@@ -17,6 +17,7 @@ use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_main_Mixin;
 use FS::cust_main;
 use FS::cust_bill_pkg;
+use FS::cust_bill_pkg_display;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
@@ -842,7 +843,9 @@ sub send {
   my $invoice_from =
     scalar(@_)
       ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+      : ( $self->_agent_invoice_from ||    #XXX should go away
+          $conf->config('invoice_from', $self->cust_main->agentnum )
+        );
 
   my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
 
@@ -898,7 +901,10 @@ sub email {
   my $invoice_from =
     scalar(@_)
       ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+      : ( $self->_agent_invoice_from ||    #XXX should go away
+          $conf->config('invoice_from', $self->cust_main->agentnum )
+        );
+
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
@@ -906,10 +912,13 @@ sub email {
   #better to notify this person than silence
   @invoicing_list = ($invoice_from) unless @invoicing_list;
 
+  my $subject = $self->email_subject($template);
+
   my $error = send_email(
     $self->generate_email(
       'from'       => $invoice_from,
       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+      'subject'    => $subject,
       'template'   => $template,
     )
   );
@@ -918,6 +927,24 @@ sub email {
 
 }
 
+sub email_subject {
+  my $self = shift;
+
+  #my $template = scalar(@_) ? shift : '';
+  #per-template?
+
+  my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+                || 'Invoice';
+
+  my $cust_main = $self->cust_main;
+  my $name = $cust_main->name;
+  my $name_short = $cust_main->name_short;
+  my $invoice_number = $self->invnum;
+  my $invoice_date = $self->_date_pretty;
+
+  eval qq("$subject");
+}
+
 =item lpr_data [ TEMPLATENAME ]
 
 Returns the postscript or plaintext for this invoice as an arrayref.
@@ -974,6 +1001,46 @@ sub fax_invoice {
 
 }
 
+=item ftp_invoice [ TEMPLATENAME ] 
+
+Sends this invoice data via FTP.
+
+TEMPLATENAME is unused?
+
+=cut
+
+sub ftp_invoice {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  $self->send_csv(
+    'protocol'   => 'ftp',
+    'server'     => $conf->config('cust_bill-ftpserver'),
+    'username'   => $conf->config('cust_bill-ftpusername'),
+    'password'   => $conf->config('cust_bill-ftppassword'),
+    'dir'        => $conf->config('cust_bill-ftpdir'),
+    'format'     => $conf->config('cust_bill-ftpformat'),
+  );
+}
+
+=item spool_invoice [ TEMPLATENAME ] 
+
+Spools this invoice data (see L<FS::spool_csv>)
+
+TEMPLATENAME is unused?
+
+=cut
+
+sub spool_invoice {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  $self->spool_csv(
+    'format'       => $conf->config('cust_bill-spoolformat'),
+    'agent_spools' => $conf->exists('cust_bill-spoolagent'),
+  );
+}
+
 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
 
 Like B<send>, but only sends the invoice if it is the newest open invoice for
@@ -1631,6 +1698,8 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 cid - 
 
+unsquelch_cdr - overrides any per customer cdr squelching when true
+
 =cut
 
 sub print_generic {
@@ -1646,8 +1715,8 @@ sub print_generic {
 
   my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
-
+    unless $cust_main->payname
+        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
 
   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
                      'html'     => [ '<%=', '%>' ],
@@ -1717,6 +1786,7 @@ sub print_generic {
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+}//;
+                       s/\\([&])/$1/g;
                        $_;
                      }  @_
                    },
@@ -1811,7 +1881,7 @@ sub print_generic {
                                                )
                          )
           );
-  } elsif ( grep /\S/, $conf->config('company_address') ) {
+  } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
 
     my $convert_map = $convert_maps{$format}{'returnaddress'};
     $returnaddress = join( "\n", &$convert_map(
@@ -1819,8 +1889,8 @@ sub print_generic {
                                          s/$/\\\\\*/;
                                          $_
                                        }
-                                     ( $conf->config('company_name'),
-                                       $conf->config('company_address'),
+                                     ( $conf->config('company_name', $self->cust_main->agentnum),
+                                       $conf->config('company_address', $self->cust_main->agentnum),
                                      )
                                  )
                      );
@@ -1836,9 +1906,9 @@ sub print_generic {
   }
 
   my %invoice_data = (
-    'company_name'    => scalar( $conf->config('company_name') ),
-    'company_address' => join("\n", $conf->config('company_address') ). "\n",
-    'custnum'         => $self->custnum,
+    'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
+    'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
+    'custnum'         => $cust_main->display_custnum,
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
     'today'           => time2str('%b %o, %Y', $today),
@@ -1851,10 +1921,11 @@ sub print_generic {
     'city'            => &$escape_function($cust_main->city),
     'state'           => &$escape_function($cust_main->state),
     'zip'             => &$escape_function($cust_main->zip),
+    'fax'             => &$escape_function($cust_main->fax),
     'returnaddress'   => $returnaddress,
     #'quantity'        => 1,
     'terms'           => $self->terms,
-    'template'        => $params{'template'},
+    'template'        => $template, #params{'template'},
     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
     # better hang on to conf_dir for a while
     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
@@ -1997,9 +2068,11 @@ sub print_generic {
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
                          'subtotal'    => 0 }; # adjusted below
 
+  my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  my $late_sections = [];
   if ( $multisection ) {
-    push @sections, $self->_items_sections;
+    push @sections, $self->_items_sections( $late_sections );
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
@@ -2038,7 +2111,7 @@ sub print_generic {
     push @buf, ['',''];
   }
 
-  foreach my $section (@sections) {
+  foreach my $section (@sections, @$late_sections) {
 
     $section->{'subtotal'} = $other_money_char.
                              sprintf('%.2f', $section->{'subtotal'})
@@ -2054,6 +2127,8 @@ sub print_generic {
     $options{'section'} = $section if $multisection;
     $options{'format'} = $format;
     $options{'escape_function'} = $escape_function;
+    $options{'format_function'} = sub { () } unless $unsquelched;
+    $options{'unsquelched'} = $unsquelched;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
@@ -2274,6 +2349,11 @@ sub print_generic {
     }
   }
 
+  if ( $multisection ) {
+    push @sections, @$late_sections
+      if $unsquelched;
+  }
+
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
@@ -2381,12 +2461,17 @@ when emailing the invoice as part of a multipart/related MIME email.
 =cut
 
 sub print_html {
-  my( $self, $today, $template, $cid ) = @_;
+  my $self = shift;
+  my %params;
+  if ( ref $_[0]  ) {
+    %params = %{ shift() }; 
+  }else{
+    $params{'time'} = shift;
+    $params{'template'} = shift;
+    $params{'cid'} = shift;
+  }
 
-  my %params = ( 'format' => 'html' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{'cid'} = $cid if $cid;
+  $params{'format'} = 'html';
 
   $self->print_generic( %params );
 }
@@ -2528,29 +2613,90 @@ Returns a string with the invoice number and date, for example:
 
 sub invnum_date_pretty {
   my $self = shift;
-  'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
+  'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
+}
+
+=item _date_pretty
+
+Returns a string with the date, for example: "3/20/2008"
+
+=cut
+
+sub _date_pretty {
+  my $self = shift;
+  time2str('%x', $self->_date);
 }
 
 sub _items_sections {
   my $self = shift;
+  my $late = shift;
 
   my %s = ();
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+  my %l = ();
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+  {
 
-      my $desc = $cust_bill_pkg->part_pkg->categoryname;
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
+      my $usage = $cust_bill_pkg->usage;
+
+      foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
+        my $desc = $display->section;
+        my $type = $display->type;
+
+        if ( $display->post_total ) {
+          if (! $type || $type eq 'S') {
+            $l{$desc} += $cust_bill_pkg->setup
+              if ( $cust_bill_pkg->setup != 0 );
+          }
+
+          if (! $type) {
+            $l{$desc} += $cust_bill_pkg->recur
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+
+          if ($type && $type eq 'R') {
+            $l{$desc} += $cust_bill_pkg->recur - $usage
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+          
+          if ($type && $type eq 'U') {
+            $l{$desc} += $usage;
+          }
+
+        } else {
+          if (! $type || $type eq 'S') {
+            $s{$desc} += $cust_bill_pkg->setup
+              if ( $cust_bill_pkg->setup != 0 );
+          }
+
+          if (! $type) {
+            $s{$desc} += $cust_bill_pkg->recur
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+
+          if ($type && $type eq 'R') {
+            $s{$desc} += $cust_bill_pkg->recur - $usage
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+          
+          if ($type && $type eq 'U') {
+            $s{$desc} += $usage;
+          }
 
-      $s{$desc} += $cust_bill_pkg->setup
-        if ( $cust_bill_pkg->setup != 0 );
+        }
 
-      $s{$desc} += $cust_bill_pkg->recur
-        if ( $cust_bill_pkg->recur != 0 );
+      }
 
     }
 
   }
 
+  push @$late, map { { 'description' => $_,
+                       'subtotal'    => $l{$_},
+                       'post_total'  => 1,
+                   } } sort keys %l;
+
   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
 
 }
@@ -2604,16 +2750,8 @@ sub _items_previous {
 
 sub _items_pkg {
   my $self = shift;
-  my %options = @_;
-  my $section = delete $options{'section'};
-  my @cust_bill_pkg =
-    grep { $_->pkgnum &&
-           ( defined($section)
-               ? $_->part_pkg->categoryname eq $section->{'description'}
-               : 1
-           )
-         } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
+  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
 }
 
 sub _taxsort {
@@ -2638,85 +2776,122 @@ sub _items_cust_bill_pkg {
 
   my $format = $opt{format} || '';
   my $escape_function = $opt{escape_function} || sub { shift };
+  my $format_function = $opt{format_function} || '';
+  my $unsquelched = $opt{unsquelched} || '';
+  my $section = $opt{section}->{description} if $opt{section};
 
   my @b = ();
-  foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
+  foreach my $cust_bill_pkg ( @$cust_bill_pkg )
+  {
+    foreach my $display ( grep { defined($section)
+                                 ? $_->section eq $section
+                                 : 1
+                               }
+                          $cust_bill_pkg->cust_bill_pkg_display
+                        )
+    {
 
-    my $cust_pkg = $cust_bill_pkg->cust_pkg;
+      my $type = $display->type;
 
-    my $desc = $cust_bill_pkg->desc;
+      my $cust_pkg = $cust_bill_pkg->cust_pkg;
 
-    my %details_opt = ( 'format'          => $format,
-                        'escape_function' => $escape_function,
-                      );
+      my $desc = $cust_bill_pkg->desc;
+      $desc = substr($desc, 0, 50). '...'
+        if $format eq 'latex' && length($desc) > 50;
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+      my %details_opt = ( 'format'          => $format,
+                          'escape_function' => $escape_function,
+                          'format_function' => $format_function,
+                        );
 
-      if ( $cust_bill_pkg->setup != 0 ) {
+      if ( $cust_bill_pkg->pkgnum > 0 ) {
 
-        my $description = $desc;
-        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+        if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
 
-        my @d = map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short($self->_date);
-        push @d, $cust_bill_pkg->details(%details_opt)
-          if $cust_bill_pkg->recur == 0;
+          my $description = $desc;
+          $description .= ' Setup' if $cust_bill_pkg->recur != 0;
 
-        push @b, {
-          description     => $description,
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          amount          => sprintf("%.2f", $cust_bill_pkg->setup),
-          unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
-          quantity        => $cust_bill_pkg->quantity,
-          ext_description => \@d,
-        };
-      }
+          my @d = map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short($self->_date);
+          push @d, $cust_bill_pkg->details(%details_opt)
+            if $cust_bill_pkg->recur == 0;
 
-      if ( $cust_bill_pkg->recur != 0 ) {
+          push @b, {
+            description     => $description,
+            #pkgpart         => $part_pkg->pkgpart,
+            pkgnum          => $cust_bill_pkg->pkgnum,
+            amount          => sprintf("%.2f", $cust_bill_pkg->setup),
+            unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
+            quantity        => $cust_bill_pkg->quantity,
+            ext_description => \@d,
+          };
 
-        my $description = $desc;
-        unless ( $conf->exists('disable_line_item_date_ranges') ) {
-          $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
-                          " - ". time2str("%x", $cust_bill_pkg->edate). ")";
         }
 
-        #at least until cust_bill_pkg has "past" ranges in addition to
-        #the "future" sdate/edate ones... see #3032
-        my @d = map &{$escape_function}($_),
-                    $cust_pkg->h_labels_short($self->_date);
-                                              #$cust_bill_pkg->edate,
-                                              #$cust_bill_pkg->sdate),
-        @d = () if $cust_bill_pkg->itemdesc;
-        push @d, $cust_bill_pkg->details(%details_opt);
-
-        push @b, {
-          description     => $description,
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          amount          => sprintf("%.2f", $cust_bill_pkg->recur),
-          unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
-          quantity        => $cust_bill_pkg->quantity,
-          ext_description => \@d,
-        };
+        if ( $cust_bill_pkg->recur != 0 &&
+             ( !$type || $type eq 'R' || $type eq 'U' )
+           )
+        {
+
+          my $is_summary = $display->summary;
+          my $description = $is_summary ? "Usage charges" : $desc;
+
+          unless ( $conf->exists('disable_line_item_date_ranges') ) {
+            $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
+                            " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+          }
+
+          #at least until cust_bill_pkg has "past" ranges in addition to
+          #the "future" sdate/edate ones... see #3032
+          my @d = ();
+          push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short($self->_date)
+                                                #$cust_bill_pkg->edate,
+                                                #$cust_bill_pkg->sdate),
+            ;
+  
+          @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
+          push @d, $cust_bill_pkg->details(%details_opt)
+            unless ($is_summary || $type && $type eq 'R');
+  
+          my $amount = 0;
+          if (!$type) {
+            $amount = $cust_bill_pkg->recur;
+          }elsif($type eq 'R') {
+            $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+          }elsif($type eq 'U') {
+            $amount = $cust_bill_pkg->usage;
+          }
+  
+          push @b, {
+            description     => $description,
+            #pkgpart         => $part_pkg->pkgpart,
+            pkgnum          => $cust_bill_pkg->pkgnum,
+            amount          => sprintf("%.2f", $amount),
+            unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
+            quantity        => $cust_bill_pkg->quantity,
+            ext_description => \@d,
+          } unless ( $type eq 'U' && ! $amount );
 
-      }
+        }
 
-    } else { #pkgnum tax or one-shot line item (??)
+      } else { #pkgnum tax or one-shot line item (??)
+
+        if ( $cust_bill_pkg->setup != 0 ) {
+          push @b, {
+            'description' => $desc,
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
+          };
+        }
+        if ( $cust_bill_pkg->recur != 0 ) {
+          push @b, {
+            'description' => "$desc (".
+                             time2str("%x", $cust_bill_pkg->sdate). ' - '.
+                             time2str("%x", $cust_bill_pkg->edate). ')',
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
+          };
+        }
 
-      if ( $cust_bill_pkg->setup != 0 ) {
-        push @b, {
-          'description' => $desc,
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-        };
-      }
-      if ( $cust_bill_pkg->recur != 0 ) {
-        push @b, {
-          'description' => "$desc (".
-                           time2str("%x", $cust_bill_pkg->sdate). ' - '.
-                           time2str("%x", $cust_bill_pkg->edate). ')',
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-        };
       }
 
     }
@@ -2787,7 +2962,7 @@ sub _items_payments {
 
 =over 4
 
-=item reprint
+=item process_reprint
 
 =cut
 
@@ -2795,7 +2970,7 @@ sub process_reprint {
   process_re_X('print', @_);
 }
 
-=item reemail
+=item process_reemail
 
 =cut
 
@@ -2803,7 +2978,7 @@ sub process_reemail {
   process_re_X('email', @_);
 }
 
-=item refax
+=item process_refax
 
 =cut
 
@@ -2811,6 +2986,22 @@ sub process_refax {
   process_re_X('fax', @_);
 }
 
+=item process_reftp
+
+=cut
+
+sub process_reftp {
+  process_re_X('ftp', @_);
+}
+
+=item respool
+
+=cut
+
+sub process_respool {
+  process_re_X('spool', @_);
+}
+
 use Storable qw(thaw);
 use Data::Dumper;
 use MIME::Base64;
@@ -2854,6 +3045,8 @@ sub re_X {
     'debug' => 1,
   } );
 
+  $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
+
   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
     if $DEBUG;