stretch-happy Releases files
[freeside.git] / FS / FS / part_pkg.pm
index 18a065d..5f7c831 100644 (file)
@@ -2,7 +2,9 @@ package FS::part_pkg;
 use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
 
 use strict;
-use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
+use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack
+             $cache_enabled %cache_link %cache_pkg_svc
+           );
 use Carp qw(carp cluck confess);
 use Scalar::Util qw( blessed );
 use DateTime;
@@ -30,9 +32,14 @@ use FS::part_pkg_usage;
 use FS::part_pkg_vendor;
 
 $DEBUG = 0;
+
 $setup_hack = 0;
 $skip_pkg_svc_hack = 0;
 
+$cache_enabled = 0;
+%cache_link = ();
+%cache_pkg_svc = ();
+
 =head1 NAME
 
 FS::part_pkg - Object methods for part_pkg objects
@@ -168,7 +175,8 @@ I<custnum_ref> and I<options>.
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, appropriate FS::pkg_svc records will be inserted.  I<hidden_svc> can 
 be set to a hashref of svcparts and flag values ('Y' or '') to set the 
-'hidden' field in these records.
+'hidden' field in these records, and I<provision_hold> can be set similarly
+for the 'provision_hold' field in these records.
 
 If I<primary_svc> is set to the svcpart of the primary service, the appropriate
 FS::pkg_svc record will be updated.
