X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pkg.pm;h=fbecd8d691009e5de96282d43d58ee04e68ad056;hb=98f6d91ec7eaa907204afbfeb90ede1e3bff656d;hp=91a5677f2a711f8e052ab176f6b1561009ed9f3b;hpb=f2aed236d3211d83f06a06b3f15320316550608c;p=freeside.git diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 91a5677f2..fbecd8d69 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -251,19 +251,53 @@ or contract_end timers to some number of months after the start date a delayed setup fee after a period of "free days", will also set the start date to the end of that period. +If the package has an automatic transfer rule (C), then +this will also order the package and set its start date. + =cut sub set_initial_timers { my $self = shift; my $part_pkg = $self->part_pkg; + my $start = $self->start_date || $self->setup || time; + foreach my $action ( qw(expire adjourn contract_end) ) { - my $months = $part_pkg->option("${action}_months",1); + my $months = $part_pkg->get("${action}_months"); if($months and !$self->get($action)) { - my $start = $self->start_date || $self->setup || time; $self->set($action, $part_pkg->add_freq($start, $months) ); } } + # if this package has an expire date and a change_to_pkgpart, set automatic + # package transfer + # (but don't call change_later, as that would call $self->replace, and we're + # probably in the middle of $self->insert right now) + if ( $part_pkg->expire_months and $part_pkg->change_to_pkgpart ) { + if ( $self->change_to_pkgnum ) { + # this can happen if a package is ordered on hold, scheduled for a + # future change _while on hold_, and then released from hold, causing + # the automatic transfer to schedule. + # + # what's correct behavior in that case? I think it's to disallow + # future-changing an on-hold package that has an automatic transfer. + # but if we DO get into this situation, let the manual package change + # win. + warn "pkgnum ".$self->pkgnum.": manual future package change blocks ". + "automatic transfer.\n"; + } else { + my $change_to = FS::cust_pkg->new( { + start_date => $self->get('expire'), + pkgpart => $part_pkg->change_to_pkgpart, + map { $_ => $self->get($_) } + qw( custnum locationnum quantity refnum salesnum contract_end ) + } ); + my $error = $change_to->insert; + + return $error if $error; + $self->set('change_to_pkgnum', $change_to->pkgnum); + } + } + # if this package has "free days" and delayed setup fee, then # set start date that many days in the future. # (this should have been set in the UI, but enforce it here) @@ -273,6 +307,7 @@ sub set_initial_timers { { $self->start_date( $part_pkg->default_start_date ); } + ''; } @@ -332,9 +367,12 @@ a location change). sub insert { my( $self, %options ) = @_; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + my $error; $error = $self->check_pkgpart unless $options{'allow_pkgpart'}; - return $error if $error; my $part_pkg = $self->part_pkg; @@ -359,15 +397,12 @@ sub insert { $self->set('start_date', ''); } else { # set expire/adjourn/contract_end timers, and free days, if appropriate - $self->set_initial_timers; + # and automatic package transfer, which can fail, so capture the result + $error = $self->set_initial_timers; } } # else this is a package change, and shouldn't have "new package" behavior - my $oldAutoCommit = $FS::UID::AutoCommit; - local $FS::UID::AutoCommit = 0; - my $dbh = dbh; - - $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ()); + $error ||= $self->SUPER::insert($options{options} ? %{$options{options}} : ()); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -461,9 +496,26 @@ hide cancelled packages. =cut +# this is still used internally to abort future package changes, so it +# does need to work + sub delete { my $self = shift; + # The following foreign keys to cust_pkg are not cleaned up here, and will + # cause package deletion to fail: + # + # cust_credit.pkgnum and commission_pkgnum (and cust_credit_void) + # cust_credit_bill.pkgnum + # cust_pay_pending.pkgnum + # cust_pay.pkgnum (and cust_pay_void) + # cust_bill_pay.pkgnum (wtf, shouldn't reference pkgnum) + # cust_pkg_usage.pkgnum + # cust_pkg.uncancel_pkgnum, change_pkgnum, main_pkgnum, and change_to_pkgnum + + # cust_svc is handled by canceling the package before deleting it + # cust_pkg_option is handled via option_Common + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; @@ -499,7 +551,13 @@ sub delete { } } - #pkg_referral? + foreach my $pkg_referral ( $self->pkg_referral ) { + my $error = $pkg_referral->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } my $error = $self->SUPER::delete(@_); if ( $error ) { @@ -807,12 +865,15 @@ sub cancel { my( $self, %options ) = @_; my $error; - # pass all suspend/cancel actions to the main package - # (unless the pkglinknum has been removed, then the link is defunct and - # this package can be canceled on its own) - if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) { - return $self->main_pkg->cancel(%options); - } + # supplemental packages can now be separately canceled, though the UI + # shouldn't permit it + # + ## pass all suspend/cancel actions to the main package + ## (unless the pkglinknum has been removed, then the link is defunct and + ## this package can be canceled on its own) + #if ( $self->main_pkgnum and $self->pkglinknum and !$options{'from_main'} ) { + # return $self->main_pkg->cancel(%options); + #} my $conf = new FS::Conf; @@ -936,8 +997,14 @@ sub cancel { $hash{main_pkgnum} = ''; } + # if there is a future package change scheduled, unlink from it (like + # abort_change) first, then delete it. + $hash{'change_to_pkgnum'} = ''; + + # save the package state my $new = new FS::cust_pkg ( \%hash ); $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('no_delay_cancel' => 1) || $change_to->delete; @@ -1285,9 +1352,13 @@ sub suspend { my( $self, %options ) = @_; my $error; - # pass all suspend/cancel actions to the main package + # supplemental packages still can't be separately suspended, but silently + # exit instead of failing or passing the action to the main package (so + # that the "Suspend customer" action doesn't trip over the supplemental + # packages and die) + if ( $self->main_pkgnum and !$options{'from_main'} ) { - return $self->main_pkg->suspend(%options); + return; } my $oldAutoCommit = $FS::UID::AutoCommit; @@ -1659,7 +1730,11 @@ sub unsuspend { if (!$self->setup) { # then this package is being released from on-hold status - $self->set_initial_timers; + $error = $self->set_initial_timers; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } my @labels = (); @@ -1943,6 +2018,13 @@ can't be transferred (also see the I config option). If unprotect_svcs is true, this method will transfer as many services as it can and then unconditionally cancel the old package. +=item contract_end + +If specified, sets this value for the contract_end date on the new package +(without regard for keep_dates or the usual date-preservation behavior.) +Will throw an error if defined but false; the UI doesn't allow editing +this unless it already exists, making removal impossible to undo. + =back At least one of locationnum, cust_location, pkgpart, refnum, cust_main, or @@ -1956,6 +2038,33 @@ For example: =cut +#used by change and change_later +#didn't put with documented check methods because it depends on change-specific opts +#and it also possibly edits the value of opts +sub _check_change { + my $self = shift; + my $opt = shift; + if ( defined($opt->{'contract_end'}) ) { + my $current_contract_end = $self->get('contract_end'); + unless ($opt->{'contract_end'}) { + if ($current_contract_end) { + return "Cannot remove contract end date when changing packages"; + } else { + #shouldn't even pass this option if there's not a current value + #but can be handled gracefully if the option is empty + warn "Contract end date passed unexpectedly"; + delete $opt->{'contract_end'}; + return ''; + } + } + unless ($current_contract_end) { + #option shouldn't be passed, throw error if it's non-empty + return "Cannot add contract end date when changing packages " . $self->pkgnum; + } + } + return ''; +} + #some false laziness w/order sub change { my $self = shift; @@ -1963,13 +2072,21 @@ sub change { my $conf = new FS::Conf; + # handle contract_end on cust_pkg same as passed option + if ( $opt->{'cust_pkg'} ) { + $opt->{'contract_end'} = $opt->{'cust_pkg'}->contract_end; + delete $opt->{'contract_end'} unless $opt->{'contract_end'}; + } + + # check contract_end, prevent adding/removing + my $error = $self->_check_change($opt); + return $error if $error; + # Transactionize this whole mess my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error; - if ( $opt->{'cust_location'} ) { $error = $opt->{'cust_location'}->find_or_insert; if ( $error ) { @@ -1992,9 +2109,12 @@ sub change { # almost. if the new pkgpart specifies start/adjourn/expire timers, # apply those. if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { - $self->set_initial_timers; + $error ||= $self->set_initial_timers; } - $error = $self->replace; + # but if contract_end was explicitly specified, that overrides all else + $self->set('contract_end', $opt->{'contract_end'}) + if $opt->{'contract_end'}; + $error ||= $self->replace; if ( $error ) { $dbh->rollback if $oldAutoCommit; return "modifying package: $error"; @@ -2051,6 +2171,9 @@ sub change { start_date contract_end)) { $hash{$date} = $self->getfield($date); } + # but if contract_end was explicitly specified, that overrides all else + $hash{'contract_end'} = $opt->{'contract_end'} + if $opt->{'contract_end'}; # allow $opt->{'locationnum'} = '' to specifically set it to null # (i.e. customer default location) @@ -2339,8 +2462,10 @@ The date for the package change. Required, and must be in the future. =item quantity -The pkgpart. locationnum, and quantity of the new package, with the same -meaning as in C. +=item contract_end + +The pkgpart, locationnum, quantity and optional contract_end of the new +package, with the same meaning as in C. =back @@ -2350,6 +2475,10 @@ sub change_later { my $self = shift; my $opt = ref($_[0]) ? shift : { @_ }; + # check contract_end, prevent adding/removing + my $error = $self->_check_change($opt); + return $error if $error; + my $oldAutoCommit = $FS::UID::AutoCommit; local $FS::UID::AutoCommit = 0; my $dbh = dbh; @@ -2363,8 +2492,6 @@ sub change_later { return "start_date $date is in the past"; } - my $error; - if ( $self->change_to_pkgnum ) { my $change_to = FS::cust_pkg->by_key($self->change_to_pkgnum); my $new_pkgpart = $opt->{'pkgpart'} @@ -2373,7 +2500,9 @@ sub change_later { if $opt->{'locationnum'} and $opt->{'locationnum'} != $change_to->locationnum; my $new_quantity = $opt->{'quantity'} if $opt->{'quantity'} and $opt->{'quantity'} != $change_to->quantity; - if ( $new_pkgpart or $new_locationnum or $new_quantity ) { + my $new_contract_end = $opt->{'contract_end'} + if $opt->{'contract_end'} and $opt->{'contract_end'} != $change_to->contract_end; + if ( $new_pkgpart or $new_locationnum or $new_quantity or $new_contract_end ) { # it hasn't been billed yet, so in principle we could just edit # it in place (w/o a package change), but that's bad form. # So change the package according to the new options... @@ -2388,8 +2517,10 @@ sub change_later { $error = $self->replace || $err_or_pkg->replace || - $change_to->cancel('no_delay_cancel' => 1) || - $change_to->delete; + #because change() might've edited existing scheduled change in place + (($err_or_pkg->pkgnum == $change_to->pkgnum) ? '' : + $change_to->cancel('no_delay_cancel' => 1) || + $change_to->delete); } else { $error = $err_or_pkg; } @@ -2413,8 +2544,10 @@ sub change_later { if $opt->{'locationnum'} and $opt->{'locationnum'} != $self->locationnum; my $new_quantity = $opt->{'quantity'} if $opt->{'quantity'} and $opt->{'quantity'} != $self->quantity; + my $new_contract_end = $opt->{'contract_end'} + if $opt->{'contract_end'} and $opt->{'contract_end'} != $self->contract_end; - return '' unless $new_pkgpart or $new_locationnum or $new_quantity; # wouldn't do anything + return '' unless $new_pkgpart or $new_locationnum or $new_quantity or $new_contract_end; # wouldn't do anything # allow $opt->{'locationnum'} = '' to specifically set it to null # (i.e. customer default location) @@ -2425,7 +2558,7 @@ sub change_later { locationnum => $opt->{'locationnum'}, start_date => $date, map { $_ => ( $opt->{$_} || $self->$_() ) } - qw( pkgpart quantity refnum salesnum ) + qw( pkgpart quantity refnum salesnum contract_end ) } ); $error = $new->insert('change' => 1, 'allow_pkgpart' => ($new_pkgpart ? 0 : 1)); @@ -2451,16 +2584,28 @@ Cancels a future package change scheduled by C. sub abort_change { my $self = shift; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $pkgnum = $self->change_to_pkgnum; my $change_to = FS::cust_pkg->by_key($pkgnum) if $pkgnum; my $error; - if ( $change_to ) { - $error = $change_to->cancel || $change_to->delete; - return $error if $error; - } $self->set('change_to_pkgnum', ''); $self->set('expire', ''); - $self->replace; + $error = $self->replace; + if ( $change_to ) { + $error ||= $change_to->cancel || $change_to->delete; + } + + if ( $oldAutoCommit ) { + if ( $error ) { + dbh->rollback; + } else { + dbh->commit; + } + } + + return $error; } =item set_quantity QUANTITY