RT# 81961 Repair broken links in POD documentation
[freeside.git] / FS / FS / cust_pkg.pm
index b2e6400..5dc3f4e 100644 (file)
@@ -143,7 +143,7 @@ Billing item definition (see L<FS::part_pkg>)
 
 =item locationnum
 
-Optional link to package location (see L<FS::location>)
+Optional link to package location (see L<FS::cust_location>)
 
 =item order_date
 
@@ -554,7 +554,7 @@ Available options are:
 
 =item reason
 
-can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
+can be set to a cancellation reason (see L<FS::reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
 
 =item reason_otaker
 
@@ -1048,18 +1048,23 @@ sub cancel {
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   return '' if $date; #no errors
 
-  my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
-  if ( !$options{'quiet'} && 
-        $conf->exists('emailcancel', $self->cust_main->agentnum) && 
-        @invoicing_list ) {
-    my $msgnum = $conf->config('cancel_msgnum', $self->cust_main->agentnum);
+  my $cust_main = $self->cust_main;
+
+  my @invoicing_list = $cust_main->invoicing_list_emailonly;
+  if (    !$options{'quiet'}
+       && $conf->config_bool('emailcancel', $cust_main->agentnum)
+       && @invoicing_list
+     )
+  {
+    my $msgnum = $conf->config('cancel_msgnum', $cust_main->agentnum);
     my $error = '';
     if ( $msgnum ) {
       my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
-      $error = $msg_template->send( 'cust_main' => $self->cust_main,
-                                    'object'    => $self );
-    }
-    else {
+      $error = $msg_template->send(
+        'cust_main' => $cust_main,
+        'object'    => $self,
+      );
+    } else {
       $error = send_email(
         'from'    => $conf->invoice_from_full( $self->cust_main->agentnum ),
         'to'      => \@invoicing_list,
@@ -1072,6 +1077,23 @@ sub cancel {
     #should this do something on errors?
   }
 
+  my %pkg_class = map { $_=>1 }
+                    $conf->config('cancel_msgnum-referring_cust-pkg_class');
+  my $ref_msgnum = $conf->config('cancel_msgnum-referring_cust');
+  if (    !$options{'quiet'}
+       && $cust_main->referral_custnum
+       && $pkg_class{ $self->classnum } 
+       && $ref_msgnum
+     )
+  {
+    my $msg_template = qsearchs('msg_template', { msgnum => $ref_msgnum });
+    my $error = $msg_template->send( 
+      'cust_main' => $cust_main->referring_cust_main,
+      'object'    => $self,
+    );
+    #should this do something on errors?
+  }
+
   ''; #no errors
 
 }
@@ -1475,7 +1497,7 @@ Available options are:
 
 =over 4
 
-=item reason - can be set to a cancellation reason (see L<FS:reason>),
+=item reason - can be set to a cancellation reason (see L<FS::reason>),
 either a reasonnum of an existing reason, or passing a hashref will create 
 a new reason.  The hashref should have the following keys: 
 - typenum - Reason type (see L<FS::reason_type>
@@ -2341,13 +2363,16 @@ sub change {
     $same_pkgpart = 0;
   }
 
+  if ($opt->{'waive_setup'}) { $self->set('waive_setup', $opt->{'waive_setup'}) }
+  else { $self->set('waive_setup', ''); }
+
   # 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 usage pools, copy invoice details, or change any
   # dates. We DO need to "transfer" services (from the package to itself) to
   # check their validity on the new pkgpart.
   if ( ! $self->setup and ! $opt->{cust_pkg} and ! $opt->{cust_main} ) {
-    foreach ( qw( locationnum pkgpart quantity refnum salesnum ) ) {
+    foreach ( qw( locationnum pkgpart quantity refnum salesnum waive_setup ) ) {
       if ( length($opt->{$_}) ) {
         $self->set($_, $opt->{$_});
       }
@@ -2740,6 +2765,10 @@ The date for the package change.  Required, and must be in the future.
 
 =item quantity
 
+=item discount
+
+Optional hashref that will be passed to $new_pkg->change_discount()
+
 =item contract_end
 
 The pkgpart, locationnum, quantity and optional contract_end of the new 
@@ -2757,6 +2786,9 @@ sub change_later {
   my $error = $self->_check_change($opt);
   return $error if $error;
 
+  my %discount;
+  %discount = %{$opt->{discount}} if ref $opt->{discount};
+
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
@@ -2809,6 +2841,10 @@ sub change_later {
                  (($err_or_pkg->pkgnum == $change_to->pkgnum) ? '' :
                   $change_to->cancel('no_delay_cancel' => 1) ||
                   $change_to->delete);
+
+        # Apply user-specified discount to new cust_pkg
+        $error = $err_or_pkg->change_discount(\%discount)
+          if !$error && %discount && $discount{discountnum} =~ /^-?\d+$/;
       } else {
         $error = $err_or_pkg;
       }
@@ -2816,6 +2852,10 @@ sub change_later {
       $self->set('expire', $date);
       $change_to->set('start_date', $date);
       $error = $self->replace || $change_to->replace;
+
+      # Apply user-specified discount to new cust_pkg
+      $error = $change_to->change_discount(\%discount)
+        if !$error && %discount && $discount{discountnum} =~ /^-?\d+$/;
     }
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -2855,6 +2895,11 @@ sub change_later {
     $self->set('expire', $date);
     $error = $self->replace;
   }
+
+  # Apply user-specified discount to new cust_pkg
+  $new->change_discount(\%discount)
+    if !$error && %discount && $discount{discountnum} =~ /^-?\d+$/;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
   } else {
@@ -3134,11 +3179,10 @@ sub process_bulk_cust_pkg {
   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;
+  die "Must select a new package definition\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
 
@@ -3153,7 +3197,14 @@ sub process_bulk_cust_pkg {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my @cust_pkgs = qsearch('cust_pkg', { 'pkgpart' => $param->{'old_pkgpart'} } );
+  my @old_pkgpart = ref($param->{'old_pkgpart'}) ? @{ $param->{'old_pkgpart'} }
+                                                 : $param->{'old_pkgpart'};
+
+  my @cust_pkgs = qsearch({
+                    'table' => 'cust_pkg',
+                    'extra_sql' => ' WHERE pkgpart IN ('.
+                                       join(',', @old_pkgpart). ')',
+                  });
 
   my $i = 0;
   foreach my $old_cust_pkg ( @cust_pkgs ) {
@@ -4358,8 +4409,10 @@ sub transfer {
     $target{$pkg_svc->svcpart} = $pkg_svc->quantity * ( $dest->quantity || 1 );
   }
 
-  foreach my $cust_svc ($dest->cust_svc) {
-    $target{$cust_svc->svcpart}--;
+  unless ( $self->pkgnum == $dest->pkgnum ) {
+    foreach my $cust_svc ($dest->cust_svc) {
+      $target{$cust_svc->svcpart}--;
+    }
   }
 
   my %svcpart2svcparts = ();
@@ -4393,24 +4446,42 @@ sub transfer {
   my $error;
   foreach my $cust_svc ($self->cust_svc) {
     my $svcnum = $cust_svc->svcnum;
-    if($target{$cust_svc->svcpart} > 0
-       or $FS::cust_svc::ignore_quantity) { # maybe should be a 'force' option
+
+    if (    $target{$cust_svc->svcpart} > 0
+         or $FS::cust_svc::ignore_quantity # maybe should be a 'force' option
+       )
+    {
       $target{$cust_svc->svcpart}--;
+
+      local $FS::cust_svc::ignore_quantity = 1
+        if $self->pkgnum == $dest->pkgnum;
+
+      #why run replace at all in the $self->pkgnum == $dest->pkgnum case?
+      # we do want to trigger location and pkg_change exports, but 
+      # without pkgnum changing from an old to new package, cust_svc->replace
+      # doesn't know how to trigger those.  :/
+      # does this mean we scrap the whole idea of "safe to modify it in place",
+      # or do we special-case and pass the info needed to cust_svc->replace? :/
+
       my $new = new FS::cust_svc { $cust_svc->hash };
       $new->pkgnum($dest_pkgnum);
       $error = $new->replace($cust_svc);
+
     } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
+
       if ( $DEBUG ) {
         warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
         warn "alternates to consider: ".
              join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
       }
+
       my @alternate = grep {
                              warn "considering alternate svcpart $_: ".
                                   "$target{$_} available in new package\n"
                                if $DEBUG;
                              $target{$_} > 0;
                            } @{$svcpart2svcparts{$cust_svc->svcpart}};
+
       if ( @alternate ) {
         warn "alternate(s) found\n" if $DEBUG;
         my $change_svcpart = $alternate[0];
@@ -4422,13 +4493,16 @@ sub transfer {
       } else {
         $remaining++;
       }
+
     } else {
       $remaining++
     }
+
     if ( $error ) {
       my @label = $cust_svc->label;
       return "service $label[1]: $error";
     }
+
   }
   return $remaining;
 }
@@ -4556,7 +4630,7 @@ Available options are:
 
 =item reason
 
-can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
+can be set to a cancellation reason (see L<FS::reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
 
 =item reason_otaker
 
@@ -4656,6 +4730,139 @@ sub insert_discount {
   $cust_pkg_discount->insert;
 }
 
+
+=item change_discount %opt
+
+Method checks if the given values represent a change in either setup or
+discount level.  If so, the existing discounts are revoked, the new
+discounts are recorded.
+
+Usage:
+
+$error = change_discount(
+  {
+    # -1: Indicates a "custom discount"
+    #  0: Indicates to remove any discount
+    # >0: discountnum to apply
+    discountnum => [-1, 0, discountnum],
+
+    # When discountnum is "-1" to indicate custom discount, include
+    # the additional fields:
+    amount      => AMOUNT_DISCOUNT
+    percent     => PERCENTAGE_DISCOUNT
+    months      => 12,
+    setup       => 1, # APPLY TO SETUP
+    _type       => amount/percentage
+  },
+);
+
+
+=cut
+
+sub change_discount {
+  my ($self, $opt) = @_;
+  return "change_discount() called with bad \%opt hashref"
+    unless ref $opt;
+
+  my %opt = %{$opt};
+
+  my @old_discount =
+    qsearch('cust_pkg_discount',{
+      pkgnum   => $self->pkgnum,
+      disabled => '',
+    });
+
+  if ($DEBUG) {
+    warn "change_discount() pkgnum: ".$self->pkgnum." \n";
+    warn "change_discount() \%opt: \n";
+    warn Dumper(\%opt);
+  }
+
+  my @to_be_disabled;
+  my %change = %opt;
+
+  return "change_discount() called with bad discountnum"
+    unless $change{discountnum} =~ /^-?\d+$/;
+
+  if ($change{discountnum} eq 0) {
+    # Removing old discount
+
+    %change = ();
+
+    push @to_be_disabled, @old_discount;
+
+  } else {
+
+    if ( grep { $_->discountnum eq $change{discountnum} } @old_discount ){
+      # Duplicate, disregard this entry
+      %change = ();
+    } else {
+      # Mark any discounts we're replacing
+      push @to_be_disabled, @old_discount;
+    }
+  }
+
+  # If we still have changes queued, create data structures for
+  # insert_discount().
+  my @discount_insert;
+  if (%change) {
+    push @discount_insert, {
+      discountnum         => $change{discountnum},
+      discountnum__type   => $change{_type},
+      discountnum_amount  => $change{amount},
+      discountnum_percent => $change{percent} ? $change{percent} : '0',
+      discountnum_months  => $change{months},
+      discountnum_setup   => $change{setup} ? 'Y' : '',
+    }
+  }
+
+  if ($DEBUG) {
+    warn "change_discount() \% opt before insert \n";
+    warn Dumper \%opt;
+    warn "\@to_be_disabled \n";
+    warn Dumper \@to_be_disabled;
+  }
+
+  # Roll these updates into a transaction
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error;
+
+  # The "waive setup fee" flag has traditionally been handled by setting
+  # $cust_pkg->waive_setup = Y.  This has been appropriately, and separately
+  # handled, and it operates on a different table than cust_pkg_discount,
+  # so the "-2 for waive setup fee" option is not being reimplemented
+  # here.  Perhaps this may change later.
+
+  # Create new discounts
+  for my $insert_discount (@discount_insert) {
+
+    # Set parameters for insert_discount into object, and insert
+    for my $k (keys %{$insert_discount}) {
+      $self->set($k, $insert_discount->{$k});
+    }
+    $error ||= $self->insert_discount();
+  }
+
+  # Disabling old discounts
+  for my $tbd (@to_be_disabled) {
+    unless ($error) {
+      $tbd->set(disabled => 'Y');
+      $error = $tbd->replace();
+    }
+  }
+
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit if $oldAutoCommit;
+  return undef;
+}
+
 =item set_usage USAGE_VALUE_HASHREF 
 
 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
@@ -5343,7 +5550,7 @@ sub search {
   }
 
   ###
-  # parse refnum (advertising source)
+  # parse (customer) refnum (advertising source)
   ###
 
   if ( exists($params->{'refnum'}) ) {
@@ -5354,7 +5561,7 @@ sub search {
       @refnum = ( $params->{'refnum'} );
     }
     my $in = join(',', grep /^\d+$/, @refnum);
-    push @where, "refnum IN($in)" if length $in;
+    push @where, "cust_main.refnum IN($in)" if length $in;
   }
 
   ###
@@ -5533,29 +5740,38 @@ sub search {
 
     foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
 
-      next unless exists($params->{$field});
+      if ( $params->{$field.'_null'} ) {
+
+        push @where, "cust_pkg.$field IS NULL";
+             # this should surely be obsoleted by now: OR cust_pkg.$field == 0 
 
-      my($beginning, $ending) = @{$params->{$field}};
+      } else {
 
-      next if $beginning == 0 && $ending == 4294967295;
+        next unless exists($params->{$field});
 
-      push @where,
-        "cust_pkg.$field IS NOT NULL",
-        "cust_pkg.$field >= $beginning",
-        "cust_pkg.$field <= $ending";
-
-      $orderby ||= "ORDER BY cust_pkg.$field";
-
-      if ( $field eq 'setup' ) {
-        $exclude_change_from = 1;
-      } elsif ( $field eq 'cancel' ) {
-        $exclude_change_to = 1;
-      } elsif ( $field eq 'change_date' ) {
-        # if we are given setup and change_date ranges, and the setup date
-        # falls in _both_ ranges, then include the package whether it was 
-        # a change or not
-        $exclude_change_from = 0;
+        my($beginning, $ending) = @{$params->{$field}};
+
+        next if $beginning == 0 && $ending == 4294967295;
+
+        push @where,
+          "cust_pkg.$field IS NOT NULL",
+          "cust_pkg.$field >= $beginning",
+          "cust_pkg.$field <= $ending";
+
+        $orderby ||= "ORDER BY cust_pkg.$field";
+
+        if ( $field eq 'setup' ) {
+          $exclude_change_from = 1;
+        } elsif ( $field eq 'cancel' ) {
+          $exclude_change_to = 1;
+        } elsif ( $field eq 'change_date' ) {
+          # if we are given setup and change_date ranges, and the setup date
+          # falls in _both_ ranges, then include the package whether it was 
+          # a change or not
+          $exclude_change_from = 0;
+        }
       }
+
     }
 
     if ($exclude_change_from) {
@@ -5568,6 +5784,7 @@ sub search {
         WHERE cust_pkg.pkgnum = changed_to_pkg.change_pkgnum
       )";
     }
+
   }
 
   $orderby ||= 'ORDER BY bill';
@@ -5676,6 +5893,8 @@ sub search {
     push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main');
   }
 
+  push @where, "cust_pkg_reason.reasonnum = '".$params->{reasonnum}."'" if $params->{reasonnum};
+
   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
 
   my $addl_from = 'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
@@ -5683,6 +5902,10 @@ sub search {
                   'LEFT JOIN cust_location USING ( locationnum ) '.
                   FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg');
 
+  if ($params->{reasonnum}) {
+    $addl_from .= 'LEFT JOIN cust_pkg_reason ON (cust_pkg_reason.pkgnum = cust_pkg.pkgnum) ';
+  }
+
   my $select;
   my $count_query;
   if ( $params->{'select_zip5'} ) {
@@ -5746,6 +5969,24 @@ sub fcc_477_count {
 
 }
 
+=item fcc_477_record
+
+Returns a fcc_477 record based on option name.
+
+=cut
+
+sub fcc_477_record {
+  my ($self, $option_name) = @_;
+
+  my $fcc_record = qsearchs({
+    'table'     => 'part_pkg_fcc_option',
+    'hashref'   => { 'pkgpart' => $self->{Hash}->{pkgpart}, 'fccoptionname' => $option_name, },
+  });
+
+  return ( $fcc_record );
+
+}
+
 =item tax_locationnum_sql
 
 Returns an SQL expression for the tax location for a package, based
@@ -6270,4 +6511,3 @@ L<FS::pkg_svc>, schema.html from the base documentation
 =cut
 
 1;
-