voip: add start time for calls to invoice details
[freeside.git] / FS / FS / cust_bill.pm
index 9f1d7a6..ecc6d5f 100644 (file)
@@ -5,6 +5,8 @@ use vars qw( @ISA $conf $money_char );
 use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
 use Text::Template;
+use File::Temp 0.14;
+use String::ShellQuote;
 use FS::UID qw( datasrc );
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email );
@@ -316,51 +318,87 @@ sub owed {
   $balance;
 }
 
-=item send
+=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>.
 
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+AGENTNUM, if specified, means that this invoice will only be sent for customers
+of the specified agent.
+
+INVOICE_FROM, if specified, overrides the default email invoice From: address.
+
 =cut
 
 sub send {
-  my($self,$template) = @_;
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+  return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
+  my $invoice_from =
+    scalar(@_)
+      ? shift
+      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+
   my @print_text = $self->print_text('', $template);
   my @invoicing_list = $self->cust_main->invoicing_list;
 
   if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list  ) { #email
 
     #better to notify this person than silence
-    @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
+    @invoicing_list = ($invoice_from) unless @invoicing_list;
 
     my $error = send_email(
-      'from'    => $conf->config('invoice_from'),
+      'from'    => $invoice_from,
       'to'      => [ grep { $_ ne 'POST' } @invoicing_list ],
       'subject' => 'Invoice',
       'body'    => \@print_text,
     );
-    return "can't send invoice: $error" if $error;
-
-  }
+    die "can't email invoice: $error\n" if $error;
+    #die "$error\n" if $error;
 
-  if ( $conf->config('invoice_latex') ) {
-    @print_text = $self->print_ps('', $template);
   }
 
   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
+    @print_text = $self->print_ps('', $template)
+      if $conf->config('invoice_latex');
     my $lpr = $conf->config('lpr');
     open(LPR, "|$lpr")
-      or return "Can't open pipe to $lpr: $!";
+      or die "Can't open pipe to $lpr: $!\n";
     print LPR @print_text;
     close LPR
-      or return $! ? "Error closing $lpr: $!"
-                   : "Exit status $? from $lpr";
+      or die $! ? "Error closing $lpr: $!\n"
+                : "Exit status $? from $lpr\n";
   }
 
   '';
 
 }
 
+=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+
+Like B<send>, but only sends the invoice if it is the newest open invoice for
+this customer.
+
+=cut
+
+sub send_if_newest {
+  my $self = shift;
+
+  return ''
+    if scalar(
+               grep { $_->owed > 0 } 
+                    qsearch('cust_bill', {
+                      'custnum' => $self->custnum,
+                      #'_date'   => { op=>'>', value=>$self->_date },
+                      'invnum'  => { op=>'>', value=>$self->invnum },
+                    } )
+             );
+    
+  $self->send(@_);
+}
+
 =item send_csv OPTIONS
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
@@ -654,6 +692,45 @@ sub batch_card {
   '';
 }
 
+sub _agent_template {
+  my $self = shift;
+  $self->_agent_plandata('agent_templatename');
+}
+
+sub _agent_invoice_from {
+  my $self = shift;
+  $self->_agent_plandata('agent_invoice_from');
+}
+
+sub _agent_plandata {
+  my( $self, $option ) = @_;
+
+  my $part_bill_event = qsearchs( 'part_bill_event',
+    {
+      'payby'     => $self->cust_main->payby,
+      'plan'      => 'send_agent',
+      'plandata'  => { 'op'    => '~',
+                       'value' => "(^|\n)agentnum ".
+                                  $self->cust_main->agentnum.
+                                  "(\n|\$)",
+                     },
+    },
+    '',
+    'ORDER BY seconds LIMIT 1'
+  );
+
+  return '' unless $part_bill_event;
+
+  if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
+    return $1;
+  } else {
+    warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
+         " plandata for $option";
+    return '';
+  }
+
+}
+
 =item print_text [ TIME [ , TEMPLATE ] ]
 
 Returns an text invoice, as a list of lines.
@@ -665,14 +742,16 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =cut
 
