backport freeside-daily -m and cust_main::bill_and_collect to 1.7, RT#4412
authorivan <ivan>
Fri, 17 Apr 2009 12:08:49 +0000 (12:08 +0000)
committerivan <ivan>
Fri, 17 Apr 2009 12:08:49 +0000 (12:08 +0000)
FS/FS/Cron/bill.pm
FS/FS/cust_main.pm
FS/bin/freeside-daily

index df446d8..8c0f835 100644 (file)
@@ -4,7 +4,8 @@ use strict;
 use vars qw( @ISA @EXPORT_OK );
 use Exporter;
 use Date::Parse;
-use FS::Record qw(qsearch qsearchs);
+use FS::UID qw(dbh);
+use FS::Record qw(qsearchs);
 use FS::cust_main;
 
 @ISA = qw( Exporter );
@@ -14,12 +15,30 @@ sub bill {
 
   my %opt = @_;
 
-  $FS::cust_main::DEBUG = 1 if $opt{'v'};
-  
-  my %search = ();
-  $search{'payby'}    = $opt{'p'} if $opt{'p'};
-  $search{'agentnum'} = $opt{'a'} if $opt{'a'};
-  
+  my $debug = 0;
+  $debug = 1 if $opt{'v'};
+  $debug = $opt{'l'} if $opt{'l'};
+  $FS::cust_main::DEBUG = $debug;
+  #$FS::cust_event::DEBUG = $opt{'l'} if $opt{'l'};
+
+  my @search = ();
+
+  push @search, "cust_main.payby    = '". $opt{'p'}. "'"
+    if $opt{'p'};
+  push @search, "cust_main.agentnum =  ". $opt{'a'}
+    if $opt{'a'};
+
+  if ( @ARGV ) {
+    push @search, "( ".
+      join(' OR ', map "cust_main.custnum = $_", @ARGV ).
+    " )";
+  }
+
+  ###
+  # generate where_pkg / where_bill_event search clause (1.7-style)
+  ###
+
   #we're at now now (and later).
   my($time)= $opt{'d'} ? str2time($opt{'d'}) : $^T;
   $time += $opt{'y'} * 86400 if $opt{'y'};
@@ -68,71 +87,57 @@ END
         )
 END
   
-  my $extra_sql = ( scalar(%search) ? ' AND ' : ' WHERE ' ). "( $where_pkg OR $where_bill_event )";
+  push @search, "( $where_pkg OR $where_bill_event )";
+
+  ###
+  # get a list of custnums
+  ###
+
+  warn "searching for customers:\n". join("\n", @search). "\n"
+    if $opt{'v'} || $opt{'l'};
+
+  my $sth = dbh->prepare(
+    "SELECT custnum FROM cust_main".
+    " WHERE ". join(' AND ', @search)
+  ) or die dbh->errstr;
+
+  $sth->execute or die $sth->errstr;
+
+  my @custnums = map { $_->[0] } @{ $sth->fetchall_arrayref };
+
+  ###
+  # for each custnum, queue or make one customer object and bill
+  # (one at a time, to reduce memory footprint with large #s of customers)
+  ###
   
-  my @cust_main;
-  if ( @ARGV ) {
-    @cust_main = map { qsearchs('cust_main', { custnum => $_, %search } ) } @ARGV
-  } else {
-    @cust_main = qsearch('cust_main', \%search, '', $extra_sql );
-  }
-  ;
+  foreach my $custnum ( @custnums ) {
   
-  my($cust_main,%saw);
-  foreach $cust_main ( @cust_main ) {
+    my %args = (
+        'time'         => $time,
+        'invoice_time' => $invoice_time,
+        'actual_time'  => $^T, #when freeside-bill was started
+                               #(not, when using -m, freeside-queued)
+        'resetup'      => ( $opt{'s'} ? $opt{'s'} : 0 ),
+    );
+
+    if ( $opt{'m'} ) {
+
+      #add job to queue that calls bill_and_collect with options
+      my $queue = new FS::queue {
+        'job'    => 'FS::cust_main::queued_bill',
+        'secure' => 'Y',
+      };
+      my $error = $queue->insert( 'custnum'=>$custnum, %args );
+
+    } else {
+
+      my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } );
+      $cust_main->bill_and_collect( %args, 'debug' => $debug );
 
-    my $custnum = $cust_main->custnum;
-  
-    # $^T not $time because -d is for pre-printing invoices
-    foreach my $cust_pkg (
-      grep { $_->expire && $_->expire <= $^T } $cust_main->ncancelled_pkgs
-    ) {
-      my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
-      my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
-                                             'reason_otaker' => $cpr->otaker
-                                           )
-                                         : ()
-                                   );
-      warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
-           " for custnum $custnum: $error"
-        if $error;
-    }
-    # $^T not $time because -d is for pre-printing invoices
-    foreach my $cust_pkg (
-      grep { (    $_->part_pkg->is_prepaid && $_->bill && $_->bill < $^T
-               || $_->adjourn && $_->adjourn <= $^T
-             )
-             && ! $_->susp
-           }
-           $cust_main->ncancelled_pkgs
-    ) {
-      my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
-        if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
-      my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
-                                              'reason_otaker' => $cpr->otaker
-                                            )
-                                          : ()
-                                    );
-      warn "Error suspending package ". $cust_pkg->pkgnum.
-           " for custnum $custnum: $error"
-        if $error;
     }
-  
-    my $error = $cust_main->bill( 'time'         => $time,
-                                  'invoice_time' => $invoice_time,
-                                  'resetup'      => $opt{'s'},
-                                );
-    warn "Error billing, custnum $custnum: $error" if $error;
-  
-    $error = $cust_main->apply_payments_and_credits;
-    warn "Error applying payments and credits, custnum $custnum: $error"
-      if $error;
-  
-    $error = $cust_main->collect( 'invoice_time' => $time,
-                                  'freq'         => $opt{'freq'},
-                                );
-    warn "Error collecting, custnum $custnum: $error" if $error;
-  
+
   }
 
 }
