diff options
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/Cron/bill.pm | 141 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 116 | ||||
-rwxr-xr-x | FS/bin/freeside-daily | 27 |
3 files changed, 209 insertions, 75 deletions
diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index df446d882..8c0f8351c 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -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; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index d3c0886e1..a5bf36de7 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -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) = @_; diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index d5748d6b9..d76cb37b9 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -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 |