+=item refnum
+
+New refnum (see L<FS::part_referral>).
+
+=item keep_dates
+
+Set to true to transfer billing dates (start_date, setup, last_bill, bill,
+susp, adjourn, cancel, expire, and contract_end) to the new package.
+
+=back
+
+At least one of locationnum, cust_location, pkgpart, refnum must be specified
+(otherwise, what's the point?)
+
+Returns either the new FS::cust_pkg object or a scalar error.
+
+For example:
+
+ my $err_or_new_cust_pkg = $old_cust_pkg->change
+
+=cut
+
+#some false laziness w/order
+sub change {
+ my $self = shift;
+ my $opt = ref($_[0]) ? shift : { @_ };
+
+# my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg, $refnum) = @_;
+#
+
+ my $conf = new FS::Conf;
+
+ # Transactionize this whole mess
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $error;
+
+ my %hash = ();
+
+ my $time = time;
+
+ #$hash{$_} = $self->$_() foreach qw( last_bill bill );
+
+ #$hash{$_} = $self->$_() foreach qw( setup );
+
+ $hash{'setup'} = $time if $self->setup;
+
+ $hash{'change_date'} = $time;
+ $hash{"change_$_"} = $self->$_()
+ foreach qw( pkgnum pkgpart locationnum );
+
+ if ( $opt->{'cust_location'} &&
+ ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
+ $error = $opt->{'cust_location'}->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "inserting cust_location (transaction rolled back): $error";
+ }
+ $opt->{'locationnum'} = $opt->{'cust_location'}->locationnum;
+ }
+
+ 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.
+ if ( $opt->{'pkgpart'} and $opt->{'pkgpart'} != $self->pkgpart ) {
+ $keep_dates = 0;
+ $unused_credit = 1 if $self->part_pkg->option('unused_credit_change', 1);
+ $hash{$_} = '' foreach qw(setup bill last_bill);
+ }
+
+ if ( $keep_dates ) {
+ foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire
+ resume start_date contract_end ) ) {
+ $hash{$date} = $self->getfield($date);
+ }
+ }
+ # allow $opt->{'locationnum'} = '' to specifically set it to null
+ # (i.e. customer default location)
+ $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'});
+
+ # Create the new package.
+ my $cust_pkg = new FS::cust_pkg {
+ custnum => $self->custnum,
+ pkgpart => ( $opt->{'pkgpart'} || $self->pkgpart ),
+ refnum => ( $opt->{'refnum'} || $self->refnum ),
+ locationnum => ( $opt->{'locationnum'} ),
+ %hash,
+ };
+
+ $error = $cust_pkg->insert( 'change' => 1 );
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ # Transfer services and cancel old package.
+
+ $error = $self->transfer($cust_pkg);
+ if ($error and $error == 0) {
+ # $old_pkg->transfer failed.
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
+ warn "trying transfer again with change_svcpart option\n" if $DEBUG;
+ $error = $self->transfer($cust_pkg, 'change_svcpart'=>1 );
+ if ($error and $error == 0) {
+ # $old_pkg->transfer failed.
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ($error > 0) {
+ # Transfers were successful, but we still had services left on the old
+ # package. We can't change the package under this circumstances, so abort.
+ $dbh->rollback if $oldAutoCommit;
+ return "Unable to transfer all services from package ". $self->pkgnum;
+ }
+
+ #reset usage if changing pkgpart
+ # AND usage rollover is off (otherwise adds twice, now and at package bill)
+ if ($self->pkgpart != $cust_pkg->pkgpart) {
+ my $part_pkg = $cust_pkg->part_pkg;
+ $error = $part_pkg->reset_usage($cust_pkg, $part_pkg->is_prepaid
+ ? ()
+ : ( 'null' => 1 )
+ )
+ if $part_pkg->can('reset_usage') && ! $part_pkg->option('usage_rollover',1);
+
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error setting usage values: $error";
+ }
+ }
+
+ #Good to go, cancel old package. Notify 'cancel' of whether to credit
+ #remaining time.
+ #Don't allow billing the package (preceding period packages and/or
+ #outstanding usage) if we are keeping dates (i.e. location changing),
+ #because the new package will be billed for the same date range.
+ $error = $self->cancel(
+ quiet => 1,
+ unused_credit => $unused_credit,
+ nobill => $keep_dates
+ );
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) {
+ #$self->cust_main
+ my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ $cust_pkg;
+
+}
+
+use Storable 'thaw';
+use MIME::Base64;
+sub process_bulk_cust_pkg {
+ my $job = shift;
+ my $param = thaw(decode_base64(shift));
+ warn Dumper($param) if $DEBUG;
+
+ my $old_part_pkg = qsearchs('part_pkg',
+ { pkgpart => $param->{'old_pkgpart'} });
+ my $new_part_pkg = qsearchs('part_pkg',
+ { pkgpart => $param->{'new_pkgpart'} });
+ die "Must select a new package type\n" unless $new_part_pkg;
+ #my $keep_dates = $param->{'keep_dates'} || 0;
+ my $keep_dates = 1; # there is no good reason to turn this off
+
+ local $SIG{HUP} = 'IGNORE';
+ local $SIG{INT} = 'IGNORE';
+ local $SIG{QUIT} = 'IGNORE';
+ local $SIG{TERM} = 'IGNORE';
+ local $SIG{TSTP} = 'IGNORE';
+ local $SIG{PIPE} = 'IGNORE';
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my @cust_pkgs = qsearch('cust_pkg', { 'pkgpart' => $param->{'old_pkgpart'} } );
+
+ my $i = 0;
+ foreach my $old_cust_pkg ( @cust_pkgs ) {
+ $i++;
+ $job->update_statustext(int(100*$i/(scalar @cust_pkgs)));
+ if ( $old_cust_pkg->getfield('cancel') ) {
+ warn '[process_bulk_cust_pkg ] skipping canceled pkgnum '.
+ $old_cust_pkg->pkgnum."\n"
+ if $DEBUG;
+ next;
+ }
+ warn '[process_bulk_cust_pkg] changing pkgnum '.$old_cust_pkg->pkgnum."\n"
+ if $DEBUG;
+ my $error = $old_cust_pkg->change(
+ 'pkgpart' => $param->{'new_pkgpart'},
+ 'keep_dates' => $keep_dates
+ );
+ if ( !ref($error) ) { # change returns the cust_pkg on success
+ $dbh->rollback;
+ die "Error changing pkgnum ".$old_cust_pkg->pkgnum.": '$error'\n";
+ }
+ }
+ $dbh->commit if $oldAutoCommit;
+ return;
+}
+
+=item last_bill
+
+Returns the last bill date, or if there is no last bill date, the setup date.
+Useful for billing metered services.
+
+=cut
+
+sub last_bill {
+ my $self = shift;
+ return $self->setfield('last_bill', $_[0]) if @_;
+ return $self->getfield('last_bill') if $self->getfield('last_bill');
+ my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
+ 'edate' => $self->bill, } );
+ $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
+}
+
+=item last_cust_pkg_reason ACTION
+
+Returns the most recent ACTION FS::cust_pkg_reason associated with the package.
+Returns false if there is no reason or the package is not currenly ACTION'd
+ACTION is one of adjourn, susp, cancel, or expire.
+
+=cut
+
+sub last_cust_pkg_reason {
+ my ( $self, $action ) = ( shift, shift );
+ my $date = $self->get($action);
+ qsearchs( {
+ 'table' => 'cust_pkg_reason',
+ 'hashref' => { 'pkgnum' => $self->pkgnum,
+ 'action' => substr(uc($action), 0, 1),
+ 'date' => $date,
+ },
+ 'order_by' => 'ORDER BY num DESC LIMIT 1',
+ } );
+}
+
+=item last_reason ACTION
+
+Returns the most recent ACTION FS::reason associated with the package.
+Returns false if there is no reason or the package is not currenly ACTION'd
+ACTION is one of adjourn, susp, cancel, or expire.
+
+=cut
+
+sub last_reason {
+ my $cust_pkg_reason = shift->last_cust_pkg_reason(@_);
+ $cust_pkg_reason->reason
+ if $cust_pkg_reason;