add cpanel export
[freeside.git] / FS / FS / cust_bill.pm
index c3ad158..46809f9 100644 (file)
@@ -3,6 +3,7 @@ package FS::cust_bill;
 use strict;
 use vars qw( @ISA $DEBUG $conf $money_char );
 use vars qw( $invoice_lines @buf ); #yuck
+use IPC::Run3;
 use Date::Format;
 use Text::Template 1.20;
 use File::Temp 0.14;
@@ -10,8 +11,9 @@ use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
 use FS::UID qw( datasrc );
-use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email send_fax );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main_Mixin;
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
@@ -24,7 +26,7 @@ use FS::part_pkg;
 use FS::cust_bill_pay;
 use FS::part_bill_event;
 
-@ISA = qw( FS::Record );
+@ISA = qw( FS::cust_main_Mixin FS::Record );
 
 $DEBUG = 0;
 
@@ -104,6 +106,13 @@ Invoices are normally created by calling the bill method of a customer object
 
 sub table { 'cust_bill'; }
 
+sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_unlinked_msg {
+  my $self = shift;
+  "WARNING: can't find cust_main.custnum ". $self->custnum.
+  ' (cust_bill.invnum '. $self->invnum. ')';
+}
+
 =item insert
 
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
@@ -414,10 +423,21 @@ sub generate_email {
     $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
     my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
 
+    my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
+    my $file;
+    if ( defined($args{'_template'}) && length($args{'_template'})
+         && -e "$path/logo_". $args{'_template'}. ".png"
+       )
+    {
+      $file = "$path/logo_". $args{'_template'}. ".png";
+    } else {
+      $file = "$path/logo.png";
+    }
+
     my $image = build MIME::Entity
       'Type'       => 'image/png',
       'Encoding'   => 'base64',
-      'Path'       => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
+      'Path'       => $file,
       'Filename'   => 'logo.png',
       'Content-ID' => "<$content_id>",
     ;
@@ -544,8 +564,8 @@ sub mimebuild_pdf {
 
 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
 
-Sends this invoice to the destinations configured for this customer: send
-emails or print.  See L<FS::cust_main_invoice>.
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
@@ -623,7 +643,7 @@ sub email {
 
 =item lpr_data [ TEMPLATENAME ]
 
-Returns the postscript or plaintext for this invoice.
+Returns the postscript or plaintext for this invoice as an arrayref.
 
 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
@@ -649,12 +669,14 @@ sub print {
   my $template = scalar(@_) ? shift : '';
 
   my $lpr = $conf->config('lpr');
-  open(LPR, "|$lpr")
-    or die "Can't open pipe to $lpr: $!\n";
-  print LPR @{ $self->lpr_data($template) };
-  close LPR
-    or die $! ? "Error closing $lpr: $!\n"
-              : "Exit status $? from $lpr\n";
+
+  my $outerr = '';
+  run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
+  if ( $? ) {
+    $outerr = ": $outerr" if length($outerr);
+    die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
+  }
+
 }
 
 =item fax [ TEMPLATENAME ] 
@@ -807,7 +829,7 @@ sub send_csv {
   ) or die "can't create csv";
   print CSV $csv->string. "\n";
 
-  #new charges (false laziness w/print_text)
+  #new charges (false laziness w/print_text and _items stuff)
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
 
     my($pkg, $setup, $recur, $sdate, $edate);
@@ -1017,9 +1039,9 @@ sub _agent_plandata {
       'plan'      => 'send_agent',
       'plandata'  => { 'op'    => '~',
                        'value' => "(^|\n)agentnum ".
-                                   '\[?[0-9 ,]*'.
-                                  '[ ,]'. $self->cust_main->agentnum. '[ ,]'
-                                  '[0-9 ,]*\]?'.
+                                   '([0-9]*, )*'.
+                                  $self->cust_main->agentnum.
+                                   '(, [0-9]*)*'.
                                   "(\n|\$)",
                      },
     },
@@ -1050,7 +1072,7 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =cut
 
-#still some false laziness w/print_text
+#still some false laziness w/_items stuff (and send_csv)
 sub print_text {
 
   my( $self, $today, $template ) = @_;
@@ -1091,50 +1113,49 @@ sub print_text {
     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
   ) {
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+    my $desc = $cust_bill_pkg->desc;
 
-      my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
-      my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
-      my $pkg = $part_pkg->pkg;
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
 
       if ( $cust_bill_pkg->setup != 0 ) {
-        my $description = $pkg;
+        my $description = $desc;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
         push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
-              $cust_pkg->h_labels($self->_date);
+              $cust_bill_pkg->cust_pkg->h_labels($self->_date);
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @buf, [
-          "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
-                                time2str("%x", $cust_bill_pkg->edate) . ")",
+          "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
+                      time2str("%x", $cust_bill_pkg->edate) . ")",
           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
-              $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
+              $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
+                                                  $cust_bill_pkg->sdate );
       }
 
       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
 
     } else { #pkgnum tax or one-shot line item
-      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
-                     ? ( $cust_bill_pkg->itemdesc || 'Tax' )
-                     : 'Tax';
+
       if ( $cust_bill_pkg->setup != 0 ) {
-        push @buf, [ $itemdesc,
+        push @buf, [ $desc,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
       }
       if ( $cust_bill_pkg->recur != 0 ) {
-        push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
-                                  . time2str("%x", $cust_bill_pkg->edate). ")",
+        push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
+                              . time2str("%x", $cust_bill_pkg->edate). ")",
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
                    ];
       }
+
     }
+
   }
 
   push @buf,['','-----------'];
@@ -1294,7 +1315,7 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =cut
 
-#still some false laziness w/print_text
+#still some false laziness w/print_text (mostly print_text should use _items stuff though)
 sub print_latex {
 
   my( $self, $today, $template ) = @_;
@@ -1358,8 +1379,8 @@ sub print_latex {
     'city'         => _latex_escape($cust_main->city),
     'state'        => _latex_escape($cust_main->state),
     'zip'          => _latex_escape($cust_main->zip),
-    'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
-    'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
+    'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
+    'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
     'returnaddress' => $returnaddress,
     'quantity'     => 1,
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
@@ -1760,20 +1781,29 @@ sub print_html {
     'terms'        => $conf->config('invoice_default_terms')
                       || 'Payable upon receipt',
     'cid'          => $cid,
+    'template'     => $template,
 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
   );
 
-  $invoice_data{'returnaddress'} =
-    length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
-      ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
-      : join("\n", map { 
-                         s/~/&nbsp;/g;
-                         s/\\\\\*?\s*$/<BR>/;
-                         s/\\hyphenation\{[\w\s\-]+\}//;
-                         $_;
-                       }
-                       $conf->config_orbase('invoice_latexreturnaddress', $template)
-            );
+  if (
+         defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
+      && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
+  ) {
+    $invoice_data{'returnaddress'} =
+      join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
+  } else {
+    $invoice_data{'returnaddress'} =
+      join("\n", map { 
+                       s/~/&nbsp;/g;
+                       s/\\\\\*?\s*$/<BR>/;
+                       s/\\hyphenation\{[\w\s\-]+\}//;
+                       $_;
+                     }
+                     $conf->config_orbase( 'invoice_latexreturnaddress',
+                                           $template
+                                         )
+          );
+  }
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
   if ( $cust_main->country eq $countrydefault ) {
@@ -1783,20 +1813,26 @@ sub print_html {
       encode_entities(code2country($cust_main->country));
   }
 
-  $invoice_data{'notes'} =
-    length($conf->config_orbase('invoice_htmlnotes', $template))
-      ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
-      : join("\n", map { 
-                         s/%%(.*)$/<!-- $1 -->/;
-                         s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
-                         s/\\begin\{enumerate\}/<ol>/;
-                         s/\\item /  <li>/;
-                         s/\\end\{enumerate\}/<\/ol>/;
-                         s/\\textbf\{(.*)\}/<b>$1<\/b>/;
-                         $_;
-                       } 
-                       $conf->config_orbase('invoice_latexnotes', $template)
-            );
+  if (
+         defined( $conf->config_orbase('invoice_htmlnotes', $template) )
+      && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
+  ) {
+    $invoice_data{'notes'} =
+      join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
+  } else {
+    $invoice_data{'notes'} = 
+      join("\n", map { 
+                       s/%%(.*)$/<!-- $1 -->/;
+                       s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
+                       s/\\begin\{enumerate\}/<ol>/;
+                       s/\\item /  <li>/;
+                       s/\\end\{enumerate\}/<\/ol>/;
+                       s/\\textbf\{(.*)\}/<b>$1<\/b>/;
+                       $_;
+                     } 
+                     $conf->config_orbase('invoice_latexnotes', $template)
+          );
+  }
 
 #  #do variable substitutions in notes
 #  $invoice_data{'notes'} =
@@ -1805,11 +1841,18 @@ sub print_html {
 #        $conf->config_orbase('invoice_latexnotes', $suffix)
 #    );
 
-   $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
-     ? join("\n", $conf->config('invoice_htmlfooter') )
-     : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
-                      $conf->config('invoice_latexfooter')
+  if (
+         defined( $conf->config_orbase('invoice_htmlfooter', $template) )
+      && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
+  ) {
+   $invoice_data{'footer'} =
+     join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
+  } else {
+   $invoice_data{'footer'} =
+       join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
+                      $conf->config_orbase('invoice_latexfooter', $template)
            );
+  }
 
   $invoice_data{'po_line'} =
     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
@@ -1986,21 +2029,19 @@ sub _items_cust_bill_pkg {
   my @b = ();
   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+    my $desc = $cust_bill_pkg->desc;
 
-      my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
-      my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
-      my $pkg = $part_pkg->pkg;
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
 
       if ( $cust_bill_pkg->setup != 0 ) {
-        my $description = $pkg;
+        my $description = $desc;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = $cust_pkg->h_labels_short($self->_date);
+        my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
         push @b, {
           description     => $description,
           #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_pkg->pkgnum,
+          pkgnum          => $cust_bill_pkg->pkgnum,
           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
           ext_description => \@d,
         };
@@ -2008,33 +2049,31 @@ sub _items_cust_bill_pkg {
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
-          description     => "$pkg (" .
+          description     => "$desc (" .
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
           #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_pkg->pkgnum,
+          pkgnum          => $cust_bill_pkg->pkgnum,
           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
-          ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
-                                                         $cust_bill_pkg->sdate),
-                               $cust_bill_pkg->details,
-                             ],
+          ext_description =>
+            [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
+                                                        $cust_bill_pkg->sdate),
+              $cust_bill_pkg->details,
+            ],
         };
       }
 
     } else { #pkgnum tax or one-shot line item (??)
 
-      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
-                     ? ( $cust_bill_pkg->itemdesc || 'Tax' )
-                     : 'Tax';
       if ( $cust_bill_pkg->setup != 0 ) {
         push @b, {
-          'description' => $itemdesc,
+          'description' => $desc,
           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
         };
       }
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
-          'description' => "$itemdesc (".
+          'description' => "$desc (".
                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
                            time2str("%x", $cust_bill_pkg->edate). ')',
           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
@@ -2104,6 +2143,122 @@ sub _items_payments {
 
 =back
 
+=head1 SUBROUTINES
+
+=over 4
+
+=item reprint
+
+=cut
+
+sub process_reprint {
+  process_re_X('print', @_);
+}
+
+=item reemail
+
+=cut
+
+sub process_reemail {
+  process_re_X('email', @_);
+}
+
+=item refax
+
+=cut
+
+sub process_refax {
+  process_re_X('fax', @_);
+}
+
+use Storable qw(thaw);
+use Data::Dumper;
+use MIME::Base64;
+sub process_re_X {
+  my( $method, $job ) = ( shift, shift );
+
+  my $param = thaw(decode_base64(shift));
+  warn Dumper($param) if $DEBUG;
+
+  re_X(
+    $method,
+    $job,
+    %$param,
+  );
+
+}
+
+sub re_X {
+  my($method, $job, %param ) = @_;
+#              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
+
+  #some false laziness w/search/cust_bill.html
+  my $distinct = '';
+  my $orderby = 'ORDER BY cust_bill._date';
+
+  my @where;
+
+  if ( $param{'begin'} =~ /^(\d+)$/ ) {
+    push @where, "cust_bill._date >= $1";
+  }
+  if ( $param{'end'} =~ /^(\d+)$/ ) {
+    push @where, "cust_bill._date < $1";
+  }
+  if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
+    push @where, "cust_main.agentnum = $1";
+  }
+
+  my $owed =
+    "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
+                 WHERE cust_bill_pay.invnum = cust_bill.invnum )
+             - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
+                 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
+
+  push @where, "0 != $owed"
+    if $param{'open'};
+
+  push @where, "cust_bill._date < ". (time-86400*$param{'days'})
+    if $param{'days'};
+
+  my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+  my $addl_from = 'left join cust_main using ( custnum )';
+
+  if ( $param{'newest_percust'} ) {
+    $distinct = 'DISTINCT ON ( cust_bill.custnum )';
+    $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
+    #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
+  }
+     
+  my @cust_bill = qsearch( 'cust_bill',
+                           {},
+                           "$distinct cust_bill.*",
+                           $extra_sql,
+                           '',
+                           $addl_from
+                         );
+
+  my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+  foreach my $cust_bill ( @cust_bill ) {
+    $cust_bill->$method();
+
+    if ( $job ) { #progressbar foo
+      $num++;
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $num / scalar(@cust_bill) )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
+  }
+
+}
+
+=back
+
 =head1 BUGS
 
 The delete method.