+#still some false laziness w/print_text
 sub print_text {
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
+
 #  my $invnum = $self->invnum;
-  my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
+  my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
@@ -716,7 +795,8 @@ sub print_text {
         push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($self->_date);
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
@@ -726,7 +806,8 @@ sub print_text {
           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
       }
 
       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
@@ -798,10 +879,11 @@ sub print_text {
     sprintf("%10.2f", $balance_due ) ];
 
   #create the template
+  $template ||= $self->_agent_template;
   my $templatefile = 'invoice_template';
-  $templatefile .= "_$template" if $template;
+  $templatefile .= "_$template" if length($template);
   my @invoice_template = $conf->config($templatefile)
-  or die "cannot load config file $templatefile";
+    or die "cannot load config file $templatefile";
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
@@ -911,7 +993,7 @@ sub print_latex {
 #  my $invnum = $self->invnum;
   my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
@@ -923,8 +1005,10 @@ sub print_latex {
   @buf = ();
 
   #create the template
+  $template ||= $self->_agent_template;
   my $templatefile = 'invoice_latex';
-  $templatefile .= "_$template" if $template;
+  my $suffix = length($template) ? "_$template" : '';
+  $templatefile .= $suffix;
   my @invoice_template = $conf->config($templatefile)
     or die "cannot load config file $templatefile";
 
@@ -941,7 +1025,7 @@ sub print_latex {
     'zip'          => _latex_escape($cust_main->zip),
     'country'      => _latex_escape($cust_main->country),
     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
-    'smallfooter'  => $conf->config('invoice_latexsmallfooter'),
+    'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
     'quantity'     => 1,
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
@@ -954,10 +1038,11 @@ sub print_latex {
   $invoice_data{'notes'} =
     join("\n",
       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-        $conf->config('invoice_latexnotes')
+        $conf->config_orbase('invoice_latexnotes', $suffix)
     );
 
   $invoice_data{'footer'} =~ s/\n+$//;
+  $invoice_data{'smallfooter'} =~ s/\n+$//;
   $invoice_data{'notes'} =~ s/\n+$//;
 
   $invoice_data{'po_line'} =
@@ -1071,17 +1156,17 @@ sub print_latex {
     $var;
   }
 
-  my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
-  my $unique = int(rand(2**31)); #UGH... use File::Temp or something
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
+                           DIR      => $dir,
+                           SUFFIX   => '.tex',
+                           UNLINK   => 0,
+                         ) or die "can't open temp file: $!\n";
+  print $fh join("\n", @filled_in ), "\n";
+  close $fh;
 
-  chdir($dir);
-  my $file = $self->invnum. ".$unique";
-
-  open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
-  print TEX join("\n", @filled_in ), "\n";
-  close TEX;
-
-  return $file;
+  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
+  return $1;
 
 }
 
@@ -1101,17 +1186,23 @@ sub print_ps {
 
   my $file = $self->print_latex(@_);
 
-  #error checking!!
-  system('pslatex', "$file.tex");
-  system('pslatex', "$file.tex");
-  system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  chdir($dir);
+
+  my $sfile = shell_quote $file;
+
+  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed; see $file.log for details?\n";
+  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed; see $file.log for details?\n";
+
+  system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
+    or die "dvips failed";
 
   open(POSTSCRIPT, "<$file.ps")
-    or die "can't open $file.ps (probable error in LaTeX template): $!\n";
+    or die "can't open $file.ps: $! (error in LaTeX template?)\n";
 
-  #rm $file.dvi $file.log $file.aux
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
-  #unlink("$file.dvi", "$file.log", "$file.aux");
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
 
   my $ps = '';
   while (<POSTSCRIPT>) {
@@ -1140,21 +1231,32 @@ sub print_pdf {
 
   my $file = $self->print_latex(@_);
 
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  chdir($dir);
+
   #system('pdflatex', "$file.tex");
   #system('pdflatex', "$file.tex");
   #! LaTeX Error: Unknown graphics extension: .eps.
 
-  #error checking!!
-  system('pslatex', "$file.tex");
-  system('pslatex', "$file.tex");
+  my $sfile = shell_quote $file;
+
+  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed: $!";
+  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed: $!";
 
   #system('dvipdf', "$file.dvi", "$file.pdf" );
-  system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
+  system(
+    "dvips -q -t letter -f $sfile.dvi ".
+    "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
+    "     -c save pop -"
+  ) == 0
+    or die "dvips | gs failed: $!";
 
   open(PDF, "<$file.pdf")
-    or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
+    or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
 
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf");
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
 
   my $pdf = '';
   while (<PDF>) {
@@ -1178,7 +1280,7 @@ sub print_pdf {
 
 sub _latex_escape {
   my $value = shift;
-  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
+  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
   $value;
 }
 
@@ -1268,32 +1370,29 @@ sub _items_cust_bill_pkg {
       if ( $cust_bill_pkg->setup != 0 ) {
         my $description = $pkg;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = ();
-        @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
+        my @d = $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,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
-          'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
-                                         $cust_pkg->labels        ),
-                                 @d,
-                               ],
+          description     => $description,
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
+          ext_description => \@d,
         };
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
-          'description'     => "$pkg (" .
+          description     => "$pkg (" .
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
-          #'pkgpart'         => $part_pkg->pkgpart,
-          'pkgnum'          => $cust_pkg->pkgnum,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
-          'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
-                                       $cust_pkg->labels          ),
-                                 $cust_bill_pkg->details,
-                               ],
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
+          ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
+                                                         $cust_bill_pkg->sdate),
+                               $cust_bill_pkg->details,
+                             ],
         };
       }
 
@@ -1342,7 +1441,7 @@ sub _items_credits {
       #'description' => 'Credit ref\#'. $_->crednum.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
-      'description' => 'Credit applied'.
+      'description' => 'Credit applied '.
                        time2str("%x",$_->cust_credit->_date). $reason,
       'amount'      => sprintf("%10.2f",$_->amount),
     };
@@ -1387,9 +1486,6 @@ The delete method.
 print_text formatting (and some logic :/) is in source, but needs to be
 slurped in from a file.  Also number of lines ($=).
 
-missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
-or something similar so the look can be completely customized?)
-
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,