query billing events to see affected objects, #15142
[freeside.git] / FS / FS / cust_pkg.pm
index 4631504..88ce3a0 100644 (file)
@@ -605,6 +605,7 @@ sub check {
     || $self->ut_numbern('susp')
     || $self->ut_numbern('cancel')
     || $self->ut_numbern('adjourn')
+    || $self->ut_numbern('resume')
     || $self->ut_numbern('expire')
     || $self->ut_numbern('dundate')
     || $self->ut_enum('no_auto', [ '', 'Y' ])
@@ -618,6 +619,9 @@ sub check {
   return "A package with both start date (future start) and setup date (already started) will never bill"
     if $self->start_date && $self->setup;
 
+  return "A future unsuspend date can only be set for a package with a suspend date"
+    if $self->resume and !$self->susp and !$self->adjourn;
+
   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
 
   if ( $self->dbdef_table->column('manual_flag') ) {
@@ -775,7 +779,7 @@ sub cancel {
     #schwartz
     map  { $_->[0] }
     sort { $a->[1] <=> $b->[1] }
-    map  { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
+    map  { [ $_, $_->svc_x ? $_->svc_x->table_info->{'cancel_weight'} : -1 ]; }
     qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
   ) {
     my $part_svc = $cust_svc->part_svc;
@@ -940,9 +944,21 @@ Available options are:
 
 =over 4
 
-=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.
+=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.
+
+=item date - can be set to a unix style timestamp to specify when to 
+suspend (adjourn)
 
-=item date - can be set to a unix style timestamp to specify when to suspend (adjourn)
+=item time - can be set to override the current time, for calculation 
+of final invoices or unused-time credits
+
+=item resume_date - can be set to a time when the package should be 
+unsuspended.  This may be more convenient than calling C<unsuspend()>
+separately.
 
 =back
 
@@ -978,16 +994,16 @@ sub suspend {
     return "";  # no error                     # complain on adjourn?
   }
 
+  my $suspend_time = $options{'time'} || time;
+
   my $date = $options{date} if $options{date}; # adjourn/suspend later
-  $date = '' if ($date && $date <= time);      # complain instead?
+  $date = '' if ($date && $date <= $suspend_time); # complain instead?
 
   if ( $date && $old->get('expire') && $old->get('expire') < $date ) {
     dbh->rollback if $oldAutoCommit;
     return "Package $pkgnum expires before it would be suspended.";
   }
 
-  my $suspend_time = $options{'time'} || time;
-
   if ( $options{'reason'} ) {
     $error = $self->insert_reason( 'reason' => $options{'reason'},
                                    'action' => $date ? 'adjourn' : 'suspend',
@@ -1006,6 +1022,12 @@ sub suspend {
   } else {
     $hash{'susp'} = $suspend_time;
   }
+
+  my $resume_date = $options{'resume_date'} || 0;
+  if ( $resume_date > ($date || $suspend_time) ) {
+    $hash{'resume'} = $resume_date;
+  }
+
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1083,6 +1105,11 @@ Available options are:
 
 =over 4
 
+=item date
+
+Can be set to a date to unsuspend the package in the future (the 'resume' 
+field).
+
 =item adjust_next_bill
 
 Can be set true to adjust the next bill date forward by
@@ -1117,15 +1144,38 @@ sub unsuspend {
 
   my $pkgnum = $old->pkgnum;
   if ( $old->get('cancel') || $self->get('cancel') ) {
-    dbh->rollback if $oldAutoCommit;
+    $dbh->rollback if $oldAutoCommit;
     return "Can't unsuspend cancelled package $pkgnum";
   }
 
   unless ( $old->get('susp') && $self->get('susp') ) {
-    dbh->rollback if $oldAutoCommit;
+    $dbh->rollback if $oldAutoCommit;
     return "";  # no error                     # complain instead?
   }
 
+  my $date = $opt{'date'};
+  if ( $date and $date > time ) { # return an error if $date <= time?
+
+    if ( $old->get('expire') && $old->get('expire') < $date ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Package $pkgnum expires before it would be unsuspended.";
+    }
+
+    my $new = new FS::cust_pkg { $self->hash };
+    $new->set('resume', $date);
+    $error = $new->replace($self, options => $self->options);
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    else {
+      $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+      return '';
+    }
+  
+  } #if $date 
+
   foreach my $cust_svc (
     qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
   ) {
@@ -1166,7 +1216,8 @@ sub unsuspend {
   }
 
   $hash{'susp'} = '';
-  $hash{'adjourn'} = '' if $hash{'adjourn'} < time;
+  $hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time;
+  $hash{'resume'} = '' if !$hash{'adjourn'};
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1224,6 +1275,7 @@ sub unadjourn {
 
   my %hash = $self->hash;
   $hash{'adjourn'} = '';
+  $hash{'resume'}  = '';
   my $new = new FS::cust_pkg ( \%hash );
   $error = $new->replace( $self, options => { $self->options } );
   if ( $error ) {
@@ -1335,7 +1387,7 @@ sub change {
   my $unused_credit = 0;
   if ( $opt->{'keep_dates'} ) {
     foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire 
-                          start_date contract_end ) ) {
+                          resume start_date contract_end ) ) {
       $hash{$date} = $self->getfield($date);
     }
   }
@@ -1350,12 +1402,16 @@ sub change {
     $hash{$_} = '' foreach qw(setup bill last_bill);
   }
 
+  # 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'} || $self->locationnum  ),
+    locationnum  => ( $opt->{'locationnum'}                        ),
     %hash,
   };
 
@@ -1736,11 +1792,13 @@ sub num_cust_event {
   $sth->fetchrow_arrayref->[0];
 }
 
-=item cust_svc [ SVCPART ]
+=item cust_svc [ SVCPART ] (old, deprecated usage)
+
+=item cust_svc [ OPTION => VALUE ... ] (current usage)
 
 Returns the services for this package, as FS::cust_svc objects (see
-L<FS::cust_svc>).  If a svcpart is specified, return only the matching
-services.
+L<FS::cust_svc>).  Available options are svcpart and svcdb.  If either is
+spcififed, returns only the matching services.
 
 =cut
 
@@ -1749,9 +1807,25 @@ sub cust_svc {
 
   return () unless $self->num_cust_svc(@_);
 
-  if ( @_ ) {
-    return qsearch( 'cust_svc', { 'pkgnum'  => $self->pkgnum,
-                                  'svcpart' => shift,          } );
+  my %opt = ();
+  if ( @_ && $_[0] =~ /^\d+/ ) {
+    $opt{svcpart} = shift;
+  } elsif ( @_ && ref($_[0]) eq 'HASH' ) {
+    %opt = %{ $_[0] };
+  } elsif ( @_ ) {
+    %opt = @_;
+  }
+
+  my %search = (
+    'table'   => 'cust_svc',
+    'hashref' => { 'pkgnum' => $self->pkgnum },
+  );
+  if ( $opt{svcpart} ) {
+    $search{hashref}->{svcpart} = $opt{'svcpart'};
+  }
+  if ( $opt{'svcdb'} ) {
+    $search{addl_from} = ' LEFT JOIN part_svc USING ( svcpart ) ';
+    $search{hashref}->{svcdb} = $opt{'svcdb'};
   }
 
   cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
@@ -1759,9 +1833,7 @@ sub cust_svc {
   #if ( $self->{'_svcnum'} ) {
   #  values %{ $self->{'_svcnum'}->cache };
   #} else {
-    $self->_sort_cust_svc(
-      [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
-    );
+    $self->_sort_cust_svc( [ qsearch(\%search) ] );
   #}
 
 }
@@ -1829,10 +1901,12 @@ sub _sort_cust_svc {
 
 }
 
-=item num_cust_svc [ SVCPART ]
+=item num_cust_svc [ SVCPART ] (old, deprecated usage)
+
+=item num_cust_svc [ OPTION => VALUE ... ] (current usage)
 
-Returns the number of provisioned services for this package.  If a svcpart is
-specified, counts only the matching services.
+Returns the number of services for this package.  Available options are svcpart
+and svcdb.  If either is spcififed, returns only the matching services.
 
 =cut
 
@@ -1847,11 +1921,31 @@ sub num_cust_svc {
   cluck "cust_pkg->num_cust_svc called, _num_cust_svc:".$self->{'_num_cust_svc'}
     if $DEBUG > 2;
 
-  my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
-  $sql .= ' AND svcpart = ?' if @_;
+  my %opt = ();
+  if ( @_ && $_[0] =~ /^\d+/ ) {
+    $opt{svcpart} = shift;
+  } elsif ( @_ && ref($_[0]) eq 'HASH' ) {
+    %opt = %{ $_[0] };
+  } elsif ( @_ ) {
+    %opt = @_;
+  }
+
+  my $select = 'SELECT COUNT(*) FROM cust_svc ';
+  my $where = ' WHERE pkgnum = ? ';
+  my @param = ($self->pkgnum);
+
+  if ( $opt{'svcpart'} ) {
+    $where .= ' AND svcpart = ? ';
+    push @param, $opt{'svcpart'};
+  }
+  if ( $opt{'svcdb'} ) {
+    $select .= ' LEFT JOIN part_svc USING ( svcpart ) ';
+    $where .= ' AND svcdb = ? ';
+    push @param, $opt{'svcdb'};
+  }
 
-  my $sth = dbh->prepare($sql)     or die  dbh->errstr;
-  $sth->execute($self->pkgnum, @_) or die $sth->errstr;
+  my $sth = dbh->prepare("$select $where") or die  dbh->errstr;
+  $sth->execute(@param) or die $sth->errstr;
   $sth->fetchrow_arrayref->[0];
 }
 
@@ -1921,7 +2015,7 @@ sub part_svc {
       max( 0, $pkg_svc->quantity - $num_cust_svc );
     $part_svc->{'Hash'}{'cust_pkg_svc'} =
         $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : []
-      unless exists($opt{summarize_size}) && $opt{summarize_size}
+      unless exists($opt{summarize_size}) && $opt{summarize_size} > 0
           && $num_cust_svc >= $opt{summarize_size};
     $part_svc->{'Hash'}{'hidden'} = $pkg_svc->hidden;
     $part_svc;
@@ -2652,7 +2746,8 @@ All svc_accts which are part of this package have their values reset.
 sub set_usage {
   my ($self, $valueref, %opt) = @_;
 
-  foreach my $cust_svc ($self->cust_svc){
+  #only svc_acct can set_usage for now
+  foreach my $cust_svc ( $self->cust_svc( 'svcdb'=>'svc_acct' ) ) {
     my $svc_x = $cust_svc->svc_x;
     $svc_x->set_usage($valueref, %opt)
       if $svc_x->can("set_usage");
@@ -2672,7 +2767,8 @@ All svc_accts which are part of this package have their values incremented.
 sub recharge {
   my ($self, $valueref) = @_;
 
-  foreach my $cust_svc ($self->cust_svc){
+  #only svc_acct can set_usage for now
+  foreach my $cust_svc ( $self->cust_svc( 'svcdb'=>'svc_acct' ) ) {
     my $svc_x = $cust_svc->svc_x;
     $svc_x->recharge($valueref)
       if $svc_x->can("recharge");
@@ -2994,7 +3090,6 @@ sub search {
       } elsif ( @c_where ) {
         push @where, ' ( '. join(' OR ', @c_where). ' ) ';
       }
-      warn $where[-1];
 
     }
     
@@ -3226,7 +3321,8 @@ sub search {
                                   $params->{'cust_fields'}
                                 ),
                      ),
-    'extra_sql'   => "$extra_sql $orderby",
+    'extra_sql'   => $extra_sql,
+    'order_by'    => $orderby,
     'addl_from'   => $addl_from,
     'count_query' => $count_query,
   };