X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling.pm;h=51b49e4f0a1c9ec97f4ef5923c82ea088429efd3;hp=2d7b690df32e4885157fb87d84a2db4141bcebe7;hb=60c34bd328a404008313d4ab78d25152ebdb9226;hpb=a4245323f5dad7d8e9d19f2be4e3f5b036274276 diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 2d7b690df..51b49e4f0 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -57,7 +57,7 @@ Cancels and suspends any packages due, generates bills, applies payments and credits, and applies collection events to run cards, send bills and notices, etc. -By default, warns on errors and continues with the next operation (but see the +Any errors prevent subsequent operations from continuing and die (but see the "fatal" flag below). Options are passed as name-value pairs. Currently available options are: @@ -131,8 +131,7 @@ sub bill_and_collect { if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } - elsif ( $options{fatal} ) { die $error; } - else { warn $error; } + else { die $error; } } $log->debug('suspending adjourned packages', %logopt); @@ -140,8 +139,7 @@ sub bill_and_collect { if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } - elsif ( $options{fatal} ) { die $error; } - else { warn $error; } + else { die $error; } } $log->debug('unsuspending resumed packages', %logopt); @@ -149,8 +147,7 @@ sub bill_and_collect { 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; } + else { die $error; } } $job->update_statustext('20,billing packages') if $job; @@ -159,8 +156,7 @@ sub bill_and_collect { if ( $error ) { $error = "Error billing custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } - elsif ( $options{fatal} ) { die $error; } - else { warn $error; } + else { die $error; } } $job->update_statustext('50,applying payments and credits') if $job; @@ -169,8 +165,7 @@ sub bill_and_collect { if ( $error ) { $error = "Error applying custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } - elsif ( $options{fatal} ) { die $error; } - else { warn $error; } + else { die $error; } } # In a batch tax environment, do not run collection if any pending @@ -195,8 +190,7 @@ sub bill_and_collect { if ( $error ) { $error = "Error collecting custnum ". $self->custnum. ": $error"; if ($options{fatal} && $options{fatal} eq 'return') { return $error; } - elsif ($options{fatal} ) { die $error; } - else { warn $error; } + else { die $error; } } } @@ -216,9 +210,11 @@ sub cancel_expired_pkgs { my @errors = (); + my @really_cancel_pkgs = (); + my @cancel_reasons = (); + CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) { my $cpr = $cust_pkg->last_cust_pkg_reason('expire'); - my $error; if ( $cust_pkg->change_to_pkgnum ) { @@ -228,19 +224,28 @@ sub cancel_expired_pkgs { $cust_pkg->change_to_pkgnum.'; not expiring'; next CUST_PKG; } - $error = $cust_pkg->change( 'cust_pkg' => $new_pkg, - 'unprotect_svcs' => 1 ); - $error = '' if ref $error eq 'FS::cust_pkg'; + my $error = $cust_pkg->change( 'cust_pkg' => $new_pkg, + 'unprotect_svcs' => 1, + ); + push @errors, $error if $error && ref($error) ne 'FS::cust_pkg'; } else { # just cancel it - $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum, - 'reason_otaker' => $cpr->otaker, - 'time' => $time, - ) - : () - ); + + push @really_cancel_pkgs, $cust_pkg; + push @cancel_reasons, $cpr; + } - push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error; + } + + if (@really_cancel_pkgs) { + + my %cancel_opt = ( 'cust_pkg' => \@really_cancel_pkgs, + 'cust_pkg_reason' => \@cancel_reasons, + 'time' => $time, + ); + + push @errors, $self->cancel_pkgs(%cancel_opt); + } join(' / ', @errors); @@ -533,14 +538,19 @@ sub bill { foreach my $part_pkg ( @part_pkg ) { - $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill ); + my $this_cust_pkg = $cust_pkg; + # for add-on packages, copy the object to avoid leaking changes back to + # the caller if pkg_list is in use; see RT#73607 + if ( $part_pkg->get('pkgpart') != $real_pkgpart ) { + $this_cust_pkg = FS::cust_pkg->new({ %hash }); + } my $pass = ''; - if ( $cust_pkg->separate_bill ) { + if ( $this_cust_pkg->separate_bill ) { # if no_auto is also set, that's fine. we just need to not have # invoices that are both auto and no_auto, and since the package # gets an invoice all to itself, it will only be one or the other. - $pass = $cust_pkg->pkgnum; + $pass = $this_cust_pkg->pkgnum; if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet push @passes, $pass; $total_setup{$pass} = do { my $z = 0; \$z }; @@ -554,17 +564,17 @@ sub bill { ); $cust_bill_pkg{$pass} = []; } - } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) { + } elsif ( ($this_cust_pkg->no_auto || $part_pkg->no_auto) ) { $pass = 'no_auto'; } - my $next_bill = $cust_pkg->getfield('bill') || 0; + my $next_bill = $this_cust_pkg->getfield('bill') || 0; my $error; # let this run once if this is the last bill upon cancellation while ( $next_bill <= $cmp_time or $options{cancel} ) { $error = $self->_make_lines( 'part_pkg' => $part_pkg, - 'cust_pkg' => $cust_pkg, + 'cust_pkg' => $this_cust_pkg, 'precommit_hooks' => \@precommit_hooks, 'line_items' => $cust_bill_pkg{$pass}, 'setup' => $total_setup{$pass}, @@ -579,12 +589,12 @@ sub bill { last if $error; # or if we're not incrementing the bill date. - last if ($cust_pkg->getfield('bill') || 0) == $next_bill; + last if ($this_cust_pkg->getfield('bill') || 0) == $next_bill; # or if we're letting it run only once last if $options{cancel}; - $next_bill = $cust_pkg->getfield('bill') || 0; + $next_bill = $this_cust_pkg->getfield('bill') || 0; #stop if -o was passed to freeside-daily last if $options{'one_recur'}; @@ -999,9 +1009,10 @@ sub _make_lines { # - it doesn't already HAVE a setup date # - or a start date in the future # - and it's not suspended + # - and it doesn't have an expire date in the past # - # The last condition used to check the "disable_setup_suspended" option but - # that's obsolete. We now never set the setup date on a suspended package. + # The "disable_setup_suspended" option is now obsolete; we never set the + # setup date on a suspended package. if ( ! $options{recurring_only} and ! $options{cancel} and ( $options{'resetup'} @@ -1012,6 +1023,8 @@ sub _make_lines { && ( ! $cust_pkg->getfield('susp') ) ) ) + and ( ! $cust_pkg->expire + || $cust_pkg->expire > $cmp_time ) ) { @@ -1024,8 +1037,14 @@ sub _make_lines { return "$@ running calc_setup for $cust_pkg\n" if $@; - $unitsetup = $cust_pkg->base_setup() - || $setup; #XXX uuh + # Only increment unitsetup here if there IS a setup fee. + # prorate_defer_bill may cause calc_setup on a setup-stage package + # to return zero, and the setup fee to be charged later. (This happens + # when it's first billed on the prorate cutoff day. RT#31276.) + if ( $setup ) { + $unitsetup = $cust_pkg->base_setup() + || $setup; #XXX uuh + } if ( $setup_param{'billed_currency'} ) { $setup_billed_currency = delete $setup_param{'billed_currency'}; @@ -1033,10 +1052,20 @@ sub _make_lines { } } - $cust_pkg->setfield('setup', $time) - unless $cust_pkg->setup; - #do need it, but it won't get written to the db - #|| $cust_pkg->pkgpart != $real_pkgpart; + if ($cust_pkg->waive_setup && $part_pkg->plan eq "prorate") { + $lineitems++; + $setup = 0 if $part_pkg->prorate_setup($cust_pkg, $time); + } + + if ( $cust_pkg->get('setup') ) { + # don't change it + } elsif ( $cust_pkg->get('start_date') ) { + # this allows start_date to be used to set the first bill date + $cust_pkg->set('setup', $cust_pkg->get('start_date')); + } else { + # if unspecified, start it right now + $cust_pkg->set('setup', $time); + } $cust_pkg->setfield('start_date', '') if $cust_pkg->start_date; @@ -1053,6 +1082,23 @@ sub _make_lines { my $recur_billed_currency = ''; my $recur_billed_amount = 0; my $sdate; + + my $override_quantity; + + # Conditions for billing the recurring fee: + # - the package doesn't have a future start date + # - and it's not suspended + # - unless suspend_bill is enabled on the package or package def + # - but still not, if the package is on hold + # - or it's suspended for a delayed cancellation + # - and its next bill date is in the past + # - or it doesn't have a next bill date yet + # - or it's a one-time charge + # - or it's a CDR plan with the "bill_every_call" option + # - or it's being canceled + # - and it doesn't have an expire date in the past (this can happen with + # advance billing) + # - again, unless it's being canceled if ( ! $cust_pkg->start_date and ( ! $cust_pkg->susp @@ -1071,6 +1117,12 @@ sub _make_lines { && $part_pkg->option('bill_every_call') ) || $options{cancel} + + and + ( ! $cust_pkg->expire + || $cust_pkg->expire > $cmp_time + || $options{cancel} + ) ) { # XXX should this be a package event? probably. events are called @@ -1133,6 +1185,11 @@ sub _make_lines { $recur_billed_amount = delete $param{'billed_amount'}; } + if ( $param{'override_quantity'} ) { + $override_quantity = $param{'override_quantity'}; + $unitrecur = $recur / $override_quantity; + } + if ( $increment_next_bill ) { my $next_bill; @@ -1177,7 +1234,8 @@ sub _make_lines { } else { # the normal case, not a supplemental package $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0); - return "unparsable frequency: ". $part_pkg->freq + return "unparsable frequency: ". + ($options{freq_override} || $part_pkg->freq) if $next_bill == -1; } @@ -1196,7 +1254,7 @@ sub _make_lines { # Add an additional setup fee at the billing stage. # Used for prorate_defer_bill. $setup += $param{'setup_fee'}; - $unitsetup += $param{'setup_fee'}; + $unitsetup = $cust_pkg->base_setup(); $lineitems++; } @@ -1206,7 +1264,7 @@ sub _make_lines { } } - } + } # end of recurring fee warn "\$setup is undefined" unless defined($setup); warn "\$recur is undefined" unless defined($recur); @@ -1270,14 +1328,14 @@ sub _make_lines { my $cust_bill_pkg = new FS::cust_bill_pkg { 'pkgnum' => $cust_pkg->pkgnum, 'setup' => $setup, - 'unitsetup' => $unitsetup, + 'unitsetup' => sprintf('%.2f', $unitsetup), 'setup_billed_currency' => $setup_billed_currency, 'setup_billed_amount' => $setup_billed_amount, 'recur' => $recur, - 'unitrecur' => $unitrecur, + 'unitrecur' => sprintf('%.2f', $unitrecur), 'recur_billed_currency' => $recur_billed_currency, 'recur_billed_amount' => $recur_billed_amount, - 'quantity' => $cust_pkg->quantity, + 'quantity' => $override_quantity || $cust_pkg->quantity, 'details' => \@details, 'discounts' => [ @setup_discounts, @recur_discounts ], 'hidden' => $part_pkg->hidden,