consolidate large numbers of accounts to avoid gigantic line items
[freeside.git] / FS / FS / cust_bill.pm
index a22f44b..a6b4e1e 100644 (file)
@@ -161,7 +161,7 @@ sub check {
 
   $self->printed(0) if $self->printed eq '';
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item previous
@@ -343,6 +343,10 @@ sub send {
 
   }
 
+  if ( $conf->config('invoice_latex') ) {
+    @print_text = $self->print_ps('', $template);
+  }
+
   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
     my $lpr = $conf->config('lpr');
     open(LPR, "|$lpr")
@@ -590,7 +594,10 @@ sub realtime_bop {
   my( $self, $method ) = @_;
 
   my $cust_main = $self->cust_main;
-  my $amount = $self->owed;
+  my $balance = $cust_main->balance;
+  my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
+  $amount = sprintf("%.2f", $amount);
+  return "not run (balance $balance)" unless $amount > 0;
 
   my $description = 'Internet Services';
   if ( $conf->exists('business-onlinepayment-description') ) {
@@ -636,7 +643,6 @@ sub batch_card {
     'state'    => $cust_main->getfield('state'),
     'zip'      => $cust_main->getfield('zip'),
     'country'  => $cust_main->getfield('country'),
-    'trancode' => 77,
     'cardnum'  => $cust_main->getfield('payinfo'),
     'exp'      => $cust_main->getfield('paydate'),
     'payname'  => $cust_main->getfield('payname'),
@@ -648,7 +654,7 @@ sub batch_card {
   '';
 }
 
-=item print_text [TIME];
+=item print_text [ TIME [ , TEMPLATE ] ]
 
 Returns an text invoice, as a list of lines.
 
@@ -705,7 +711,9 @@ sub print_text {
       my $pkg = $part_pkg->pkg;
 
       if ( $cust_bill_pkg->setup != 0 ) {
-        push @buf, [ "$pkg Setup",
+        my $description = $pkg;
+        $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->labels;
@@ -783,8 +791,10 @@ sub print_text {
   }
 
   #balance due
+  my $balance_due_msg = $self->balance_due_msg;
+
   push @buf,['','-----------'];
-  push @buf,['Balance Due', $money_char. 
+  push @buf,[$balance_due_msg, $money_char. 
     sprintf("%10.2f", $balance_due ) ];
 
   #create the template
@@ -878,6 +888,505 @@ sub print_text {
 
 }
 
+=item print_latex [ TIME [ , TEMPLATE ] ]
+
+Internal method - returns a filename of a filled-in LaTeX template for this
+invoice (Note: add ".tex" to get the actual filename).
+
+See print_ps and print_pdf for methods that return PostScript and PDF output.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+#still some false laziness w/print_text
+sub print_latex {
+
+  my( $self, $today, $template ) = @_;
+  $today ||= time;
+
+#  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';
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
+
+  #my @collect = ();
+  #my($description,$amount);
+  @buf = ();
+
+  #create the template
+  my $templatefile = 'invoice_latex';
+  $templatefile .= "_$template" if $template;
+  my @invoice_template = $conf->config($templatefile)
+    or die "cannot load config file $templatefile";
+
+  my %invoice_data = (
+    'invnum'       => $self->invnum,
+    'date'         => time2str('%b %o, %Y', $self->_date),
+    'agent'        => _latex_escape($cust_main->agent->agent),
+    'payname'      => _latex_escape($cust_main->payname),
+    'company'      => _latex_escape($cust_main->company),
+    'address1'     => _latex_escape($cust_main->address1),
+    'address2'     => _latex_escape($cust_main->address2),
+    'city'         => _latex_escape($cust_main->city),
+    'state'        => _latex_escape($cust_main->state),
+    'zip'          => _latex_escape($cust_main->zip),
+    'country'      => _latex_escape($cust_main->country),
+    'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
+    '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') ),
+  );
+
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
+
+  #do variable substitutions in notes
+  $invoice_data{'notes'} =
+    join("\n",
+      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+        $conf->config('invoice_latexnotes')
+    );
+
+  $invoice_data{'footer'} =~ s/\n+$//;
+  $invoice_data{'smallfooter'} =~ s/\n+$//;
+  $invoice_data{'notes'} =~ s/\n+$//;
+
+  $invoice_data{'po_line'} =
+    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+      ? _latex_escape("Purchase Order #". $cust_main->payinfo)
+      : '~';
+
+  my @line_item = ();
+  my @total_item = ();
+  my @filled_in = ();
+  while ( @invoice_template ) {
+    my $line = shift @invoice_template;
+
+    if ( $line =~ /^%%Detail\s*$/ ) {
+
+      while ( ( my $line_item_line = shift @invoice_template )
+              !~ /^%%EndDetail\s*$/                            ) {
+        push @line_item, $line_item_line;
+      }
+      foreach my $line_item ( $self->_items ) {
+      #foreach my $line_item ( $self->_items_pkg ) {
+        $invoice_data{'ref'} = $line_item->{'pkgnum'};
+        $invoice_data{'description'} = _latex_escape($line_item->{'description'});
+        if ( exists $line_item->{'ext_description'} ) {
+          $invoice_data{'description'} .=
+            "\\tabularnewline\n~~".
+            join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
+        }
+        $invoice_data{'amount'} = $line_item->{'amount'};
+        $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+        push @filled_in,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
+      }
+
+    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+
+      while ( ( my $total_item_line = shift @invoice_template )
+              !~ /^%%EndTotalDetails\s*$/                      ) {
+        push @total_item, $total_item_line;
+      }
+
+      my @total_fill = ();
+
+      my $taxtotal = 0;
+      foreach my $tax ( $self->_items_tax ) {
+        $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
+        $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
+        push @total_fill,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      if ( $taxtotal ) {
+        $invoice_data{'total_item'} = 'Sub-total';
+        $invoice_data{'total_amount'} =
+          '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
+        unshift @total_fill,
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      $invoice_data{'total_item'} = '\textbf{Total}';
+      $invoice_data{'total_amount'} =
+        '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
+      push @total_fill,
+        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+            @total_item;
+
+      #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+
+      # credits
+      foreach my $credit ( $self->_items_credits ) {
+        $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
+        #$credittotal
+        $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
+        push @total_fill, 
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      # payments
+      foreach my $payment ( $self->_items_payments ) {
+        $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
+        #$paymenttotal
+        $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
+        push @total_fill, 
+          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+              @total_item;
+      }
+
+      $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
+      $invoice_data{'total_amount'} =
+        '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
+      push @total_fill,
+        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+            @total_item;
+
+      push @filled_in, @total_fill;
+
+    } else {
+      #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
+      $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
+      push @filled_in, $line;
+    }
+
+  }
+
+  sub nounder {
+    my $var = $1;
+    $var =~ s/_/\-/g;
+    $var;
+  }
+
+  my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
+  my $unique = int(rand(2**31)); #UGH... use File::Temp or something
+
+  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;
+
+}
+
+=item print_ps [ TIME [ , TEMPLATE ] ]
+
+Returns an postscript invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_ps {
+  my $self = shift;
+
+  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" );
+
+  open(POSTSCRIPT, "<$file.ps")
+    or die "can't open $file.ps (probable error in LaTeX template): $!\n";
+
+  unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
+
+  my $ps = '';
+  while (<POSTSCRIPT>) {
+    $ps .= $_;
+  }
+
+  close POSTSCRIPT;
+
+  return $ps;
+
+}
+
+=item print_pdf [ TIME [ , TEMPLATE ] ]
+
+Returns an PDF invoice, as a scalar.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_pdf {
+  my $self = shift;
+
+  my $file = $self->print_latex(@_);
+
+  #system('pdflatex', "$file.tex");
+  #system('pdflatex', "$file.tex");
+  #! LaTeX Error: Unknown graphics extension: .eps.
+
+  #error checking!!
+  system('pslatex', "$file.tex");
+  system('pslatex', "$file.tex");
+
+  #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 -");
+
+  open(PDF, "<$file.pdf")
+    or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
+
+#  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
+
+  my $pdf = '';
+  while (<PDF>) {
+    $pdf .= $_;
+  }
+
+  close PDF;
+
+  return $pdf;
+
+}
+
+# quick subroutine for print_latex
+#
+# There are ten characters that LaTeX treats as special characters, which
+# means that they do not simply typeset themselves: 
+#      # $ % & ~ _ ^ \ { }
+#
+# TeX ignores blanks following an escaped character; if you want a blank (as
+# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
+
+sub _latex_escape {
+  my $value = shift;
+  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
+  $value;
+}
+
+#utility methods for print_*
+
+sub balance_due_msg {
+  my $self = shift;
+  my $msg = 'Balance Due';
+  return $msg unless $conf->exists('invoice_default_terms');
+  if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
+  } elsif ( $conf->config('invoice_default_terms') ) {
+    $msg .= ' - '. $conf->config('invoice_default_terms');
+  }
+  $msg;
+}
+
+sub _items {
+  my $self = shift;
+  my @display = scalar(@_)
+                ? @_
+                : qw( _items_previous _items_pkg );
+                #: qw( _items_pkg );
+                #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+  my @b = ();
+  foreach my $display ( @display ) {
+    push @b, $self->$display(@_);
+  }
+  @b;
+}
+
+sub _items_previous {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+  my @b = ();
+  foreach ( @pr_cust_bill ) {
+    push @b, {
+      'description' => 'Previous Balance, Invoice #'. $_->invnum. 
+                       ' ('. time2str('%x',$_->_date). ')',
+      #'pkgpart'     => 'N/A',
+      'pkgnum'      => 'N/A',
+      'amount'      => sprintf("%10.2f", $_->owed),
+    };
+  }
+  @b;
+
+  #{
+  #    'description'     => 'Previous Balance',
+  #    #'pkgpart'         => 'N/A',
+  #    'pkgnum'          => 'N/A',
+  #    'amount'          => sprintf("%10.2f", $pr_total ),
+  #    'ext_description' => [ map {
+  #                                 "Invoice ". $_->invnum.
+  #                                 " (". time2str("%x",$_->_date). ") ".
+  #                                 sprintf("%10.2f", $_->owed)
+  #                         } @pr_cust_bill ],
+
+  #};
+}
+
+sub _items_pkg {
+  my $self = shift;
+  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+sub _items_tax {
+  my $self = shift;
+  my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+}
+
+sub _items_cust_bill_pkg {
+  my $self = shift;
+  my $cust_bill_pkg = shift;
+
+  my @b = ();
+  foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
+
+    if ( $cust_bill_pkg->pkgnum ) {
+
+      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;
+
+      my %labels;
+      #tie %labels, 'Tie::IxHash';
+      push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
+      my @ext_description;
+      foreach my $label ( keys %labels ) {
+        my @values = @{ $labels{$label} };
+        my $num = scalar(@values);
+        if ( $num > 5 ) {
+          push @ext_description, "$label ($num)";
+        } else {
+          push @ext_description, map { "$label: $_" } @values;
+        }
+      }
+
+      if ( $cust_bill_pkg->setup != 0 ) {
+        my $description = $pkg;
+        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+        my @d = @ext_description;
+        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' => \@d,
+        };
+      }
+
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @b, {
+          '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' => [ @ext_description,
+                                 $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,
+          'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
+        };
+      }
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @b, {
+          'description' => "$itemdesc (".
+                           time2str("%x", $cust_bill_pkg->sdate). ' - '.
+                           time2str("%x", $cust_bill_pkg->edate). ')',
+          'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
+        };
+      }
+
+    }
+
+  }
+
+  @b;
+
+}
+
+sub _items_credits {
+  my $self = shift;
+
+  my @b;
+  #credits
+  foreach ( $self->cust_credited ) {
+
+    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+
+    my $reason = $_->cust_credit->reason;
+    #my $reason = substr($_->cust_credit->reason,0,32);
+    #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
+    $reason = " ($reason) " if $reason;
+    push @b, {
+      #'description' => 'Credit ref\#'. $_->crednum.
+      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
+      #                 $reason,
+      'description' => 'Credit applied'.
+                       time2str("%x",$_->cust_credit->_date). $reason,
+      'amount'      => sprintf("%10.2f",$_->amount),
+    };
+  }
+  #foreach ( @cr_cust_credit ) {
+  #  push @buf,[
+  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
+  #    $money_char. sprintf("%10.2f",$_->credited)
+  #  ];
+  #}
+
+  @b;
+
+}
+
+sub _items_payments {
+  my $self = shift;
+
+  my @b;
+  #get & print payments
+  foreach ( $self->cust_bill_pay ) {
+
+    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+
+    push @b, {
+      'description' => "Payment received ".
+                       time2str("%x",$_->cust_pay->_date ),
+      'amount'      => sprintf("%10.2f", $_->amount )
+    };
+  }
+
+  @b;
+
+}
+
 =back
 
 =head1 BUGS