X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pkg.pm;h=140f961cc65c87f2198f180c4bbce7663c03843c;hb=5cda22da9d0771878b5256895e1655d75f3ea1f6;hp=39c0a42a34e2aedbaa47ac3a5a18db581b7e3695;hpb=70889cbb815cd80925c21891fcf145c95983ad8b;p=freeside.git diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 39c0a42a3..140f961cc 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -56,18 +56,27 @@ $disable_agentcheck = 0; $upgrade = 0; #go away after setup+start dates cleaned up for old customers +our $cache_enabled = 0; + +sub _simplecache { + my( $self, $hashref ) = @_; + if ( $cache_enabled && $hashref->{'pkg'} && $hashref->{'plan'} ) { + $self->{'_pkgpart'} = FS::part_pkg->new($hashref); + } +} + sub _cache { my $self = shift; my ( $hashref, $cache ) = @_; - #if ( $hashref->{'pkgpart'} ) { - if ( $hashref->{'pkg'} ) { - # #@{ $self->{'_pkgnum'} } = (); - # my $subcache = $cache->subcache('pkgpart', 'part_pkg'); - # $self->{'_pkgpart'} = $subcache; - # #push @{ $self->{'_pkgnum'} }, - # FS::part_pkg->new_or_cached($hashref, $subcache); - $self->{'_pkgpart'} = FS::part_pkg->new($hashref); - } +# #if ( $hashref->{'pkgpart'} ) { +# if ( $hashref->{'pkg'} ) { +# # #@{ $self->{'_pkgnum'} } = (); +# # my $subcache = $cache->subcache('pkgpart', 'part_pkg'); +# # $self->{'_pkgpart'} = $subcache; +# # #push @{ $self->{'_pkgnum'} }, +# # FS::part_pkg->new_or_cached($hashref, $subcache); +# $self->{'_pkgpart'} = FS::part_pkg->new($hashref); +# } if ( exists $hashref->{'svcnum'} ) { #@{ $self->{'_pkgnum'} } = (); my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum}); @@ -108,6 +117,8 @@ FS::cust_pkg - Object methods for cust_pkg objects $seconds = $record->seconds_since($timestamp); + #bulk cancel+order... perhaps slightly deprecated, only used by the bulk + # cancel+order in the web UI and nowhere else (edit/process/cust_pkg.cgi) $error = FS::cust_pkg::order( $custnum, \@pkgparts ); $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] ); @@ -178,11 +189,6 @@ date order taker (see L) -=item manual_flag - -If this field is set to 1, disables the automatic -unsuspension of this package when using the B config option. - =item quantity If not set, defaults to 1 @@ -791,14 +797,9 @@ 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 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) - -=item no_delay_cancel - for internal use, prevents delay_cancel behavior +=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 unadulterated cancel) +other time you're really sure you want an immediate cancel) =back @@ -806,6 +807,12 @@ 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; @@ -909,13 +916,28 @@ sub cancel { } } + # if a reasonnum was passed, get the actual reason object so we can check + # unused_credit + + my $reason; + if ($options{'reason'} =~ /^\d+$/) { + $reason = FS::reason->by_key($options{'reason'}); + } + unless ($date) { - # credit remaining time if appropriate + # credit remaining time if any of these are true: + # - unused_credit => 1 was passed (this happens when canceling a package + # for a package change when unused_credit_change is set) + # - no unused_credit option, and there is a cancel reason, and the cancel + # reason says to credit the package + # - no unused_credit option, and the package definition says to credit the + # package on cancellation my $do_credit; if ( exists($options{'unused_credit'}) ) { $do_credit = $options{'unused_credit'}; - } - else { + } elsif ( defined($reason) && $reason->unused_credit ) { + $do_credit = 1; + } else { $do_credit = $self->part_pkg->option('unused_credit_cancel', 1); } if ( $do_credit ) { @@ -1365,6 +1387,7 @@ sub suspend { if $error; } + my $cust_pkg_reason; if ( $options{'reason'} ) { $error = $self->insert_reason( 'reason' => $options{'reason'}, 'action' => $date ? 'adjourn' : 'suspend', @@ -1375,6 +1398,11 @@ sub suspend { dbh->rollback if $oldAutoCommit; return "Error inserting cust_pkg_reason: $error"; } + $cust_pkg_reason = qsearchs('cust_pkg_reason', { + 'date' => $date ? $date : $suspend_time, + 'action' => $date ? 'A' : 'S', + 'pkgnum' => $self->pkgnum, + }); } # if a reasonnum was passed, get the actual reason object so we can check @@ -1455,6 +1483,27 @@ sub suspend { } } + # suspension fees: if there is a feepart, and it's not an unsuspend fee, + # and this is not a suspend-before-cancel + if ( $cust_pkg_reason ) { + my $reason_obj = $cust_pkg_reason->reason; + if ( $reason_obj->feepart and + ! $reason_obj->fee_on_unsuspend and + ! $options{'from_cancel'} ) { + + # register the need to charge a fee, cust_main->bill will do the rest + warn "registering suspend fee: pkgnum ".$self->pkgnum.", feepart ".$reason->feepart."\n" + if $DEBUG; + my $cust_pkg_reason_fee = FS::cust_pkg_reason_fee->new({ + 'pkgreasonnum' => $cust_pkg_reason->num, + 'pkgnum' => $self->pkgnum, + 'feepart' => $reason->feepart, + 'nextbill' => $reason->fee_hold, + }); + $error ||= $cust_pkg_reason_fee->insert; + } + } + my $conf = new FS::Conf; if ( $conf->config('suspend_email_admin') && !$options{'from_cancel'} ) { @@ -1750,23 +1799,39 @@ sub unsuspend { my $unsusp_pkg; - if ( $reason && $reason->unsuspend_pkgpart ) { - my $part_pkg = FS::part_pkg->by_key($reason->unsuspend_pkgpart) - or $error = "Unsuspend package definition ".$reason->unsuspend_pkgpart. - " not found."; - my $start_date = $self->cust_main->next_bill_date - if $reason->unsuspend_hold; - - if ( $part_pkg ) { - $unsusp_pkg = FS::cust_pkg->new({ - 'custnum' => $self->custnum, - 'pkgpart' => $reason->unsuspend_pkgpart, - 'start_date' => $start_date, - 'locationnum' => $self->locationnum, - # discount? probably not... + if ( $reason ) { + if ( $reason->unsuspend_pkgpart ) { + #warn "Suspend reason '".$reason->reason."' uses deprecated unsuspend_pkgpart feature.\n"; # in 4.x + my $part_pkg = FS::part_pkg->by_key($reason->unsuspend_pkgpart) + or $error = "Unsuspend package definition ".$reason->unsuspend_pkgpart. + " not found."; + my $start_date = $self->cust_main->next_bill_date + if $reason->unsuspend_hold; + + if ( $part_pkg ) { + $unsusp_pkg = FS::cust_pkg->new({ + 'custnum' => $self->custnum, + 'pkgpart' => $reason->unsuspend_pkgpart, + 'start_date' => $start_date, + 'locationnum' => $self->locationnum, + # discount? probably not... + }); + + $error ||= $self->cust_main->order_pkg( 'cust_pkg' => $unsusp_pkg ); + } + } + # new way, using fees + if ( $reason->feepart and $reason->fee_on_unsuspend ) { + # register the need to charge a fee, cust_main->bill will do the rest + warn "registering unsuspend fee: pkgnum ".$self->pkgnum.", feepart ".$reason->feepart."\n" + if $DEBUG; + my $cust_pkg_reason_fee = FS::cust_pkg_reason_fee->new({ + 'pkgreasonnum' => $cust_pkg_reason->num, + 'pkgnum' => $self->pkgnum, + 'feepart' => $reason->feepart, + 'nextbill' => $reason->fee_hold, }); - - $error ||= $self->cust_main->order_pkg( 'cust_pkg' => $unsusp_pkg ); + $error ||= $cust_pkg_reason_fee->insert; } if ( $error ) { @@ -1933,6 +1998,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 @@ -1946,6 +2018,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; @@ -1953,6 +2052,16 @@ 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 local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -1965,27 +2074,53 @@ sub change { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error; + if ( $opt->{'cust_location'} ) { + $error = $opt->{'cust_location'}->find_or_insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "creating location record: $error"; + } + $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum; + } + + # Before going any further here: if the package is still in the pre-setup + # state, it's safe to modify it in place. No need to charge/credit for + # partial period, transfer services, transfer usage pools, copy invoice + # details, or change any dates. + if ( ! $self->setup and ! $opt->{cust_pkg} and ! $opt->{cust_main} ) { + foreach ( qw( locationnum pkgpart quantity refnum salesnum ) ) { + if ( length($opt->{$_}) ) { + $self->set($_, $opt->{$_}); + } + } + # almost. if the new pkgpart specifies start/adjourn/expire timers, + # apply those. + if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) { + $self->set_initial_timers; + } + # 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"; + } else { + $dbh->commit if $oldAutoCommit; + return $self; + } + } my %hash = (); my $time = time; - $hash{'setup'} = $time if $self->setup; + $hash{'setup'} = $time if $self->get('setup'); $hash{'change_date'} = $time; $hash{"change_$_"} = $self->$_() foreach qw( pkgnum pkgpart locationnum ); - if ( $opt->{'cust_location'} ) { - $error = $opt->{'cust_location'}->find_or_insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return "creating location record: $error"; - } - $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum; - } - if ( $opt->{'cust_pkg'} ) { # treat changing to a package with a different pkgpart as a # pkgpart change (because it is) @@ -2000,27 +2135,34 @@ sub change { my $unused_credit = 0; my $keep_dates = $opt->{'keep_dates'}; - # Special case. If the pkgpart is changing, and the customer is - # going to be credited for remaining time, don't keep setup, bill, - # or last_bill dates, and DO pass the flag to cancel() to credit - # the customer. + + # Special case. If the pkgpart is changing, and the customer is going to be + # credited for remaining time, don't keep setup, bill, or last_bill dates, + # and DO pass the flag to cancel() to credit the customer. If the old + # package had a setup date, set the new package's setup to the package + # change date so that it has the same status as before. if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart and $self->part_pkg->option('unused_credit_change', 1) ) { $unused_credit = 1; $keep_dates = 0; - $hash{$_} = '' foreach qw(setup bill last_bill); + $hash{'last_bill'} = ''; + $hash{'bill'} = ''; } if ( $keep_dates ) { - foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire - resume start_date contract_end ) ) { + foreach my $date ( qw(setup bill last_bill) ) { $hash{$date} = $self->getfield($date); } } - # always keep this date, regardless of anything - # (the date of the package change is in a different field) - $hash{'order_date'} = $self->getfield('order_date'); + # always keep the following dates + foreach my $date (qw(order_date susp adjourn cancel expire resume + 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) @@ -2032,6 +2174,9 @@ sub change { # 2. (more importantly) changing a package before it's billed $hash{'waive_setup'} = $self->waive_setup; + # if this package is scheduled for a future package change, preserve that + $hash{'change_to_pkgnum'} = $self->change_to_pkgnum; + my $custnum = $self->custnum; if ( $opt->{cust_main} ) { my $cust_main = $opt->{cust_main}; @@ -2053,10 +2198,15 @@ sub change { # changed from this package. $cust_pkg = $opt->{'cust_pkg'}; - foreach ( qw( pkgnum pkgpart locationnum ) ) { - $cust_pkg->set("change_$_", $self->get($_)); + # follow all the above rules for date changes, etc. + foreach (keys %hash) { + $cust_pkg->set($_, $hash{$_}); } - $cust_pkg->set('change_date', $time); + # except those that implement the future package change behavior + foreach (qw(change_to_pkgnum start_date expire)) { + $cust_pkg->set($_, ''); + } + $error = $cust_pkg->replace; } else { @@ -2078,7 +2228,9 @@ sub change { } # Transfer services and cancel old package. - + # Enforce service limits only if this is a pkgpart change. + local $FS::cust_svc::ignore_quantity; + $FS::cust_svc::ignore_quantity = 1 if $same_pkgpart; $error = $self->transfer($cust_pkg); if ($error and $error == 0) { # $old_pkg->transfer failed. @@ -2283,8 +2435,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 @@ -2294,6 +2448,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; @@ -2307,8 +2465,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'} @@ -2317,7 +2473,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... @@ -2332,8 +2490,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; } @@ -2357,8 +2517,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) @@ -2369,7 +2531,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)); @@ -2818,6 +2980,20 @@ sub calc_recur { $self->part_pkg->calc_recur($self, @_); } +=item base_setup + +Returns the base setup fee (per unit) of this package, from the package +definition. + +=cut + +# minimal version for 3.x; in 4.x this can invoke currency conversion + +sub base_setup { + my $self = shift; + $self->part_pkg->unit_setup($self); +} + =item base_recur Calls the I of the FS::part_pkg object associated with this billing @@ -3033,16 +3209,15 @@ sub cust_svc_unsorted_arrayref { } my %search = ( - 'table' => 'cust_svc', - 'hashref' => { 'pkgnum' => $self->pkgnum }, + 'select' => 'cust_svc.*, part_svc.*', + 'table' => 'cust_svc', + 'hashref' => { 'pkgnum' => $self->pkgnum }, + 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart )', ); - if ( $opt{svcpart} ) { - $search{hashref}->{svcpart} = $opt{'svcpart'}; - } - if ( $opt{'svcdb'} ) { - $search{addl_from} = ' LEFT JOIN part_svc USING ( svcpart ) '; - $search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} ); - } + $search{hashref}->{svcpart} = $opt{svcpart} + if $opt{svcpart}; + $search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{svcdb} ) + if $opt{svcdb}; [ qsearch(\%search) ]; @@ -3167,28 +3342,33 @@ Returns a list of FS::part_svc objects representing services included in this package but not yet provisioned. Each FS::part_svc object also has an extra field, I, which specifies the number of available services. +Accepts option I; if true, only returns part_svc for which the +associated pkg_svc has the provision_hold flag set. + =cut sub available_part_svc { my $self = shift; + my %opt = @_; my $pkg_quantity = $self->quantity || 1; grep { $_->num_avail > 0 } - map { - my $part_svc = $_->part_svc; - $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking - $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart); - - # more evil encapsulation breakage - if($part_svc->{'Hash'}{'num_avail'} > 0) { - my @exports = $part_svc->part_export_did; - $part_svc->{'Hash'}{'can_get_dids'} = scalar(@exports); - } - - $part_svc; - } - $self->part_pkg->pkg_svc; + map { + my $part_svc = $_->part_svc; + $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking + $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart); + + # more evil encapsulation breakage + if ($part_svc->{'Hash'}{'num_avail'} > 0) { + my @exports = $part_svc->part_export_did; + $part_svc->{'Hash'}{'can_get_dids'} = scalar(@exports); + } + + $part_svc; + } + grep { $opt{'provision_hold'} ? $_->provision_hold : 1 } + $self->part_pkg->pkg_svc; } =item part_svc [ OPTION => VALUE ... ] @@ -3421,6 +3601,9 @@ 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. +Accepts option I which should be +the value of the config setting, to avoid looking it up again. + 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. @@ -3428,15 +3611,18 @@ really the whole point of the delay_cancel option. =cut sub is_status_delay_cancel { - my ($self) = @_; + my ($self,%opt) = @_; 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 $expdays = $opt{'part_pkg-delay_cancel-days'}; + unless ($expdays) { + my $conf = new FS::Conf; + $expdays = $conf->config('part_pkg-delay_cancel-days') || 1; + } my $expsecs = 60*60*24*$expdays; return 0 unless $self->expire < time + $expsecs; return 1; @@ -3634,6 +3820,7 @@ Returns the parent customer object (see L). sub cust_main { my $self = shift; + cluck 'cust_pkg->cust_main called' if $DEBUG; qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); } @@ -5361,6 +5548,9 @@ sub _X_show_zero { =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF [ REFNUM ] ] ] +Bulk cancel + order subroutine. Perhaps slightly deprecated, only used by the +bulk cancel+order in the web UI and nowhere else (edit/process/cust_pkg.cgi) + CUSTNUM is a customer (see L) PKGPARTS is a list of pkgparts specifying the the billing item definitions (see @@ -5574,6 +5764,78 @@ sub bulk_change { ''; } +=item forward_emails + +Returns a hash of svcnums and corresponding email addresses +for svc_acct services that can be used as source or dest +for svc_forward services provisioned in this package. + +Accepts options I OR I for a svc_forward +service; if included, will ensure the current values of the +specified service are included in the list, even if for some +other reason they wouldn't be. If called as a class method +with a specified service, returns only these current values. + +Caution: does not actually check if svc_forward services are +available to be provisioned on this package. + +=cut + +sub forward_emails { + my $self = shift; + my %opt = @_; + + #load optional service, thoroughly validated + die "Use svcnum or svc_forward, not both" + if $opt{'svcnum'} && $opt{'svc_forward'}; + my $svc_forward = $opt{'svc_forward'}; + $svc_forward ||= qsearchs('svc_forward',{ 'svcnum' => $opt{'svcnum'} }) + if $opt{'svcnum'}; + die "Specified service is not a forward service" + if $svc_forward && (ref($svc_forward) ne 'FS::svc_forward'); + die "Specified service not found" + if ($opt{'svcnum'} || $opt{'svc_forward'}) && !$svc_forward; + + my %email; + + ## everything below was basically copied from httemplate/edit/svc_forward.cgi + ## with minimal refactoring, not sure why we can't just load all svc_accts for this custnum + + #add current values from specified service, if there was one + if ($svc_forward) { + foreach my $method (qw( srcsvc_acct dstsvc_acct )) { + my $svc_acct = $svc_forward->$method(); + $email{$svc_acct->svcnum} = $svc_acct->email if $svc_acct; + } + } + + if (ref($self) eq 'FS::cust_pkg') { + + #and including the rest for this customer + my($u_part_svc,@u_acct_svcparts); + foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) { + push @u_acct_svcparts,$u_part_svc->getfield('svcpart'); + } + + my $custnum = $self->getfield('custnum'); + foreach my $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) { + my $cust_pkgnum = $i_cust_pkg->getfield('pkgnum'); + #now find the corresponding record(s) in cust_svc (for this pkgnum!) + foreach my $acct_svcpart (@u_acct_svcparts) { + foreach my $i_cust_svc ( + qsearch( 'cust_svc', { 'pkgnum' => $cust_pkgnum, + 'svcpart' => $acct_svcpart } ) + ) { + my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $i_cust_svc->svcnum } ); + $email{$svc_acct->svcnum} = $svc_acct->email; + } + } + } + } + + return %email; +} + # Used by FS::Upgrade to migrate to a new database. sub _upgrade_data { # class method my ($class, %opts) = @_;