X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pkg.pm;h=efe35efa31760a2a0893bd01ae5693fe6ab4cd03;hb=8e50b6f6fbaa9d732a371114c0dfc95c326cb890;hp=5e070e38d163b1b493ef55af5b75ab2cd680934c;hpb=3a8fa4f27ef08792e3d653cee167979a411f89d1;p=freeside.git diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 5e070e38d..efe35efa3 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -667,8 +667,9 @@ sub check { || $self->ut_numbern('resume') || $self->ut_numbern('expire') || $self->ut_numbern('dundate') - || $self->ut_enum('no_auto', [ '', 'Y' ]) - || $self->ut_enum('waive_setup', [ '', 'Y' ]) + || $self->ut_flag('no_auto', [ '', 'Y' ]) + || $self->ut_flag('waive_setup', [ '', 'Y' ]) + || $self->ut_flag('separate_bill') || $self->ut_textn('agent_pkgid') || $self->ut_enum('recur_show_zero', [ '', 'Y', 'N', ]) || $self->ut_enum('setup_show_zero', [ '', 'Y', 'N', ]) @@ -784,12 +785,22 @@ to a different pkgpart or location, and probably shouldn't be in any other case. If it's not set, the 'unused_credit_cancel' part_pkg option will be used. +=item no_delay_cancel - prevents delay_cancel behavior +no matter what other options say, for use when changing packages (or any +other time you're really sure you want an immediate cancel) + =back If there is an error, returns the error, otherwise returns false. =cut +#NOT DOCUMENTING - this should only be used when calling recursively +#=item delay_cancel - for internal use, to allow proper handling of +#supplemental packages when the main package is flagged to suspend +#before cancelling, probably shouldn't be used otherwise (set the +#corresponding package option instead) + sub cancel { my( $self, %options ) = @_; my $error; @@ -823,6 +834,21 @@ sub cancel { my $date = $options{'date'} if $options{'date'}; # expire/cancel later $date = '' if ($date && $date <= $cancel_time); # complain instead? + my $delay_cancel = $options{'no_delay_cancel'} ? 0 : $options{'delay_cancel'}; + if ( !$date && $self->part_pkg->option('delay_cancel',1) + && (($self->status eq 'active') || ($self->status eq 'suspended')) + && !$options{'no_delay_cancel'} + ) { + my $expdays = $conf->config('part_pkg-delay_cancel-days') || 1; + my $expsecs = 60*60*24*$expdays; + my $suspfor = $self->susp ? $cancel_time - $self->susp : 0; + $expsecs = $expsecs - $suspfor if $suspfor; + unless ($expsecs <= 0) { #if it's already been suspended long enough, don't re-suspend + $delay_cancel = 1; + $date = $cancel_time + $expsecs; + } + } + #race condition: usage could be ongoing until unprovisioned #resolved by performing a change package instead (which unprovisions) and #later cancelling @@ -887,12 +913,16 @@ sub cancel { return $error; } } - } #unless $date my %hash = $self->hash; if ( $date ) { $hash{'expire'} = $date; + if ($delay_cancel) { + # just to be sure these are clear + $hash{'adjourn'} = undef; + $hash{'resume'} = undef; + } } else { $hash{'cancel'} = $cancel_time; } @@ -908,7 +938,7 @@ sub cancel { $error = $new->replace( $self, options => { $self->options } ); if ( $self->change_to_pkgnum ) { my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum); - $error ||= $change_to->cancel || $change_to->delete; + $error ||= $change_to->cancel('no_delay_cancel' => 1) || $change_to->delete; } if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -916,18 +946,31 @@ sub cancel { } foreach my $supp_pkg ( $self->supplemental_pkgs ) { - $error = $supp_pkg->cancel(%options, 'from_main' => 1); + $error = $supp_pkg->cancel(%options, + 'from_main' => 1, + 'date' => $date, #in case it got changed by delay_cancel + 'delay_cancel' => $delay_cancel, + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "canceling supplemental pkg#".$supp_pkg->pkgnum.": $error"; } } - foreach my $usage ( $self->cust_pkg_usage ) { - $error = $usage->delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "deleting usage pools: $error"; + if ($delay_cancel && !$options{'from_main'}) { + $error = $new->suspend( + 'from_cancel' => 1, + 'time' => $cancel_time + ); + } + + unless ($date) { + foreach my $usage ( $self->cust_pkg_usage ) { + $error = $usage->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "deleting usage pools: $error"; + } } } @@ -947,7 +990,7 @@ sub cancel { } else { $error = send_email( - 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), + 'from' => $conf->invoice_from_full( $self->cust_main->agentnum ), 'to' => \@invoicing_list, 'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ), 'body' => [ map "$_\n", $conf->config('cancelmessage') ], @@ -1030,7 +1073,8 @@ sub uncancel { setup susp adjourn resume expire start_date contract_end dundate change_date change_pkgpart change_locationnum - manual_flag no_auto quantity agent_pkgid recur_show_zero setup_show_zero + manual_flag no_auto separate_bill quantity agent_pkgid + recur_show_zero setup_show_zero ), }; @@ -1226,6 +1270,9 @@ separately. =item from_main - allows a supplemental package to be suspended, rather than redirecting the method call to its main package. For internal use. +=item from_cancel - used when suspending from the cancel method, forces +this to skip everything besides basic suspension. For internal use. + =back If there is an error, returns the error, otherwise returns false. @@ -1268,7 +1315,7 @@ sub suspend { } # some false laziness with sub cancel - if ( !$options{nobill} && !$date && + if ( !$options{nobill} && !$date && !$options{'from_cancel'} && $self->part_pkg->option('bill_suspend_as_cancel',1) ) { # kind of a kludge--'bill_suspend_as_cancel' to avoid having to # make the entire cust_main->bill path recognize 'suspend' and @@ -1333,17 +1380,19 @@ sub suspend { unless ( $date ) { # then we are suspending now - # credit remaining time if appropriate - # (if required by the package def, or the suspend reason) - my $unused_credit = $self->part_pkg->option('unused_credit_suspend',1) - || ( defined($reason) && $reason->unused_credit ); + unless ($options{'from_cancel'}) { + # credit remaining time if appropriate + # (if required by the package def, or the suspend reason) + my $unused_credit = $self->part_pkg->option('unused_credit_suspend',1) + || ( defined($reason) && $reason->unused_credit ); - if ( $unused_credit ) { - warn "crediting unused time on pkg#".$self->pkgnum."\n" if $DEBUG; - my $error = $self->credit_remaining('suspend', $suspend_time); - if ($error) { - $dbh->rollback if $oldAutoCommit; - return $error; + if ( $unused_credit ) { + warn "crediting unused time on pkg#".$self->pkgnum."\n" if $DEBUG; + my $error = $self->credit_remaining('suspend', $suspend_time); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } } @@ -1374,7 +1423,7 @@ sub suspend { } my $conf = new FS::Conf; - if ( $conf->config('suspend_email_admin') ) { + if ( $conf->config('suspend_email_admin') && !$options{'from_cancel'} ) { my $error = send_email( 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), @@ -1424,6 +1473,21 @@ are mandatory. =cut +# Implementation note: +# +# If you pkgpart-change a package that has been billed, and it's set to give +# credit on package change, then this method gets called and then the new +# package will have no last_bill date. Therefore the customer will be credited +# only once (per billing period) even if there are multiple package changes. +# +# If you location-change a package that has been billed, this method will NOT +# be called and the new package WILL have the last bill date of the old +# package. +# +# If the new package is then canceled within the same billing cycle, +# credit_remaining needs to run calc_remain on the OLD package to determine +# the amount of unused time to credit. + sub credit_remaining { # Add a credit for remaining service my ($self, $mode, $time) = @_; @@ -1441,10 +1505,29 @@ sub credit_remaining { and $next_bill >= $time # which is in the future ) { my @cust_credit_source_bill_pkg = (); - my $remaining_value = $self->calc_remain( + my $remaining_value = 0; + + my $remain_pkg = $self; + $remaining_value = $remain_pkg->calc_remain( 'time' => $time, 'cust_credit_source_bill_pkg' => \@cust_credit_source_bill_pkg, ); + + # we may have to walk back past some package changes to get to the + # one that actually has unused time + while ( $remaining_value == 0 ) { + if ( $remain_pkg->change_pkgnum ) { + $remain_pkg = FS::cust_pkg->by_key($remain_pkg->change_pkgnum); + } else { + # the package has really never been billed + return; + } + $remaining_value = $remain_pkg->calc_remain( + 'time' => $time, + 'cust_credit_source_bill_pkg' => \@cust_credit_source_bill_pkg, + ); + } + if ( $remaining_value > 0 ) { warn "Crediting for $remaining_value on package ".$self->pkgnum."\n" if $DEBUG; @@ -1583,19 +1666,46 @@ sub unsuspend { my $conf = new FS::Conf; #adjust the next bill date forward - $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive - if $inactive > 0 + # increment next bill date if certain conditions are met: + # - it was due to be billed at some point + # - either the global or local config says to do this + my $adjust_bill = 0; + if ( + $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} ) && ( $opt{'adjust_next_bill'} || $conf->exists('unsuspend-always_adjust_next_bill_date') || $self->part_pkg->option('unsuspend_adjust_bill', 1) ) - && ! $self->option('suspend_bill',1) - && ( ! $self->part_pkg->option('suspend_bill',1) - || $self->option('no_suspend_bill',1) - ) - && $hash{'order_date'} != $hash{'susp'} - ; + ) { + $adjust_bill = 1; + } + + # but not if: + # - the package billed during suspension + # - or it was ordered on hold + # - or the customer was credited for the unused time + + if ( $self->option('suspend_bill',1) + or ( $self->part_pkg->option('suspend_bill',1) + and ! $self->option('no_suspend_bill',1) + ) + or $hash{'order_date'} == $hash{'susp'} + ) { + $adjust_bill = 0; + } + + if ( $adjust_bill ) { + if ( $self->part_pkg->option('unused_credit_suspend') + or ( ref($reason) and $reason->unused_credit ) ) { + # then the customer was credited for the unused time before suspending, + # so their next bill should be immediate + $hash{'bill'} = time; + } else { + # add the length of time suspended to the bill date + $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive; + } + } $hash{'susp'} = ''; $hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time; @@ -2095,6 +2205,7 @@ sub change { unused_credit => $unused_credit, nobill => $keep_dates, change_custnum => ( $self->custnum != $custnum ? $custnum : '' ), + no_delay_cancel => 1, ); if ($error) { $dbh->rollback if $oldAutoCommit; @@ -2192,7 +2303,7 @@ sub change_later { $error = $self->replace || $err_or_pkg->replace || - $change_to->cancel || + $change_to->cancel('no_delay_cancel' => 1) || $change_to->delete; } else { $error = $err_or_pkg; @@ -2312,6 +2423,7 @@ and, I: - start_date: the date when it will be billed - amount: the setup fee to be charged - quantity: the multiplier for the setup fee +- separate_bill: whether to put the charge on a separate invoice If you pass 'adjust_commission' => 1, and the classnum changes, and there are commission credits linked to this charge, they will be recalculated. @@ -2367,7 +2479,8 @@ sub modify_charge { } if ( !$self->get('setup') ) { - # not yet billed, so allow amount, setup_cost, quantity and start_date + # not yet billed, so allow amount, setup_cost, quantity, start_date, + # and separate_bill if ( exists($opt{'amount'}) and $part_pkg->option('setup_fee') != $opt{'amount'} @@ -2397,9 +2510,22 @@ sub modify_charge { $self->set('start_date', $opt{'start_date'}); } + if ( exists($opt{'separate_bill'}) + and $opt{'separate_bill'} ne $self->separate_bill ) { + + $self->set('separate_bill', $opt{'separate_bill'}); + } + } # else simply ignore them; the UI shouldn't allow editing the fields + + if ( exists($opt{'taxclass'}) + and $part_pkg->taxclass ne $opt{'taxclass'}) { + + $part_pkg->set('taxclass', $opt{'taxclass'}); + } + my $error; if ( $part_pkg->modified or $pkg_opt_modified ) { # can we safely modify the package def? @@ -3249,7 +3375,7 @@ Class method that returns the list of possible status strings for packages =cut tie my %statuscolor, 'Tie::IxHash', - 'on hold' => '7E0079', #purple! + 'on hold' => 'FF00F5', #brighter purple! 'not yet billed' => '009999', #teal? cyan? 'one-time charge' => '0000CC', #blue #'000000', 'active' => '00CC00', @@ -3280,6 +3406,34 @@ sub statuscolor { $statuscolor{$self->status}; } +=item is_status_delay_cancel + +Returns true if part_pkg has option delay_cancel, +cust_pkg status is 'suspended' and expire is set +to cancel package within the next day (or however +many days are set in global config part_pkg-delay_cancel-days. + +This is not a real status, this only meant for hacking display +values, because otherwise treating the package as suspended is +really the whole point of the delay_cancel option. + +=cut + +sub is_status_delay_cancel { + my ($self) = @_; + if ( $self->main_pkgnum and $self->pkglinknum ) { + return $self->main_pkg->is_status_delay_cancel; + } + return 0 unless $self->part_pkg->option('delay_cancel',1); + return 0 unless $self->status eq 'suspended'; + return 0 unless $self->expire; + my $conf = new FS::Conf; + my $expdays = $conf->config('part_pkg-delay_cancel-days') || 1; + my $expsecs = 60*60*24*$expdays; + return 0 unless $self->expire < time + $expsecs; + return 1; +} + =item pkg_label Returns a label for this package. (Currently "pkgnum: pkg - comment" or @@ -4667,7 +4821,7 @@ sub order { $dbh->rollback if $oldAutoCommit; return "Unable to transfer all services from package ".$old_pkg->pkgnum; } - $error = $old_pkg->cancel( quiet=>1 ); + $error = $old_pkg->cancel( quiet=>1, 'no_delay_cancel'=>1 ); if ($error) { $dbh->rollback; return $error;