changes to support new invoice template features, #28080
[freeside.git] / FS / FS / cust_bill.pm
index a747a78..85c4bac 100644 (file)
@@ -2,7 +2,7 @@ package FS::cust_bill;
 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
 
 use strict;
-use vars qw( $DEBUG $me $date_format );
+use vars qw( $DEBUG $me );
              # but NOT $conf
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
@@ -15,7 +15,6 @@ use GD::Barcode;
 use FS::UID qw( datasrc );
 use FS::Misc qw( send_email send_fax do_print );
 use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
@@ -25,12 +24,10 @@ use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
-use FS::cust_pay_batch;
 use FS::cust_bill_event;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
-use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
 use FS::bill_batch;
@@ -47,7 +44,6 @@ $me = '[FS::cust_bill]';
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   my $conf = new FS::Conf; #global
-  $date_format      = $conf->config('date_format')      || '%x'; #/YY
 } );
 
 =head1 NAME
@@ -161,7 +157,7 @@ sub notice_name {
   $self->conf->config('notice_name') || 'Invoice'
 }
 
-sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } 
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -492,7 +488,9 @@ sub cust_bill_pkg {
   qsearch(
     { 'table'    => 'cust_bill_pkg',
       'hashref'  => { 'invnum' => $self->invnum },
-      'order_by' => 'ORDER BY billpkgnum',
+      'order_by' => 'ORDER BY billpkgnum', #important?  otherwise we could use
+                                           # the AUTLOADED FK search.  or should
+                                           # that default to ORDER by the pkey?
     }
   );
 }
@@ -634,13 +632,6 @@ sub num_cust_event {
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
-=cut
-
-sub cust_main {
-  my $self = shift;
-  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-}
-
 =item cust_suspend_if_balance_over AMOUNT
 
 Suspends the customer associated with this invoice if the total amount owed on
@@ -660,57 +651,6 @@ sub cust_suspend_if_balance_over {
   }
 }
 