+
+1;
index d3c0886..a5bf36d 100644 (file)
@@ -1897,6 +1897,112 @@ sub agent {
   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
 }
 
+=item bill_and_collect 
+
+Cancels and suspends any packages due, generates bills, applies payments and
+cred
+
+Warns on errors (Does not currently: If there is an error, returns the error, otherwise returns false.)
+
+Options are passed as name-value pairs.  Currently available options are:
+
+=over 4
+
+=item time
+
+Bills the customer as if it were that time.  Specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.  For example:
+
+ use Date::Parse;
+ ...
+ $cust_main->bill( 'time' => str2time('April 20th, 2001') );
+
+=item invoice_time
+
+Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
+
+=item resetup
+
+If set true, re-charges setup fees.
+
+=item debug
+
+Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
+
+=back
+
+=cut
+
+sub bill_and_collect {
+  my( $self, %options ) = @_;
+
+  ###
+  # cancel packages
+  ###
+
+  #$options{actual_time} not $options{time} because freeside-daily -d is for
+  #pre-printing invoices
+  my @cancel_pkgs = grep { $_->expire && $_->expire <= $options{actual_time} }
+                         $self->ncancelled_pkgs;
+
+  foreach my $cust_pkg ( @cancel_pkgs ) {
+    my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
+    my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
+                                           'reason_otaker' => $cpr->otaker
+                                         )
+                                       : ()
+                                 );
+    warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
+         " for custnum ". $self->custnum. ": $error"
+      if $error;
+  }
+
+  ###
+  # suspend packages
+  ###
+
+  #$options{actual_time} not $options{time} because freeside-daily -d is for
+  #pre-printing invoices
+  my @susp_pkgs = 
+    grep { ! $_->susp
+           && (    (    $_->part_pkg->is_prepaid
+                     && $_->bill
+                     && $_->bill < $options{actual_time}
+                   )
+                || (    $_->adjourn
+                    && $_->adjourn <= $options{actual_time}
+                  )
+              )
+         }
+         $self->ncancelled_pkgs;
+
+  foreach my $cust_pkg ( @susp_pkgs ) {
+    my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
+      if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
+    my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
+                                            'reason_otaker' => $cpr->otaker
+                                          )
+                                        : ()
+                                  );
+
+    warn "Error suspending package ". $cust_pkg->pkgnum.
+         " for custnum ". $self->custnum. ": $error"
+      if $error;
+  }
+
+  ###
+  # bill and collect
+  ###
+
+  my $error = $self->bill( %options );
+  warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
+
+  $self->apply_payments_and_credits;
+
+  $error = $self->collect( %options );
+  warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
+
+}
+
 =item bill OPTIONS
 
 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
@@ -6280,6 +6386,16 @@ sub _agent_plandata {
 
 }
 
+sub queued_bill {
+  ## actual sub, not a method, designed to be called from the queue.
+  ## sets up the customer, and calls the bill_and_collect
+  my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
+  my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
+      $cust_main->bill_and_collect(
+        %args,
+      );
+}
+
 sub _upgrade_data { #class method
   my ($class, %opts) = @_;
 
index d5748d6..d76cb37 100755 (executable)
@@ -6,7 +6,7 @@ use FS::UID qw(adminsuidsetup);
 
 &untaint_argv; #what it sounds like  (eww)
 use vars qw(%opt);
-getopts("p:a:d:vsy:n", \%opt);
+getopts("p:a:d:vl:sy:nmk", \%opt);
 
 my $user = shift or die &usage;
 adminsuidsetup $user;
@@ -14,12 +14,19 @@ adminsuidsetup $user;
 use FS::Cron::bill qw(bill);
 bill(%opt);
 
-use FS::Cron::notify qw(notify_flat_delay);
-notify_flat_delay(%opt);
+#what to do about the below when using -m?  that is the question.
 
-use FS::Cron::vacuum qw(vacuum);
-vacuum();
+unless ( $opt{k} ) {
 
+  use FS::Cron::notify qw(notify_flat_delay);
+  notify_flat_delay(%opt);
+
+  use FS::Cron::vacuum qw(vacuum);
+  vacuum();
+
+}
+
+#you can skip this just by not having the config
 use FS::Cron::backup qw(backup_scp);
 backup_scp();
 
@@ -37,7 +44,7 @@ sub untaint_argv {
 }
 
 sub usage {
-  die "Usage:\n\n  freeside-daily [ -d 'date' ] user [ custnum custnum ... ]\n";
+  die "Usage:\n\n  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] [ -l level ] [ -m ] [ -k ] user [ custnum custnum ... ]\n";
 }
 
 ###
@@ -50,7 +57,7 @@ freeside-daily - Run daily billing and invoice collection events.
 
 =head1 SYNOPSIS
 
-  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] user [ custnum custnum ... ]
+  freeside-daily [ -d 'date' ] [ -y days ] [ -p 'payby' ] [ -a agentnum ] [ -s ] [ -v ] [ -l level ] [ -m ] [ -k ] user [ custnum custnum ... ]
 
 =head1 DESCRIPTION
 
@@ -80,6 +87,12 @@ the bill and collect methods of a cust_main object.  See L<FS::cust_main>.
 
   -v: enable debugging
 
+  -l: debugging level
+
+  -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing.
+
+  -k: skip notify_flat_delay and vacuum
+
 user: From the mapsecrets file - see config.html from the base documentation
 
 custnum: if one or more customer numbers are specified, only bills those