forgotten space in typeset invoice credit lines
[freeside.git] / FS / FS / cust_bill.pm
index d6e4ab6..b249533 100644 (file)
@@ -5,6 +5,7 @@ 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 FS::UID qw( datasrc );
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email );
@@ -316,41 +317,59 @@ 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;
 
   }
 
+  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")
-      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";
   }
 
   '';
@@ -590,7 +609,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') ) {
@@ -647,6 +669,44 @@ 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 $cust_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 $cust_bill_event;
+
+  if ( $cust_bill_event->plandata =~ /^$option (.*)$/m ) {
+    return $1;
+  } else {
+    warn "can't parse plandata for $1";
+    return '';
+  }
+
+}
+
 =item print_text [ TIME [ , TEMPLATE ] ]
 
 Returns an text invoice, as a list of lines.
@@ -704,7 +764,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;
@@ -782,24 +844,18 @@ sub print_text {
   }
 
   #balance due
-  my $balance_due_msg = 'Balance Due';
-
-  if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $balance_due_msg .=
-      ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
-  } elsif ( $conf->config('invoice_default_terms') ) {
-    $balance_due_msg .= ' - '. $conf->config('invoice_default_terms');
-  }
+  my $balance_due_msg = $self->balance_due_msg;
 
   push @buf,['','-----------'];
   push @buf,[$balance_due_msg, $money_char. 
     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
@@ -886,9 +942,12 @@ sub print_text {
 
 }
 
-=item print_ps [ TIME [ , TEMPLATE ] ]
+=item print_latex [ TIME [ , TEMPLATE ] ]
 
-Returns an postscript invoice, as a scalar.
+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.
@@ -898,7 +957,7 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 =cut
 
 #still some false laziness w/print_text
-sub print_ps {
+sub print_latex {
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
@@ -918,43 +977,49 @@ sub print_ps {
   @buf = ();
 
   #create the template
-  my $templatefile = 'invoice_template_latex';
-  $templatefile .= "_$template" if $template;
+  $template ||= $self->_agent_template;
+  my $templatefile = 'invoice_latex';
+  my $suffix = length($template) ? "_$template" : '';
+  $templatefile .= $suffix;
   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'    => $cust_main->agent->agent,
-    'payname'  => $cust_main->payname,
-    'company'  => $cust_main->company,
-    'address1' => $cust_main->address1,
-    'address2' => $cust_main->address2,
-    'city'     => $cust_main->city,
-    'state'    => $cust_main->state,
-    'zip'      => $cust_main->zip,
-    'country'  => $cust_main->country,
-    'footer'   => <<'END', #should come from config value
-Ivan Kohler\\
-1339 Hayes St.\\
-San Francisco, CA~~94117\\
-ivan@sisd.com~~~~+1 415 462 1624\\
-Freeside - open-source billing - http://www.sisd.com/freeside\\
-END
-
+    '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') ),
   );
 
-  #$invoice_data{'footer'} =~ s/\n+$//;
-
   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_orbase('invoice_latexnotes', $suffix)
+    );
+
+  $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 )
-      ? "Purchase Order #". $cust_main->payinfo
+      ? _latex_escape("Purchase Order #". $cust_main->payinfo)
       : '~';
 
   my @line_item = ();
@@ -969,14 +1034,14 @@ END
               !~ /^%%EndDetail\s*$/                            ) {
         push @line_item, $line_item_line;
       }
-      #foreach my $line_item ( $self->_items ) {
-      foreach my $line_item ( $self->_items_pkg ) {
+      foreach my $line_item ( $self->_items ) {
+      #foreach my $line_item ( $self->_items_pkg ) {
         $invoice_data{'ref'} = $line_item->{'pkgnum'};
-        $invoice_data{'description'} = $line_item->{'description'};
+        $invoice_data{'description'} = _latex_escape($line_item->{'description'});
         if ( exists $line_item->{'ext_description'} ) {
           $invoice_data{'description'} .=
             "\\tabularnewline\n~~".
-            join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
+            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';
@@ -995,29 +1060,58 @@ END
 
       my $taxtotal = 0;
       foreach my $tax ( $self->_items_tax ) {
-        $invoice_data{'total_item'} = $tax->{'description'};
+        $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->owed ). '}';
+        '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
       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->owed - $taxtotal );
-        unshift @total_fill,
+      #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 {
@@ -1034,27 +1128,51 @@ END
     $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;
+
+  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
+  return $1;
+
+}
+
+=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(@_);
+
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
   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;
+  system("pslatex $file.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed: $!";
+  system("pslatex $file.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed: $!";
 
-  #error checking!!
-  system('pslatex', "$file.tex");
-  system('pslatex', "$file.tex");
-  #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
-  system('dvips', '-t', 'letter', "$file.dvi" );
+  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: $!\n";
+  open(POSTSCRIPT, "<$file.ps")
+    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>) {
@@ -1067,13 +1185,93 @@ END
 
 }
 
+=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(@_);
+
+  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.
+
+  system("pslatex $file.tex >/dev/null 2>&1") == 0
+    or die "pslatex $file.tex failed: $!";
+  system("pslatex $file.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 -"
+  ) == 0
+    or die "dvips | gs failed: $!";
+
+  open(PDF, "<$file.pdf")
+    or die "can't open $file.pdf: $! (error in LaTeX template?)\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_pkg );
+                : 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 ) {
@@ -1088,13 +1286,28 @@ sub _items_previous {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my @b = ();
   foreach ( @pr_cust_bill ) {
-    push @b, [
-      "Previous Balance, Invoice #". $_->invnum. 
-                 " (". time2str("%x",$_->_date). ")",
-      $money_char. sprintf("%10.2f",$_->owed)
-    ];
+    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 {
@@ -1122,18 +1335,31 @@ sub _items_cust_bill_pkg {
       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 @d = ();
-        @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 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'     => "$pkg Setup",
-          'pkgpart'         => $part_pkg->pkgpart,
+          '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,
-                               ],
+          'ext_description' => \@d,
         };
       }
 
@@ -1142,11 +1368,10 @@ sub _items_cust_bill_pkg {
           'description'     => "$pkg (" .
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
-          'pkgpart'         => $part_pkg->pkgpart,
+          #'pkgpart'         => $part_pkg->pkgpart,
           'pkgnum'          => $cust_pkg->pkgnum,
           'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
-          'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
-                                       $cust_pkg->labels          ),
+          'ext_description' => [ @ext_description,
                                  $cust_bill_pkg->details,
                                ],
         };
@@ -1189,14 +1414,18 @@ sub _items_credits {
 
     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
 
-    my $reason = substr($_->cust_credit->reason,0,32);
-    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
+    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,[
-      "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
-        $reason,
-      $money_char. sprintf("%10.2f",$_->amount)
-    ];
+    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,[
@@ -1218,10 +1447,11 @@ sub _items_payments {
 
     #something more elaborate if $_->amount ne ->cust_pay->paid ?
 
-    push @b,[
-      "Payment received ". time2str("%x",$_->cust_pay->_date ),
-      $money_char. sprintf("%10.2f",$_->amount )
-    ];
+    push @b, {
+      'description' => "Payment received ".
+                       time2str("%x",$_->cust_pay->_date ),
+      'amount'      => sprintf("%10.2f", $_->amount )
+    };
   }
 
   @b;
@@ -1237,9 +1467,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>,