fix the same problem with processing payments & masked ACH amounts, RT#6374
[freeside.git] / FS / FS / cust_bill.pm
index 35daea7..e6d0b0d 100644 (file)
@@ -139,7 +139,49 @@ Really, don't use it.
 sub delete {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
-  $self->SUPER::delete(@_);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $table (qw(
+    cust_bill_event
+    cust_credit_bill
+    cust_bill_pay
+    cust_bill_pay
+    cust_credit_bill
+    cust_pay_batch
+    cust_bill_pay_batch
+    cust_bill_pkg
+  )) {
+
+    foreach my $linked ( $self->$table() ) {
+      my $error = $linked->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
 }
 
 =item replace OLD_RECORD
@@ -353,6 +395,16 @@ sub cust_pay {
   #;
 }
 
+sub cust_pay_batch {
+  my $self = shift;
+  qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
+}
+
+sub cust_bill_pay_batch {
+  my $self = shift;
+  qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+}
+
 =item cust_bill_pay
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
@@ -367,6 +419,8 @@ sub cust_bill_pay {
 
 =item cust_credited
 
+=item cust_credit_bill
+
 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
 
 =cut
@@ -378,6 +432,10 @@ sub cust_credited {
   ;
 }
 
+sub cust_credit_bill {
+  shift->cust_credited(@_);
+}
+
 =item tax
 
 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
@@ -733,7 +791,7 @@ sub mimebuild_pdf {
     'Encoding'    => 'base64',
     'Data'        => [ $self->print_pdf(@_) ],
     'Disposition' => 'attachment',
-    'Filename'    => 'invoice.pdf',
+    'Filename'    => 'invoice-'. $self->invnum. '.pdf',
   );
 }
 
@@ -790,13 +848,15 @@ sub send {
 
   my @invoicing_list = $self->cust_main->invoicing_list;
 
+  #$self->email_invoice($template, $invoice_from)
   $self->email($template, $invoice_from)
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
+  #$self->print_invoice($template)
   $self->print($template)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-  $self->fax($template)
+  $self->fax_invoice($template)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
@@ -828,6 +888,7 @@ sub queueable_email {
 
 }
 
+#sub email_invoice {
 sub email {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
@@ -842,10 +903,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,
     )
   );
@@ -854,6 +918,23 @@ sub email {
 
 }
 
+sub email_subject {
+  my $self = shift;
+
+  #my $template = scalar(@_) ? shift : '';
+  #per-template?
+
+  my $subject = $conf->config('invoice_subject') || '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.
@@ -877,6 +958,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
+#sub print_invoice {
 sub print {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
@@ -884,7 +966,7 @@ sub print {
   do_print $self->lpr_data($template);
 }
 
-=item fax [ TEMPLATENAME ] 
+=item fax_invoice [ TEMPLATENAME ] 
 
 Faxes this invoice.
 
@@ -892,7 +974,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
-sub fax {
+sub fax_invoice {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
 
@@ -909,6 +991,46 @@ sub fax {
 
 }
 
+=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
@@ -1212,11 +1334,7 @@ sub print_csv {
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
-    my $duedate = '';
-    if (    $conf->exists('invoice_default_terms') 
-         && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
-      $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
-    }
+    my $duedate = $self->balance_due_date;
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
@@ -1796,13 +1914,16 @@ sub print_latex {
     'date'         => time2str('%b %o, %Y', $self->_date),
     'today'        => time2str('%b %o, %Y', $today),
     'agent'        => _latex_escape($cust_main->agent->agent),
+    'agent_custid' => _latex_escape($cust_main->agent_custid),
     '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),
+    #'quantity'     => 1,
     'zip'          => _latex_escape($cust_main->zip),
+    'fax'          => _latex_escape($cust_main->fax),
     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
     'returnaddress' => $returnaddress,
@@ -1810,9 +1931,23 @@ sub print_latex {
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+    'current_charges'  => sprintf('%.2f', $self->charged ),
+    'previous_balance' => sprintf("%.2f", $pr_total),
+    'balance'      => sprintf("%.2f", $balance_due),
+    'duedate'      => $self->balance_due_date,
+    'ship_enable'  => $conf->exists('invoice-ship_address'),
+    'unitprices'   => $conf->exists('invoice-unitprice'),
   );
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
+  my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+  foreach ( qw( contact company address1 address2 city state zip country fax) ){
+    my $method = $prefix.$_;
+    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+  }
+  $invoice_data{'ship_country'} = ''
+    if ( $invoice_data{'ship_country'} eq $countrydefault );
+
   if ( $cust_main->country eq $countrydefault ) {
     $invoice_data{'country'} = '';
   } else {
@@ -1828,6 +1963,28 @@ sub print_latex {
   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
     if $DEBUG;
 
+  #do variable substitution in coupon
+  foreach my $include (qw( coupon )) {
+
+    my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
+
+    my $inc_tt = new Text::Template (
+      TYPE       => 'ARRAY',
+      SOURCE     => [ map "$_\n", @inc_src ],
+      DELIMITERS => [ '[@--', '--@]' ],
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+    unless ( $inc_tt->compile() ) {
+      my $error = "Can't compile $include template: $Text::Template::ERROR\n";
+      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+      die $error;
+    }
+
+    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+    $invoice_data{$include} =~ s/\n+$//
+  }
+
   $invoice_data{'footer'} =~ s/\n+$//;
   $invoice_data{'smallfooter'} =~ s/\n+$//;
   $invoice_data{'notes'} =~ s/\n+$//;
@@ -1851,7 +2008,7 @@ sub print_latex {
                 !~ /^%%EndDetail\s*$/                            ) {
           push @line_item, $line_item_line;
         }
-        foreach my $line_item ( $self->_items ) {
+        foreach my $line_item ( $self->_items ) { #( 'format'=>'latex' ) ) {
         #foreach my $line_item ( $self->_items_pkg ) {
           $invoice_data{'ref'} = $line_item->{'pkgnum'};
           $invoice_data{'description'} =
@@ -1863,7 +2020,9 @@ sub print_latex {
                     map _latex_escape($_), @{$line_item->{'ext_description'}}
                   );
           }
-          $invoice_data{'amount'} = $line_item->{'amount'};
+          $invoice_data{'amount'}       = $line_item->{'amount'};
+          $invoice_data{'unit_amount'}  = $line_item->{'unit_amount'};
+          $invoice_data{'quantity'}     = $line_item->{'quantity'};
           $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;
@@ -1969,6 +2128,7 @@ sub print_latex {
         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
       }
       $detail->{'amount'} = $line_item->{'amount'};
+      $detail->{'unit_amount'} = $line_item->{'unit_amount'};
       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
   
       push @detail_items, $detail;
@@ -1985,11 +2145,14 @@ sub print_latex {
     }
   
     if ( $taxtotal ) {
+      $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
       my $total = {};
       $total->{'total_item'} = 'Sub-total';
       $total->{'total_amount'} =
         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
       unshift @total_items, $total;
+    }else{
+      $invoice_data{'taxtotal'} = '0.00';
     }
   
     {
@@ -2011,22 +2174,26 @@ sub print_latex {
       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
   
       # credits
+      my $credittotal = 0;
       foreach my $credit ( $self->_items_credits ) {
         my $total;
         $total->{'total_item'} = _latex_escape($credit->{'description'});
-        #$credittotal
+        $credittotal += $credit->{'amount'};
         $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
         push @total_items, $total;
       }
+      $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
   
       # payments
+      my $paymenttotal = 0;
       foreach my $payment ( $self->_items_payments ) {
         my $total = {};
         $total->{'total_item'} = _latex_escape($payment->{'description'});
-        #$paymenttotal
+        $paymenttotal += $payment->{'amount'};
         $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
         push @total_items, $total;
       }
+      $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
   
       { 
         my $total;
@@ -2146,6 +2313,7 @@ sub print_html {
     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
     'today'        => time2str('%b %o, %Y', $today),
     'agent'        => encode_entities($cust_main->agent->agent),
+    'agent_custid' => encode_entities($cust_main->agent_custid),
     'payname'      => encode_entities($cust_main->payname),
     'company'      => encode_entities($cust_main->company),
     'address1'     => encode_entities($cust_main->address1),
@@ -2153,13 +2321,22 @@ sub print_html {
     'city'         => encode_entities($cust_main->city),
     'state'        => encode_entities($cust_main->state),
     'zip'          => encode_entities($cust_main->zip),
+    'fax'          => encode_entities($cust_main->fax),
     'terms'        => $conf->config('invoice_default_terms')
                       || 'Payable upon receipt',
     'cid'          => $cid,
     'template'     => $template,
+    'ship_enable'  => $conf->exists('invoice-ship_address'),
+    'unitprices'   => $conf->exists('invoice-unitprice'),
 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
   );
 
+  my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+  foreach ( qw( contact company address1 address2 city state zip country fax) ){
+    my $method = $prefix.$_;
+    $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
+  }
+
   if (
          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
@@ -2172,6 +2349,7 @@ sub print_html {
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+\}//;
+                       s/\\([&])/$1/g;
                        $_;
                      }
                      $conf->config_orbase( 'invoice_latexreturnaddress',
@@ -2353,6 +2531,16 @@ sub balance_due_msg {
   $msg;
 }
 
+sub balance_due_date {
+  my $self = shift;
+  my $duedate = '';
+  if (    $conf->exists('invoice_default_terms') 
+       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+  }
+  $duedate;
+}
+
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
@@ -2362,7 +2550,18 @@ 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 {
@@ -2438,6 +2637,8 @@ sub _items_cust_bill_pkg {
     my $cust_pkg = $cust_bill_pkg->cust_pkg;
 
     my $desc = $cust_bill_pkg->desc;
+    $desc = substr($desc, 0, 50). '...'
+      if $format eq 'latex' && length($desc) > 50;
 
     my %details_opt = ( 'format'          => $format,
                         'escape_function' => $escape_function,
@@ -2450,8 +2651,11 @@ sub _items_cust_bill_pkg {
         my $description = $desc;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
 
-        my @d = map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short($self->_date);
+        my @d = ();
+        push @d, map &{$escape_function}($_),
+                     $cust_pkg->h_labels_short($self->_date)
+          unless $cust_pkg->part_pkg->hide_svc_detail;
+
         push @d, $cust_bill_pkg->details(%details_opt)
           if $cust_bill_pkg->recur == 0;
 
@@ -2460,6 +2664,8 @@ sub _items_cust_bill_pkg {
           #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,
         };
       }
@@ -2468,16 +2674,21 @@ sub _items_cust_bill_pkg {
 
         my $description = $desc;
         unless ( $conf->exists('disable_line_item_date_ranges') ) {
-          $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
-                   " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+          $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
+                          " - ". time2str("%x", $cust_bill_pkg->edate). ")";
         }
 
+        my @d = ();
+
         #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),
+        push @d, map &{$escape_function}($_),
+                     $cust_pkg->h_labels_short($self->_date)
+                                               #$cust_bill_pkg->edate,
+                                               #$cust_bill_pkg->sdate),
+          unless $cust_pkg->part_pkg->hide_svc_detail
+              || $cust_bill_pkg->itemdesc;
+
         push @d, $cust_bill_pkg->details(%details_opt);
 
         push @b, {
@@ -2485,8 +2696,9 @@ sub _items_cust_bill_pkg {
           #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,
-
         };
 
       }
@@ -2576,7 +2788,7 @@ sub _items_payments {
 
 =over 4
 
-=item reprint
+=item process_reprint
 
 =cut
 
@@ -2584,7 +2796,7 @@ sub process_reprint {
   process_re_X('print', @_);
 }
 
-=item reemail
+=item process_reemail
 
 =cut
 
@@ -2592,7 +2804,7 @@ sub process_reemail {
   process_re_X('email', @_);
 }
 
-=item refax
+=item process_refax
 
 =cut
 
@@ -2600,6 +2812,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;
@@ -2643,6 +2871,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;