diff options
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/Cron/bill.pm | 193 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 5 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 51 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 5 |
4 files changed, 164 insertions, 90 deletions
diff --git a/FS/FS/Cron/bill.pm b/FS/FS/Cron/bill.pm index 29f5f36c9..61b47355a 100644 --- a/FS/FS/Cron/bill.pm +++ b/FS/FS/Cron/bill.pm @@ -13,10 +13,16 @@ use FS::part_event; use FS::part_event_condition; @ISA = qw( Exporter ); -@EXPORT_OK = qw ( bill ); +@EXPORT_OK = qw ( bill bill_where ); -sub bill { +#freeside-daily %opt: +# -s: re-charge setup fees +# -v: enable debugging +# -l: debugging level +# -m: Experimental multi-process mode uses the job queue for multi-process and/or multi-machine billing. +# -r: Multi-process mode dry run option +sub bill { my %opt = @_; my $check_freq = $opt{'check_freq'} || '1d'; @@ -24,91 +30,24 @@ sub bill { 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.archived != 'Y' OR archived IS NULL )"; #disable? - - 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_event search clause - ### - #we're at now now (and later). - my($time)= $opt{'d'} ? str2time($opt{'d'}) : $^T; - $time += $opt{'y'} * 86400 if $opt{'y'}; - - my $invoice_time = $opt{'n'} ? $^T : $time; + $opt{'time'} = $opt{'d'} ? str2time($opt{'d'}) : $^T; + $opt{'time'} += $opt{'y'} * 86400 if $opt{'y'}; - # select * from cust_main where - my $where_pkg = <<"END"; - 0 < ( select count(*) from cust_pkg - where cust_main.custnum = cust_pkg.custnum - and ( cancel is null or cancel = 0 ) - and ( setup is null or setup = 0 - or bill is null or bill <= $time - or ( expire is not null and expire <= $^T ) - or ( adjourn is not null and adjourn <= $^T ) - ) - ) -END - - my $where_event = join(' OR ', map { - my $eventtable = $_; - - my $join = FS::part_event_condition->join_conditions_sql( $eventtable ); - my $where = FS::part_event_condition->where_conditions_sql( $eventtable, - 'time'=>$time, - ); - - my $are_part_event = - "0 < ( SELECT COUNT(*) FROM part_event $join - WHERE check_freq = '$check_freq' - AND eventtable = '$eventtable' - AND ( disabled = '' OR disabled IS NULL ) - AND $where - ) - "; - - if ( $eventtable eq 'cust_main' ) { - $are_part_event; - } else { - "0 < ( SELECT COUNT(*) FROM $eventtable - WHERE cust_main.custnum = $eventtable.custnum - AND $are_part_event - ) - "; - } - - } FS::part_event->eventtables); - - push @search, "( $where_pkg OR $where_event )"; + $opt{'invoice_time'} = $opt{'n'} ? $^T : $opt{'time'}; ### # get a list of custnums ### - warn "searching for customers:\n". join("\n", @search). "\n" - if $opt{'v'} || $opt{'l'}; - my $cursor_dbh = dbh->clone; $cursor_dbh->do( "DECLARE cron_bill_cursor CURSOR FOR ". - " SELECT custnum FROM cust_main WHERE ". join(' AND ', @search) + " SELECT custnum FROM cust_main WHERE ". bill_where( %opt ) ) or die $cursor_dbh->errstr; while ( 1 ) { @@ -129,8 +68,8 @@ END foreach my $custnum ( @custnums ) { my %args = ( - 'time' => $time, - 'invoice_time' => $invoice_time, + 'time' => $opt{'time'}, + 'invoice_time' => $opt{'invoice_time'}, 'actual_time' => $^T, #when freeside-bill was started #(not, when using -m, freeside-queued) 'check_freq' => $check_freq, @@ -175,4 +114,108 @@ END } +# freeside-daily %opt: +# -d: Pretend it's 'date'. Date is in any format Date::Parse is happy with, +# but be careful. +# +# -y: In addition to -d, which specifies an absolute date, the -y switch +# specifies an offset, in days. For example, "-y 15" would increment the +# "pretend date" 15 days from whatever was specified by the -d switch +# (or now, if no -d switch was given). +# +# -n: When used with "-d" and/or "-y", specifies that invoices should be dated +# with today's date, irregardless of the pretend date used to pre-generate +# the invoices. +# +# -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>) +# +# -a: Only process customers with the specified agentnum +# +# -v: enable debugging +# +# -l: debugging level + +sub bill_where { + my( %opt ) = @_; + + my $time = $opt{'time'}; + my $invoice_time = $opt{'invoice_time'}; + + my $check_freq = $opt{'check_freq'} || '1d'; + + my @search = (); + + push @search, "( cust_main.archived != 'Y' OR archived IS NULL )"; #disable? + + 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_event search clause + ### + + # select * from cust_main where + my $where_pkg = <<"END"; + EXISTS( + SELECT 1 FROM cust_pkg + WHERE cust_main.custnum = cust_pkg.custnum + AND ( cancel IS NULL OR cancel = 0 ) + AND ( ( ( setup IS NULL OR setup = 0 ) + AND ( start_date IS NULL OR start_date = 0 + OR ( start_date IS NOT NULL AND start_date <= $^T ) + ) + ) + OR bill IS NULL OR bill <= $time + OR ( expire IS NOT NULL AND expire <= $^T ) + OR ( adjourn IS NOT NULL AND adjourn <= $^T ) + ) + ) +END + + my $where_event = join(' OR ', map { + my $eventtable = $_; + + my $join = FS::part_event_condition->join_conditions_sql( $eventtable ); + my $where = FS::part_event_condition->where_conditions_sql( $eventtable, + 'time'=>$time, + ); + + my $are_part_event = + "EXISTS ( SELECT 1 FROM part_event $join + WHERE check_freq = '$check_freq' + AND eventtable = '$eventtable' + AND ( disabled = '' OR disabled IS NULL ) + AND $where + ) + "; + + if ( $eventtable eq 'cust_main' ) { + $are_part_event; + } else { + "EXISTS ( SELECT 1 FROM $eventtable + WHERE cust_main.custnum = $eventtable.custnum + AND $are_part_event + ) + "; + } + + } FS::part_event->eventtables); + + push @search, "( $where_pkg OR $where_event )"; + + warn "searching for customers:\n". join("\n", @search). "\n" + if $opt{'v'} || $opt{'l'}; + + join(' AND ', @search); + +} + 1; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 4351f2876..3c203b9d1 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1085,6 +1085,7 @@ sub tables_hashref { 'pkgpart', 'int', '', '', '', '', 'locationnum', 'int', 'NULL', '', '', '', 'otaker', 'varchar', '', 32, '', '', + 'start_date', @date_type, '', '', 'setup', @date_type, '', '', 'bill', @date_type, '', '', 'last_bill', @date_type, '', '', @@ -1102,8 +1103,8 @@ sub tables_hashref { 'primary_key' => 'pkgnum', 'unique' => [], 'index' => [ ['custnum'], ['pkgpart'], [ 'locationnum' ], - ['setup'], ['last_bill'], ['bill'], ['susp'], ['adjourn'], - ['expire'], ['cancel'], + [ 'start_date' ], ['setup'], ['last_bill'], ['bill'], + ['susp'], ['adjourn'], ['expire'], ['cancel'], ['change_date'], ], }, diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index e5f289ca3..5768bebf9 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -9,6 +9,7 @@ use Safe; use Carp; use Exporter; use Scalar::Util qw( blessed ); +use List::Util qw( min ); use Time::Local qw(timelocal); use Data::Dumper; use Tie::IxHash; @@ -2056,6 +2057,18 @@ sub unsuspended_pkgs { grep { ! $_->susp } $self->ncancelled_pkgs; } +=item next_bill_date + +Returns the next date this customer will be billed, as a UNIX timestamp, or +undef if no active package has a next bill date. + +=cut + +sub next_bill_date { + my $self = shift; + min( map $_->get('bill'), grep $_->get('bill'), $self->unsuspended_pkgs ); +} + =item num_cancelled_pkgs Returns the number of cancelled packages (see L<FS::cust_pkg>) for this @@ -2760,14 +2773,19 @@ sub _make_lines { my $setup = 0; my $unitsetup = 0; - if ( ! $cust_pkg->setup && - ( - ( $conf->exists('disable_setup_suspended_pkgs') && - ! $cust_pkg->getfield('susp') - ) || ! $conf->exists('disable_setup_suspended_pkgs') - ) - || $options{'resetup'} - ) { + if ( $options{'resetup'} + || ( ! $cust_pkg->setup + && ( ! $cust_pkg->start_date + || $cust_pkg->start_date <= $time + ) + && ( ! $conf->exists('disable_setup_suspended_pkgs') + || ( $conf->exists('disable_setup_suspended_pkgs') && + ! $cust_pkg->getfield('susp') + ) + ) + ) + ) + { warn " bill setup\n" if $DEBUG > 1; $lineitems++; @@ -2783,6 +2801,9 @@ sub _make_lines { #do need it, but it won't get written to the db #|| $cust_pkg->pkgpart != $real_pkgpart; + $cust_pkg->setfield('start_date', '') + if $cust_pkg->start_date; + } ### @@ -6727,12 +6748,14 @@ the error, otherwise returns false. sub charge { my $self = shift; - my ( $amount, $quantity, $pkg, $comment, $classnum, $additional ); + my ( $amount, $quantity, $start_date, $classnum ); + my ( $pkg, $comment, $additional ); my ( $setuptax, $taxclass ); #internal taxes my ( $taxproduct, $override ); #vendor (CCH) taxes if ( ref( $_[0] ) ) { $amount = $_[0]->{amount}; $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1; + $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : ''; $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge'; $comment = exists($_[0]->{comment}) ? $_[0]->{comment} : '$'. sprintf("%.2f",$amount); @@ -6742,9 +6765,10 @@ sub charge { $additional = $_[0]->{additional}; $taxproduct = $_[0]->{taxproductnum}; $override = { '' => $_[0]->{tax_override} }; - }else{ + } else { $amount = shift; $quantity = 1; + $start_date = ''; $pkg = @_ ? shift : 'One-time charge'; $comment = @_ ? shift : '$'. sprintf("%.2f",$amount); $setuptax = ''; @@ -6802,9 +6826,10 @@ sub charge { } my $cust_pkg = new FS::cust_pkg ( { - 'custnum' => $self->custnum, - 'pkgpart' => $pkgpart, - 'quantity' => $quantity, + 'custnum' => $self->custnum, + 'pkgpart' => $pkgpart, + 'quantity' => $quantity, + 'start_date' => $start_date, } ); $error = $cust_pkg->insert; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 881e005de..d2f0690eb 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -122,6 +122,10 @@ Billing item definition (see L<FS::part_pkg>) Optional link to package location (see L<FS::location>) +=item start_date + +date + =item setup date @@ -479,6 +483,7 @@ sub check { || $self->ut_foreign_key('custnum', 'cust_main', 'custnum') || $self->ut_numbern('pkgpart') || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum') + || $self->ut_numbern('start_date') || $self->ut_numbern('setup') || $self->ut_numbern('bill') || $self->ut_numbern('susp') |