-=item cust_credit
-
-Depreciated.  See the cust_credited method.
-
- #Returns a list consisting of the total previous credited (see
- #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
- #outstanding credits (FS::cust_credit objects).
-
-=cut
-
-sub cust_credit {
-  use Carp;
-  croak "FS::cust_bill->cust_credit depreciated; see ".
-        "FS::cust_bill->cust_credit_bill";
-  #my $self = shift;
-  #my $total = 0;
-  #my @cust_credit = sort { $a->_date <=> $b->_date }
-  #  grep { $_->credited != 0 && $_->_date < $self->_date }
-  #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
-  #;
-  #foreach (@cust_credit) { $total += $_->credited; }
-  #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated.  See the cust_bill_pay method.
-
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
-
-=cut
-
-sub cust_pay {
-  use Carp;
-  croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
-  #my $self = shift;
-  #sort { $a->_date <=> $b->_date }
-  #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
-  #;
-}
-
-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.
@@ -1084,6 +1024,8 @@ sub generate_email {
   my %return = (
     'from'      => $args{'from'},
     'subject'   => ($args{'subject'} || $self->email_subject),
+    'custnum'   => $self->custnum,
+    'msgtype'   => 'invoice',
   );
 
   $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
@@ -1136,6 +1078,7 @@ sub generate_email {
     $alternative->attach(
       'Type'        => 'text/plain',
       'Encoding'    => 'quoted-printable',
+      'Charset'     => 'UTF-8',
       #'Encoding'    => '7bit',
       'Data'        => $data,
       'Disposition' => 'inline',
@@ -1414,7 +1357,7 @@ sub email {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::email called with positional parameters";
   }
@@ -1489,7 +1432,7 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 sub lpr_data {
   my $self = shift;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     # nobody does this anyway
     die "FS::cust_bill::lpr_data called with positional parameters";
@@ -1515,7 +1458,7 @@ sub print {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::print called with positional parameters";
   }
@@ -1550,7 +1493,7 @@ sub fax_invoice {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::fax_invoice called with positional parameters";
   }
@@ -1677,6 +1620,7 @@ sub send_csv {
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
+  # don't localize dates here, they're a defined format
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
   
@@ -1970,25 +1914,33 @@ sub print_csv {
 
   my $time = $opt{'time'} || time;
 
+  my $tracctnum = ''; #leaking out from billco-specific sections :/
   if ( $format eq 'billco' ) {
 
+    my $account_num =
+      $self->conf->config('billco-account_num', $cust_main->agentnum);
+
+    $tracctnum = $account_num eq 'display_custnum'
+                   ? $cust_main->display_custnum
+                   : $opt{'tracctnum'};
+
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
-    my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+    my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
-      foreach ( $self->_items_payments, $self->_items_credits ) ;
+      foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
     $csv->combine(
       '',                         #  1 | N/A-Leave Empty               CHAR   2
       '',                         #  2 | N/A-Leave Empty               CHAR  15
-      $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
+      $tracctnum,                 #  3 | Transaction Account No        CHAR  15
       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
@@ -2225,7 +2177,7 @@ sub print_csv {
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
-        $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
+        $tracctnum,             #  3 | Account Number             CHAR  15
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
@@ -2262,7 +2214,7 @@ sub print_csv {
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
           ($cust_bill_pkg->edate 
-            ?time2str("%x", $cust_bill_pkg->edate)
+            ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
   
@@ -2967,6 +2919,49 @@ sub _items_svc_phone_sections {
 
 }
 
+=sub _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this 
+invoice.  Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
+  my $self = shift;
+  my %opt = @_;
+
+  my $escape = $opt{escape} || sub { $_[0] };
+  my $invnum = $self->invnum;
+  my @classes = qsearch({
+      'table'     => 'usage_class',
+      'select'    => 'classnum, classname, SUM(amount) AS amount',
+      'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+                     ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+      'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+                     ' GROUP BY classnum, classname, weight'.
+                     ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+                     ' ORDER BY weight ASC',
+  });
+  my @l;
+  my $section = {
+    description   => &{$escape}($self->mt('Usage Summary')),
+    no_subtotal   => 1,
+    usage_section => 1,
+  };
+  foreach my $class (@classes) {
+    push @l, {
+      'description'     => &{$escape}($class->classname),
+      'amount'          => sprintf('%.2f', $class->amount),
+      'usage_classnum'  => $class->classnum,
+      'section'         => $section,
+    };
+  }
+  return @l;
+}
+
 sub _items_previous {
   my $self = shift;
   my $conf = $self->conf;
@@ -2975,8 +2970,8 @@ sub _items_previous {
   my @b = ();
   foreach ( @pr_cust_bill ) {
     my $date = $conf->exists('invoice_show_prior_due_date')
-               ? 'due '. $_->due_date2str($date_format)
-               : time2str($date_format, $_->_date);
+               ? 'due '. $_->due_date2str('short')
+               : $self->time2str_local('short', $_->_date);
     push @b, {
       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
       #'pkgpart'     => 'N/A',
@@ -3008,14 +3003,22 @@ sub _items_credits {
   #credits
   my @objects;
   if ( $self->conf->exists('previous_balance-payments_since') ) {
-    my $date = 0;
-    $date = $self->previous_bill->_date if $self->previous_bill;
-    @objects = qsearch('cust_credit', {
-        'custnum' => $self->custnum,
-        '_date'   => {op => '>=', value => $date},
+    if ( $opt{'template'} eq 'statement' ) {
+      # then the current bill is a "statement" (i.e. an invoice sent as
+      # a payment receipt)
+      # and in that case we want to see payments on or after THIS invoice
+      @objects = qsearch('cust_credit', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $self->_date},
+      });
+    } else {
+      my $date = 0;
+      $date = $self->previous_bill->_date if $self->previous_bill;
+      @objects = qsearch('cust_credit', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $date},
       });
-      # hard to do this in the qsearch...
-    @objects = grep { $_->_date < $self->_date } @objects;
+    }
   } else {
     @objects = $self->cust_credited;
   }
@@ -3032,7 +3035,7 @@ sub _items_credits {
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => $self->mt('Credit applied').' '.
-                       time2str($date_format,$obj->_date). $reason,
+                       $self->time2str_local('short', $obj->_date). $reason,
       'amount'      => sprintf("%.2f",$obj->amount),
     };
   }
@@ -3043,18 +3046,32 @@ sub _items_credits {
 
 sub _items_payments {
   my $self = shift;
+  my %opt = @_;
 
   my @b;
   my $detailed = $self->conf->exists('invoice_payment_details');
   my @objects;
   if ( $self->conf->exists('previous_balance-payments_since') ) {
-    my $date = 0;
-    $date = $self->previous_bill->_date if $self->previous_bill;
-    @objects = qsearch('cust_pay', {
+    # then show payments dated on/after the previous bill...
+    if ( $opt{'template'} eq 'statement' ) {
+      # then the current bill is a "statement" (i.e. an invoice sent as
+      # a payment receipt)
+      # and in that case we want to see payments on or after THIS invoice
+      @objects = qsearch('cust_pay', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $self->_date},
+      });
+    } else {
+      # the normal case: payments on or after the previous invoice
+      my $date = 0;
+      $date = $self->previous_bill->_date if $self->previous_bill;
+      @objects = qsearch('cust_pay', {
         'custnum' => $self->custnum,
         '_date'   => {op => '>=', value => $date},
       });
-    @objects = grep { $_->_date < $self->_date } @objects;
+      # and before the current bill...
+      @objects = grep { $_->_date < $self->_date } @objects;
+    }
   } else {
     @objects = $self->cust_bill_pay;
   }
@@ -3062,8 +3079,9 @@ sub _items_payments {
   foreach my $obj (@objects) {
     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
     my $desc = $self->mt('Payment received').' '.
-               time2str($date_format, $cust_pay->_date );
-    $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
+               $self->time2str_local('short', $cust_pay->_date );
+    $desc .= $self->mt(' via ') .
+             $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
       if $detailed;
 
     push @b, {
@@ -3342,6 +3360,22 @@ flag, return net invoices only
 
 =item newest_percust
 
+=item custnum
+
+Return only invoices belonging to that customer.
+
+=item cust_classnum
+
+Limit to that customer class (single value or arrayref).
+
+=item payby
+
+Limit to customers with that payment method (single value or arrayref).
+
+=item refnum
+
+Limit to customers with that advertising source.
+
 =back
 
 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
@@ -3372,13 +3406,33 @@ sub search_sql_where {
     push @search, "cust_bill.custnum = $1";
   }
 
-  #customer classnum
+  #customer classnum (false laziness w/ cust_main/Search.pm)
   if ( $param->{'cust_classnum'} ) {
-    my $classnums = $param->{'cust_classnum'};
-    $classnums = [ $classnums ] if !ref($classnums);
-    $classnums = [ grep /^\d+$/, @$classnums ];
-    push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
-      if @$classnums;
+
+    my @classnum = ref( $param->{'cust_classnum'} )
+                     ? @{ $param->{'cust_classnum'} }
+                     :  ( $param->{'cust_classnum'} );
+
+    @classnum = grep /^(\d*)$/, @classnum;
+
+    if ( @classnum ) {
+      push @search, '( '. join(' OR ', map {
+                                             $_ ? "cust_main.classnum = $_"
+                                                : "cust_main.classnum IS NULL"
+                                           }
+                                           @classnum
+                              ).
+                    ' )';
+    }
+
+  }
+
+  #payby
+  if ( $param->{payby} ) {
+    my $payby = $param->{payby};
+    $payby = [ $payby ] unless ref $payby;
+    my $payby_in = join(',', map {dbh->quote($_)} @$payby);
+    push @search, "cust_main.payby IN($payby_in)" if length($payby_in);
   }
 
   #_date