@@ -248,6 +256,7 @@ sub insert {
     warn "  inserting pkg_svc records" if $DEBUG;
     my $pkg_svc = $options{'pkg_svc'} || {};
     my $hidden_svc = $options{'hidden_svc'} || {};
+    my $provision_hold = $options{'provision_hold'} || {};
     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
       my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
       my $primary_svc =
@@ -261,6 +270,7 @@ sub insert {
         'quantity'    => $quantity, 
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden_svc->{$part_svc->svcpart},
+        'provision_hold' => $provision_hold->{$part_svc->svcpart},
       } );
       my $error = $pkg_svc->insert;
       if ( $error ) {
@@ -335,19 +345,21 @@ sub delete {
 Replaces OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
-Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc> 
-and I<options>
+Currently available options are: I<pkg_svc>, I<hidden_svc>, I<primary_svc>,
+I<provision_hold> and I<options>
 
 If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
 values, the appropriate FS::pkg_svc records will be replaced.  I<hidden_svc>
 can be set to a hashref of svcparts and flag values ('Y' or '') to set the 
-'hidden' field in these records.
+'hidden' field in these records.  I<provision_hold> can be set 
+to a hashref of svcparts and flag values ('Y' or '') to set the field 
+in those records.
 
-If I<primary_svc> is set to the svcpart of the primary service, the appropriate
-FS::pkg_svc record will be updated.
+If I<primary_svc> is set to the svcpart of the primary service, the
+appropriate FS::pkg_svc record will be updated.
 
-If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
-will be replaced.
+If I<options> is set to a hashref, the appropriate FS::part_pkg_option
+records will be replaced.
 
 =cut
 
@@ -447,10 +459,12 @@ sub replace {
   warn "  replacing pkg_svc records" if $DEBUG;
   my $pkg_svc = $options->{'pkg_svc'};
   my $hidden_svc = $options->{'hidden_svc'} || {};
+  my $provision_hold = $options->{'provision_hold'} || {};
   if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs
     foreach my $part_svc ( qsearch('part_svc', {} ) ) {
       my $quantity = $pkg_svc->{$part_svc->svcpart} || 0;
       my $hidden = $hidden_svc->{$part_svc->svcpart} || '';
+      my $provision_hold = $provision_hold->{$part_svc->svcpart} || '';
       my $primary_svc =
         ( defined($options->{'primary_svc'}) && $options->{'primary_svc'}
           && $options->{'primary_svc'} == $part_svc->svcpart
@@ -466,16 +480,19 @@ sub replace {
       my $old_quantity = 0;
       my $old_primary_svc = '';
       my $old_hidden = '';
+      my $old_provision_hold = '';
       if ( $old_pkg_svc ) {
         $old_quantity = $old_pkg_svc->quantity;
         $old_primary_svc = $old_pkg_svc->primary_svc 
           if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed?
         $old_hidden = $old_pkg_svc->hidden;
+        $old_provision_hold = $old_pkg_svc->provision_hold;
       }
    
       next unless $old_quantity != $quantity || 
                   $old_primary_svc ne $primary_svc ||
-                  $old_hidden ne $hidden;
+                  $old_hidden ne $hidden ||
+                  $old_provision_hold ne $provision_hold;
     
       my $new_pkg_svc = new FS::pkg_svc( {
         'pkgsvcnum'   => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ),
@@ -484,6 +501,7 @@ sub replace {
         'quantity'    => $quantity, 
         'primary_svc' => $primary_svc,
         'hidden'      => $hidden,
+        'provision_hold' => $provision_hold,
       } );
       my $error = $old_pkg_svc
                     ? $new_pkg_svc->replace($old_pkg_svc)
@@ -651,6 +669,40 @@ sub check {
   '';
 }
 
+=item check_options
+
+For a passed I<$options> hashref, validates any options that
+have 'validate' subroutines defined in the info hash, 
+then validates the entire hashref if the price plan has 
+its own 'validate' subroutine defined in the info hash 
+(I<$options> values might be altered.)  
+
+Returns error message, or empty string if valid.
+
+Invoked by L</insert> and L</replace> via the equivalent
+methods in L<FS::option_Common>.
+
+=cut
+
+sub check_options {
+  my ($self,$options) = @_;
+  foreach my $option (keys %$options) {
+    if (exists $plans{ $self->plan }->{fields}->{$option}) {
+      if (exists($plans{$self->plan}->{fields}->{$option}->{'validate'})) {
+        # pass option name for use in error message
+        # pass a reference to the $options value, so it can be cleaned up
+        my $error = &{$plans{$self->plan}->{fields}->{$option}->{'validate'}}($option,\($options->{$option}));
+        return $error if $error;
+      }
+    } # else "option does not exist" error?
+  }
+  if (exists($plans{$self->plan}->{'validate'})) {
+    my $error = &{$plans{$self->plan}->{'validate'}}($options);
+    return $error if $error;
+  }
+  return '';
+}
+
 =item supersede OLD [, OPTION => VALUE ... ]
 
 Inserts this package as a successor to the package OLD.  All options are as
@@ -952,19 +1004,19 @@ sub type_pkgs {
 sub pkg_svc {
   my $self = shift;
 
+  return @{ $cache_pkg_svc{$self->pkgpart} }
+    if $cache_enabled && $cache_pkg_svc{$self->pkgpart};
+
 #  #sort { $b->primary cmp $a->primary } 
 #    grep { $_->quantity }
 #      qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
 
   my $opt = ref($_[0]) ? $_[0] : { @_ };
-  my %pkg_svc = map  { $_->svcpart => $_ }
-                grep { $_->quantity }
-                qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
+  my %pkg_svc = map  { $_->svcpart => $_ } $self->_pkg_svc;
 
   unless ( $opt->{disable_linked} ) {
     foreach my $dst_pkg ( map $_->dst_pkg, $self->svc_part_pkg_link ) {
-      my @pkg_svc = grep { $_->quantity }
-                    qsearch( 'pkg_svc', { pkgpart=>$dst_pkg->pkgpart } );
+      my @pkg_svc = $dst_pkg->_pkg_svc;
       foreach my $pkg_svc ( @pkg_svc ) {
         if ( $pkg_svc{$pkg_svc->svcpart} ) {
           my $quantity = $pkg_svc{$pkg_svc->svcpart}->quantity;
@@ -976,8 +1028,23 @@ sub pkg_svc {
     }
   }
 
-  values(%pkg_svc);
+  my @pkg_svc = values(%pkg_svc);
 
+  $cache_pkg_svc{$self->pkgpart} = \@pkg_svc if $cache_enabled;
+
+  @pkg_svc;
+
+}
+
+sub _pkg_svc {
+  my $self = shift;
+  grep { $_->quantity }
+    qsearch({
+      'select'    => 'pkg_svc.*, part_svc.*',
+      'table'     => 'pkg_svc',
+      'addl_from' => 'LEFT JOIN part_svc USING ( svcpart )',
+      'hashref'   => { 'pkgpart' => $self->pkgpart },
+    });
 }
 
 =item svcpart [ SVCDB ]
@@ -1258,12 +1325,10 @@ sub option {
   my( $self, $opt, $ornull ) = @_;
 
   #cache: was pulled up in the original part_pkg query
-  if ( $opt =~ /^(setup|recur)_fee$/ && defined($self->hashref->{"_$opt"}) ) {
-    return $self->hashref->{"_$opt"};
-  }
+  return $self->hashref->{"_opt_$opt"}
+    if exists $self->hashref->{"_opt_$opt"};
 
-  cluck "$self -> option: searching for $opt"
-    if $DEBUG;
+  cluck "$self -> option: searching for $opt" if $DEBUG;
   my $part_pkg_option =
     qsearchs('part_pkg_option', {
       pkgpart    => $self->pkgpart,
@@ -1343,14 +1408,25 @@ sub supp_part_pkg_link {
 
 sub _part_pkg_link {
   my( $self, $type ) = @_;
-  qsearch({ table    => 'part_pkg_link',
-            hashref  => { 'src_pkgpart' => $self->pkgpart,
-                          'link_type'   => $type,
-                          #protection against infinite recursive links
-                          'dst_pkgpart' => { op=>'!=', value=> $self->pkgpart },
-                        },
-            order_by => "ORDER BY hidden",
-         });
+
+  return @{ $cache_link{$type}->{$self->pkgpart} }
+    if $cache_enabled && $cache_link{$type}->{$self->pkgpart};
+
+  cluck $type.'_part_pkg_link called' if $DEBUG;
+
+  my @ppl = 
+    qsearch({ table    => 'part_pkg_link',
+              hashref  => { src_pkgpart => $self->pkgpart,
+                            link_type   => $type,
+                            #protection against infinite recursive links
+                            dst_pkgpart => { op=>'!=', value=> $self->pkgpart },
+                          },
+              order_by => "ORDER BY hidden",
+           });
+
+  $cache_link{$type}->{$self->pkgpart} = \@ppl if $cache_enabled;
+
+  return @ppl;
 }
 
 sub self_and_bill_linked {
@@ -1649,6 +1725,18 @@ sub recur_margin_permonth {
   $self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_);
 }
 
+=item intro_end PACKAGE
+
+Takes an L<FS::cust_pkg> object.  If this plan has an introductory rate,
+returns the expected date the intro period will end. If there is no intro
+rate, returns zero.
+
+=cut
+
+sub intro_end {
+  0;
+}
+
 =item format OPTION DATA
 
 Returns data formatted according to the function 'format' described
@@ -1897,6 +1985,55 @@ sub queueable_upgrade {
     FS::upgrade_journal->set_done($upgrade);
   }
 
+  # remove custom flag from one-time charge packages that were accidentally
+  # flagged as custom
+  $search = FS::Cursor->new({
+    'table'   => 'part_pkg',
+    'hashref' => { 'freq'   => '0',
+                   'custom' => 'Y',
+                   'family_pkgpart' => { op => '!=', value => '' },
+                 },
+    'addl_from' => ' JOIN
+  (select pkgpart from cust_pkg group by pkgpart having count(*) = 1)
+    AS singular_pkg USING (pkgpart)',
+  });
+  my @fields = grep {     $_ ne 'pkgpart'
+                      and $_ ne 'custom'
+                      and $_ ne 'disabled' } FS::part_pkg->fields;
+  PKGPART: while (my $part_pkg = $search->fetch) {
+    # can't merge the package back into its parent (too late for that)
+    # but we can remove the custom flag if it's not actually customized,
+    # i.e. nothing has been changed.
+
+    my $family_pkgpart = $part_pkg->family_pkgpart;
+    next PKGPART if $family_pkgpart == $part_pkg->pkgpart;
+    my $parent_pkg = FS::part_pkg->by_key($family_pkgpart);
+    foreach my $field (@fields) {
+      if ($part_pkg->get($field) ne $parent_pkg->get($field)) {
+        next PKGPART;
+      }
+    }
+    # options have to be identical too
+    # but links, FCC options, discount plans, and usage packages can't be
+    # changed through the "modify charge" UI, so skip them
+    my %newopt = $part_pkg->options;
+    my %oldopt = $parent_pkg->options;
+    OPTION: foreach my $option (keys %newopt) {
+      if (delete $newopt{$option} ne delete $oldopt{$option}) {
+        next PKGPART;
+      }
+    }
+    if (keys(%newopt) or keys(%oldopt)) {
+      next PKGPART;
+    }
+    # okay, now replace it
+    warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n";
+    $part_pkg->set('custom', '');
+    my $error = $part_pkg->replace;
+    die $error if $error;
+  } # $search->fetch
+
+  return;
 }
 
 =item curuser_pkgs_sql