speed up service search
[freeside.git] / FS / FS / cust_pkg.pm
index 55a55ee..b2cb413 100644 (file)
@@ -267,6 +267,12 @@ a ticket will be added to this customer with this subject
 
 an optional queue name for ticket additions
 
+=item allow_pkgpart
+
+Don't check the legality of the package definition.  This should be used
+when performing a package change that doesn't change the pkgpart (i.e. 
+a location change).
+
 =back
 
 =cut
@@ -274,7 +280,8 @@ an optional queue name for ticket additions
 sub insert {
   my( $self, %options ) = @_;
 
-  my $error = $self->check_pkgpart;
+  my $error;
+  $error = $self->check_pkgpart unless $options{'allow_pkgpart'};
   return $error if $error;
 
   my $part_pkg = $self->part_pkg;
@@ -613,7 +620,6 @@ sub check {
     $self->ut_numbern('pkgnum')
     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('pkgpart')
-    || $self->check_pkgpart
     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
     || $self->ut_numbern('start_date')
     || $self->ut_numbern('setup')
@@ -654,14 +660,19 @@ sub check {
 
 =item check_pkgpart
 
+Check the pkgpart to make sure it's allowed with the reg_code and/or
+promo_code of the package (if present) and with the customer's agent.
+Called from C<insert>, unless we are doing a package change that doesn't
+affect pkgpart.
+
 =cut
 
 sub check_pkgpart {
   my $self = shift;
 
-  my $error = $self->ut_numbern('pkgpart');
-  return $error if $error;
+  # my $error = $self->ut_numbern('pkgpart'); # already done
 
+  my $error;
   if ( $self->reg_code ) {
 
     unless ( grep { $self->pkgpart == $_->pkgpart }
@@ -981,6 +992,7 @@ sub uncancel {
 
   my $error = $cust_pkg->insert(
     'change' => 1, #supresses any referral credit to a referring customer
+    'allow_pkgpart' => 1, # allow this even if the package def is disabled
   );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
@@ -1749,12 +1761,23 @@ sub change {
 
   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";
+
+    if ( ! $opt->{'cust_location'}->locationnum ) {
+      # not inserted yet
+      $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;
+
+  }
+
+  # whether to override pkgpart checking on the new package
+  my $same_pkgpart = 1;
+  if ( $opt->{'pkgpart'} and ( $opt->{'pkgpart'} != $self->pkgpart ) ) {
+    $same_pkgpart = 0;
   }
 
   my $unused_credit = 0;
@@ -1781,6 +1804,12 @@ sub change {
   # (i.e. customer default location)
   $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'});
 
+  # usually this doesn't matter.  the two cases where it does are:
+  # 1. unused_credit_change + pkgpart change + setup fee on the new package
+  # and
+  # 2. (more importantly) changing a package before it's billed
+  $hash{'waive_setup'} = $self->waive_setup;
+
   # Create the new package.
   my $cust_pkg = new FS::cust_pkg {
     custnum      => $self->custnum,
@@ -1789,7 +1818,8 @@ sub change {
     locationnum  => ( $opt->{'locationnum'}                        ),
     %hash,
   };
-  $error = $cust_pkg->insert( 'change' => 1 );
+  $error = $cust_pkg->insert( 'change' => 1,
+                              'allow_pkgpart' => $same_pkgpart );
   if ($error) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -1847,6 +1877,23 @@ sub change {
     }
   }
 
+  # transfer discounts, if we're not changing pkgpart
+  if ( $same_pkgpart ) {
+    foreach my $old_discount ($self->cust_pkg_discount_active) {
+      # don't remove the old discount, we may still need to bill that package.
+      my $new_discount = new FS::cust_pkg_discount {
+        'pkgnum'      => $cust_pkg->pkgnum,
+        'discountnum' => $old_discount->discountnum,
+        'months_used' => $old_discount->months_used,
+      };
+      $error = $new_discount->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error transferring discounts: $error";
+      }
+    }
+  }
+
   # Order any supplemental packages.
   my $part_pkg = $cust_pkg->part_pkg;
   my @old_supp_pkgs = $self->supplemental_pkgs;
@@ -1874,14 +1921,14 @@ sub change {
         contract_end  => $cust_pkg->contract_end,
         refnum        => $cust_pkg->refnum,
         discountnum   => $cust_pkg->discountnum,
-        waive_setup   => $cust_pkg->waive_setup
+        waive_setup   => $cust_pkg->waive_setup,
     });
     if ( $old and $opt->{'keep_dates'} ) {
       foreach (qw(setup bill last_bill)) {
         $new->set($_, $old->get($_));
       }
     }
-    $error = $new->insert;
+    $error = $new->insert( allow_pkgpart => $same_pkgpart );
     # transfer services
     if ( $old ) {
       $error ||= $old->transfer($new);
@@ -1931,6 +1978,24 @@ sub change {
 
 }
 
+=item set_quantity QUANTITY
+
+Change the package's quantity field.  This is the one package property
+that can safely be changed without canceling and reordering the package
+(because it doesn't affect tax eligibility).  Returns an error or an 
+empty string.
+
+=cut
+
+sub set_quantity {
+  my $self = shift;
+  $self = $self->replace_old; # just to make sure
+  my $qty = shift;
+  ($qty =~ /^\d+$/ and $qty > 0) or return "bad package quantity $qty";
+  $self->set('quantity' => $qty);
+  $self->replace;
+}
+
 use Storable 'thaw';
 use MIME::Base64;
 sub process_bulk_cust_pkg {
@@ -2621,7 +2686,7 @@ sub statuscolor {
 =item pkg_label
 
 Returns a label for this package.  (Currently "pkgnum: pkg - comment" or
-"pkg-comment" depending on user preference).
+"pkg - comment" depending on user preference).
 
 =cut
 
@@ -2648,6 +2713,17 @@ sub pkg_label_long {
   $label;
 }
 
+=item pkg_locale
+
+Returns a customer-localized label for this package.
+
+=cut
+
+sub pkg_locale {
+  my $self = shift;
+  $self->part_pkg->pkg_locale( $self->cust_main->locale );
+}
+
 =item primary_cust_svc
 
 Returns a primary service (as FS::cust_svc object) if one can be identified.
@@ -3337,6 +3413,12 @@ sub apply_usage {
   my $dbh = dbh;
   my $order = FS::Conf->new->config('cdr-minutes_priority');
 
+  my $is_classnum;
+  if ( $classnum ) {
+    $is_classnum = ' part_pkg_usage_class.classnum = '.$classnum;
+  } else {
+    $is_classnum = ' part_pkg_usage_class.classnum IS NULL';
+  }
   my @usage_recs = qsearch({
       'table'     => 'cust_pkg_usage',
       'addl_from' => ' JOIN part_pkg_usage       USING (pkgusagepart)'.
@@ -3346,7 +3428,7 @@ sub apply_usage {
       'extra_sql' => " WHERE ( cust_pkg.pkgnum = $pkgnum OR ".
                      " ( cust_pkg.custnum = $custnum AND ".
                      " part_pkg_usage.shared IS NOT NULL ) ) AND ".
-                     " part_pkg_usage_class.classnum = $classnum AND ".
+                     $is_classnum . ' AND '.
                      " cust_pkg_usage.minutes > 0",
       'order_by'  => " ORDER BY priority ASC",
   });
@@ -3436,7 +3518,7 @@ sub apply_usage {
         $cust_pkg_usage->set('minutes', $cust_pkg_usage->minutes + $delta);
         $error = $cust_pkg_usage->replace;
       }
-      warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n";
+      #warn 'CDR '.$cdr->acctid . ' stealing allocation '.$cdr_cust_pkg_usage->cdrusagenum.' from CDR '.$old_cdr->acctid."\n";
       $error ||= $cdr_cust_pkg_usage->replace;
       # deduct the stolen minutes
       $minutes -= $cdr_cust_pkg_usage->minutes;