X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling.pm;h=608dbe15e0c9ca1eb5df69abcc6fcba982810be9;hb=9be8cf75b8e510fb0e6489f90188d391b861b247;hp=20986b0c8e96ae3e33c701639d79448b96ecf225;hpb=6fd2fa88c4376e5382cb8bc55248f35b08f4f9e6;p=freeside.git diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 20986b0c8..608dbe15e 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -7,6 +7,7 @@ use Data::Dumper; use List::Util qw( min ); use FS::UID qw( dbh ); use FS::Record qw( qsearch qsearchs dbdef ); +use FS::Misc::DateTime qw( day_end ); use FS::cust_bill; use FS::cust_bill_pkg; use FS::cust_bill_pkg_display; @@ -20,7 +21,7 @@ use FS::cust_bill_pkg_tax_rate_location; use FS::part_event; use FS::part_event_condition; use FS::pkg_category; -use POSIX; +use FS::Log; # 1 is mostly method/subroutine entry and options # 2 traces progress of some operations @@ -104,6 +105,9 @@ options of those methods are also available. sub bill_and_collect { my( $self, %options ) = @_; + my $log = FS::Log->new('bill_and_collect'); + $log->debug('start', object => $self, agentnum => $self->agentnum); + my $error; #$options{actual_time} not $options{time} because freeside-daily -d is for @@ -112,8 +116,13 @@ sub bill_and_collect { $options{'actual_time'} ||= time; my $job = $options{'job'}; + my $actual_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $options{actual_time} ) + : $options{actual_time} + ); + $job->update_statustext('0,cleaning expired packages') if $job; - $error = $self->cancel_expired_pkgs( $self->day_end( $options{actual_time} ) ); + $error = $self->cancel_expired_pkgs( $actual_time ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -121,7 +130,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->suspend_adjourned_pkgs( $self->day_end( $options{actual_time} ) ); + $error = $self->suspend_adjourned_pkgs( $actual_time ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -129,6 +138,14 @@ sub bill_and_collect { else { warn $error; } } + $error = $self->unsuspend_resumed_pkgs( $actual_time ); + if ( $error ) { + $error = "Error resuming custnum ".$self->custnum. ": $error"; + if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } + elsif ( $options{fatal} ) { die $error; } + else { warn $error; } + } + $job->update_statustext('20,billing packages') if $job; $error = $self->bill( %options ); if ( $error ) { @@ -160,24 +177,12 @@ sub bill_and_collect { } } $job->update_statustext('100,finished') if $job; + $log->debug('finish', object => $self, agentnum => $self->agentnum); ''; } -sub day_end { - # XXX: sometimes "incorrect" if crossing DST boundaries? - - my $self = shift; - my $time = shift; - - return $time unless $conf->exists('next-bill-ignore-time'); - - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = - localtime($time); - mktime(59,59,23,$mday,$mon,$year,$wday,$yday,$isdst); -} - sub cancel_expired_pkgs { my ( $self, $time, %options ) = @_; @@ -197,7 +202,7 @@ sub cancel_expired_pkgs { push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error; } - scalar(@errors) ? join(' / ', @errors) : ''; + join(' / ', @errors); } @@ -239,7 +244,25 @@ sub suspend_adjourned_pkgs { push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error; } - scalar(@errors) ? join(' / ', @errors) : ''; + join(' / ', @errors); + +} + +sub unsuspend_resumed_pkgs { + my ( $self, $time, %options ) = @_; + + my @unsusp_pkgs = $self->ncancelled_pkgs( { + 'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time " + } ); + + my @errors = (); + + foreach my $cust_pkg ( @unsusp_pkgs ) { + my $error = $cust_pkg->unsuspend( 'time' => $time ); + push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error; + } + + join(' / ', @errors); } @@ -266,7 +289,9 @@ charges, etc. =item freq_override If set, then override the normal frequency and look for a part_pkg_discount -to take at that frequency. +to take at that frequency. This is appropriate only when the normal +frequency for all packages is monthly, and is an error otherwise. Use +C to limit the set of packages included in billing. =item time @@ -331,6 +356,11 @@ sub bill { my $time = $options{'time'} || time; my $invoice_time = $options{'invoice_time'} || $time; + my $cmp_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $time ) + : $time + ); + $options{'not_pkgpart'} ||= {}; $options{'not_pkgpart'} = { map { $_ => 1 } split(/\s*,\s*/, $options{'not_pkgpart'}) @@ -393,7 +423,7 @@ sub bill { next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart}; - warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1; + warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG; #? to avoid use of uninitialized value errors... ? $cust_pkg->setfield('bill', '') @@ -418,7 +448,7 @@ sub bill { my $next_bill = $cust_pkg->getfield('bill') || 0; my $error; - while ( $next_bill <= $time ) { + while ( $next_bill <= $cmp_time ) { $error = $self->_make_lines( 'part_pkg' => $part_pkg, 'cust_pkg' => $cust_pkg, @@ -431,11 +461,17 @@ sub bill { 'real_pkgpart' => $real_pkgpart, 'options' => \%options, ); - # Stop if anything goes wrong, or if we're not incrementing - # the bill date. + + # Stop if anything goes wrong last if $error; + + # or if we're not incrementing the bill date. last if ($cust_pkg->getfield('bill') || 0) == $next_bill; + $next_bill = $cust_pkg->getfield('bill') || 0; + + #stop if -o was passed to freeside-daily + last if $options{'one_recur'}; } if ($error) { $dbh->rollback if $oldAutoCommit && !$options{no_commit}; @@ -772,9 +808,9 @@ sub calculate_taxes { #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg; foreach my $tax ( keys %$taxlisthash ) { - foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) { - next unless ref($_) eq 'FS::cust_bill_pkg'; - + foreach ( @{ $taxlisthash->{$tax} }[1 .. scalar(@{ $taxlisthash->{$tax}}) - 1] ) { + next unless ref($_) eq 'FS::cust_bill_pkg'; #IS needed for CCH tax-on-tax + my @cust_tax_exempt_pkg = splice( @{ $_->_cust_tax_exempt_pkg } ); next unless @cust_tax_exempt_pkg; #just avoiding the prob when irrelevant? @@ -859,30 +895,41 @@ sub _make_lines { my $time = $params{'time'} or die "no time specified"; my (%options) = %{$params{options}}; + if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) { + # this should never happen + die 'freq_override billing attempted on non-monthly package '. + $cust_pkg->pkgnum; + } + my $dbh = dbh; my $real_pkgpart = $params{real_pkgpart}; my %hash = $cust_pkg->hash; my $old_cust_pkg = new FS::cust_pkg \%hash; my @details = (); - my @discounts = (); my $lineitems = 0; $cust_pkg->pkgpart($part_pkg->pkgpart); + my $cmp_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $time ) + : $time + ); + ### # bill setup ### my $setup = 0; my $unitsetup = 0; - my %setup_param = (); + my @setup_discounts = (); + my %setup_param = ( 'discounts' => \@setup_discounts ); if ( ! $options{recurring_only} and ! $options{cancel} and ( $options{'resetup'} || ( ! $cust_pkg->setup && ( ! $cust_pkg->start_date - || $cust_pkg->start_date <= $self->day_end($time) + || $cust_pkg->start_date <= $cmp_time ) && ( ! $conf->exists('disable_setup_suspended_pkgs') || ( $conf->exists('disable_setup_suspended_pkgs') && @@ -923,11 +970,15 @@ sub _make_lines { #XXX unit stuff here too my $recur = 0; my $unitrecur = 0; + my @recur_discounts = (); my $sdate; if ( ! $cust_pkg->start_date - and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) ) + and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1) + || ( $part_pkg->option('suspend_bill', 1) ) + && ! $cust_pkg->option('no_suspend_bill',1) + ) and - ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $self->day_end($time) ) + ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time ) || ( $part_pkg->plan eq 'voip_cdr' && $part_pkg->option('bill_every_call') ) @@ -951,16 +1002,16 @@ sub _make_lines { #over two params! lets at least switch to a hashref for the rest... my $increment_next_bill = ( $part_pkg->freq ne '0' - && ( $cust_pkg->getfield('bill') || 0 ) <= $self->day_end($time) + && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time && !$options{cancel} ); - my %param = ( 'precommit_hooks' => $precommit_hooks, + my %param = ( %setup_param, + 'precommit_hooks' => $precommit_hooks, 'increment_next_bill' => $increment_next_bill, - 'discounts' => \@discounts, + 'discounts' => \@recur_discounts, 'real_pkgpart' => $real_pkgpart, 'freq_override' => $options{freq_override} || '', 'setup_fee' => 0, - %setup_param, ); my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur'; @@ -1046,8 +1097,10 @@ sub _make_lines { return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum; } - my $discount_show_always = ($recur == 0 && scalar(@discounts) - && $conf->exists('discount-show-always')); + my $discount_show_always = $conf->exists('discount-show-always') + && ( ($setup == 0 && scalar(@setup_discounts)) + || ($recur == 0 && scalar(@recur_discounts)) + ); if ( $setup != 0 || $recur != 0 @@ -1076,12 +1129,17 @@ sub _make_lines { 'unitrecur' => $unitrecur, 'quantity' => $cust_pkg->quantity, 'details' => \@details, - 'discounts' => \@discounts, + 'discounts' => [ @setup_discounts, @recur_discounts ], 'hidden' => $part_pkg->hidden, 'freq' => $part_pkg->freq, }; - if ( $part_pkg->recur_temporality eq 'preceding' ) { + if ( $part_pkg->option('prorate_defer_bill',1) + and !$hash{last_bill} ) { + # both preceding and upcoming, technically + $cust_bill_pkg->sdate( $cust_pkg->setup ); + $cust_bill_pkg->edate( $cust_pkg->bill ); + } elsif ( $part_pkg->recur_temporality eq 'preceding' ) { $cust_bill_pkg->sdate( $hash{last_bill} ); $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1 $cust_bill_pkg->edate( $time ) if $options{cancel}; @@ -1107,6 +1165,10 @@ sub _make_lines { return $error if $error; } + $cust_bill_pkg->set_display( part_pkg => $part_pkg, + real_pkgpart => $real_pkgpart, + ); + push @$cust_bill_pkgs, $cust_bill_pkg; } #if $setup != 0 || $recur != 0 @@ -1161,7 +1223,7 @@ sub _handle_taxes { } else { - my @loc_keys = qw( city county state country ); + my @loc_keys = qw( district city county state country ); my %taxhash; if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) { my $cust_location = $cust_pkg->cust_location; @@ -1172,13 +1234,15 @@ sub _handle_taxes { ? 'ship_' : ''; %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys; + # special case--there's no 'ship_district' field + $taxhash{'district'} = $self->get('district'); } $taxhash{'taxclass'} = $part_pkg->taxclass; my @taxes = (); my %taxhash_elim = %taxhash; - my @elim = qw( city county state ); + my @elim = qw( district city county state ); do { #first try a match with taxclass @@ -1222,12 +1286,6 @@ sub _handle_taxes { } - #what's this doing in the middle of _handle_taxes? probably should split - #this into three parts above in _make_lines - $cust_bill_pkg->set_display( part_pkg => $part_pkg, - real_pkgpart => $real_pkgpart, - ); - my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; foreach my $key (keys %tax_cust_bill_pkg) { my @taxes = @{ $taxes{$key} || [] }; @@ -1477,17 +1535,23 @@ sub retry_realtime { cust_bill_batch ); - my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'", - @realtime_events - ). - ' ) '; + my $is_realtime_event = + ' part_event.action IN ( '. + join(',', map "'$_'", @realtime_events ). + ' ) '; + + my $batch_or_statustext = + "( part_event.action = 'cust_bill_batch' + OR ( statustext IS NOT NULL AND statustext != '' ) + )"; + - my @cust_event = qsearchs({ + my @cust_event = qsearch({ 'table' => 'cust_event', 'select' => 'cust_event.*', 'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join", 'hashref' => { 'status' => 'done' }, - 'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ". + 'extra_sql' => " AND $batch_or_statustext ". " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1" }); @@ -1702,8 +1766,9 @@ sub due_cust_event { #??? #my $DEBUG = $opt{'debug'} + $opt{'debug'} ||= 0; # silence some warnings local($DEBUG) = $opt{'debug'} - if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG; + if $opt{'debug'} > $DEBUG; $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG; warn "$me due_cust_event called with options ". @@ -2154,6 +2219,7 @@ sub apply_payments { cancel_expired_pkgs suspend_adjourned_pkgs + unsuspend_resumed_pkgs bill (do_cust_event pre-bill)