add location to svc_phone, RT#7047
[freeside.git] / FS / FS / cust_pkg.pm
index 4724514..dbce6cb 100644 (file)
@@ -1,17 +1,19 @@
 package FS::cust_pkg;
 
 use strict;
-use vars qw(@ISA $disable_agentcheck $DEBUG);
+use base qw( FS::cust_main_Mixin FS::location_Mixin
+             FS::m2m_Common FS::option_Common FS::Record
+           );
+use vars qw(@ISA $disable_agentcheck $DEBUG $me);
 use Carp qw(cluck);
 use Scalar::Util qw( blessed );
 use List::Util qw(max);
 use Tie::IxHash;
+use Time::Local qw( timelocal_nocheck );
 use MIME::Entity;
 use FS::UID qw( getotaker dbh );
 use FS::Misc qw( send_email );
 use FS::Record qw( qsearch qsearchs );
-use FS::m2m_Common;
-use FS::cust_main_Mixin;
 use FS::cust_svc;
 use FS::part_pkg;
 use FS::cust_main;
@@ -38,9 +40,8 @@ use FS::svc_forward;
 # for sending cancel emails in sub cancel
 use FS::Conf;
 
-@ISA = qw( FS::m2m_Common FS::cust_main_Mixin FS::option_Common FS::Record );
-
 $DEBUG = 0;
+$me = '[FS::cust_pkg]';
 
 $disable_agentcheck = 0;
 
@@ -249,6 +250,26 @@ an optional queue name for ticket additions
 sub insert {
   my( $self, %options ) = @_;
 
+  if ( $self->part_pkg->option('start_1st') && !$self->start_date ) {
+    my ($sec,$min,$hour,$mday,$mon,$year) = (localtime(time) )[0,1,2,3,4,5];
+    $mon += 1 unless $mday == 1;
+    until ( $mon < 12 ) { $mon -= 12; $year++; }
+    $self->start_date( timelocal_nocheck(0,0,0,1,$mon,$year) );
+  }
+
+  my $expire_months = $self->part_pkg->option('expire_months');
+  if ( $expire_months && !$self->expire ) {
+    my $start = $self->start_date || $self->setup || time;
+
+    #false laziness w/part_pkg::add_freq
+    my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($start) )[0,1,2,3,4,5];
+    $mon += $expire_months;
+    until ( $mon < 12 ) { $mon -= 12; $year++; }
+
+    #$self->expire( timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year) );
+    $self->expire( timelocal_nocheck(0,0,0,$mday,$mon,$year) );
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -603,8 +624,9 @@ sub cancel {
   #resolved by performing a change package instead (which unprovisions) and
   #later cancelling
   if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+      my $copy = $self->new({$self->hash});
       my $error =
-        $self->cust_main->bill( pkg_list => [ $self ], cancel => 1 );
+        $copy->cust_main->bill( pkg_list => [ $copy ], cancel => 1 );
       warn "Error billing during cancel, custnum ".
         #$self->cust_main->custnum. ": $error"
         ": $error"
@@ -1188,13 +1210,14 @@ sub change {
   }
 
   #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');
+      if $part_pkg->can('reset_usage') && ! $part_pkg->option('usage_rollover');
 
     if ($error) {
       $dbh->rollback if $oldAutoCommit;
@@ -1205,11 +1228,21 @@ sub change {
   #Good to go, cancel old package.
   $error = $self->cancel( quiet=>1 );
   if ($error) {
-    $dbh->rollback;
+    $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;
 
 }
@@ -1522,8 +1555,11 @@ sub h_cust_svc {
 sub _sort_cust_svc {
   my( $self, $arrayref ) = @_;
 
+  my $sort =
+    sub ($$) { my ($a, $b) = @_; $b->[1] cmp $a->[1]  or  $a->[2] <=> $b->[2] };
+
   map  { $_->[0] }
-  sort { $b->[1] cmp $a->[1]  or  $a->[2] <=> $b->[2] } 
+  sort $sort
   map {
         my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
                                              'svcpart' => $_->svcpart     } );
@@ -1835,6 +1871,19 @@ sub h_labels {
   map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
 }
 
+=item labels_short
+
+Like labels, except returns a simple flat list, and shortens long
+(currently >5 or the cust_bill-max_same_services configuration value) lists of
+identical services to one line that lists the service label and the number of
+individual services rather than individual items.
+
+=cut
+
+sub labels_short {
+  shift->_labels_short( 'labels', @_ );
+}
+
 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
 
 Like h_labels, except returns a simple flat list, and shortens long
@@ -1845,7 +1894,11 @@ individual services rather than individual items.
 =cut
 
 sub h_labels_short {
-  my $self = shift;
+  shift->_labels_short( 'h_labels', @_ );
+}
+
+sub _labels_short {
+  my( $self, $method ) = ( shift, shift );
 
   my $conf = new FS::Conf;
   my $max_same_services = $conf->config('cust_bill-max_same_services') || 5;
@@ -1862,7 +1915,18 @@ sub h_labels_short {
     if ( $num > $max_same_services ) {
       push @labels, "$label ($num)";
     } else {
-      push @labels, map { "$label: $_" } @values;
+      if ( $conf->exists('cust_bill-consolidate_services') ) {
+        # push @labels, "$label: ". join(', ', @values);
+        while ( @values ) {
+          my $detail = "$label: ";
+          $detail .= shift(@values). ', '
+            while @values && length($detail.$values[0]) < 78;
+          $detail =~ s/, $//;
+          push @labels, $detail;
+        }
+      } else {
+        push @labels, map { "$label: $_" } @values;
+      }
     }
   }
 
@@ -1881,29 +1945,24 @@ sub cust_main {
   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
 }
 
+#these subs are in location_Mixin.pm now... unfortunately the POD doesn't mixin
+
 =item cust_location
 
 Returns the location object, if any (see L<FS::cust_location>).
 
-=cut
-
-sub cust_location {
-  my $self = shift;
-  return '' unless $self->locationnum;
-  qsearchs( 'cust_location', { 'locationnum' => $self->locationnum } );
-}
-
 =item cust_location_or_main
 
 If this package is associated with a location, returns the locaiton (see
 L<FS::cust_location>), otherwise returns the customer (see L<FS::cust_main>).
 
+=item location_label [ OPTION => VALUE ... ]
+
+Returns the label of the location object (see L<FS::cust_location>).
+
 =cut
 
-sub cust_location_or_main {
-  my $self = shift;
-  $self->cust_location || $self->cust_main;
-}
+#end of subs in location_Mixin.pm now... unfortunately the POD doesn't mixin
 
 =item seconds_since TIMESTAMP
 
@@ -2191,6 +2250,7 @@ Returns an SQL expression identifying active packages.
 
 sub active_sql { "
   ". $_[0]->recurring_sql(). "
+  AND cust_pkg.setup IS NOT NULL AND cust_pkg.setup != 0
   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
   AND ( cust_pkg.susp   IS NULL OR cust_pkg.susp   = 0 )
 "; }
@@ -2250,7 +2310,7 @@ sub cancel_sql {
   "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
 }
 
-=item search_sql HASHREF
+=item search HASHREF
 
 (Class method)
 
@@ -2277,7 +2337,7 @@ active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
 
 =item pkgpart
 
-list specified how?
+pkgpart or arrayref or hashref of pkgparts
 
 =item setup
 
@@ -2323,7 +2383,7 @@ specifies the user for agent virtualization
 
 =cut
 
-sub search_sql { 
+sub search {
   my ($class, $params) = @_;
   my @where = ();
 
@@ -2337,6 +2397,15 @@ sub search_sql {
   }
 
   ##
+  # parse custnum
+  ##
+
+  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where,
+      "cust_pkg.custnum = $1";
+  }
+
+  ##
   # parse status
   ##
 
@@ -2345,8 +2414,8 @@ sub search_sql {
 
     push @where, FS::cust_pkg->active_sql();
 
-  } elsif (    $params->{'magic'}  eq 'not yet billed'
-            || $params->{'status'} eq 'not yet billed' ) {
+  } elsif (    $params->{'magic'}  =~ /^not[ _]yet[ _]billed$/
+            || $params->{'status'} =~ /^not[ _]yet[ _]billed$/ ) {
 
     push @where, FS::cust_pkg->not_yet_billed_sql();
 
@@ -2380,7 +2449,7 @@ sub search_sql {
   {
     $classnum = $1;
     if ( $classnum ) { #a specific class
-      push @where, "classnum = $classnum";
+      push @where, "part_pkg.classnum = $classnum";
 
       #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
       #die "classnum $classnum not found!" unless $pkg_class[0];
@@ -2388,7 +2457,7 @@ sub search_sql {
 
     } elsif ( $classnum eq '' ) { #the empty class
 
-      push @where, "classnum IS NULL";
+      push @where, "part_pkg.classnum IS NULL";
       #$title .= 'Empty class ';
       #@pkg_class = ( '(empty class)' );
     } elsif ( $classnum eq '0' ) {
@@ -2445,9 +2514,24 @@ sub search_sql {
   # parse part_pkg
   ###
 
-  my $pkgpart = join (' OR pkgpart=',
-                      grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
-  push @where,  '(pkgpart=' . $pkgpart . ')' if $pkgpart;
+  if ( ref($params->{'pkgpart'}) ) {
+
+    my @pkgpart = ();
+    if ( ref($params->{'pkgpart'}) eq 'HASH' ) {
+      @pkgpart = grep $params->{'pkgpart'}{$_}, keys %{ $params->{'pkgpart'} };
+    } elsif ( ref($params->{'pkgpart'}) eq 'ARRAY' ) {
+      @pkgpart = @{ $params->{'pkgpart'} };
+    } else {
+      die 'unhandled pkgpart ref '. $params->{'pkgpart'};
+    }
+
+    @pkgpart = grep /^(\d+)$/, @pkgpart;
+
+    push @where, 'pkgpart IN ('. join(',', @pkgpart). ')' if scalar(@pkgpart);
+
+  } elsif ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
+    push @where, "pkgpart = $1";
+  } 
 
   ###
   # parse dates
@@ -2528,18 +2612,18 @@ sub search_sql {
 
     if ($access_user) {
       push @where, $access_user->agentnums_sql('table'=>'cust_main');
-    }else{
+    } else {
       push @where, "1=0";
     }
-  }else{
+  } else {
     push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main');
   }
 
   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
 
-  my $addl_from = 'LEFT JOIN cust_main USING ( custnum  ) '.
-                  'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
-                  'LEFT JOIN pkg_class USING ( classnum ) ';
+  my $addl_from = 'LEFT JOIN part_pkg  USING ( pkgpart  ) '.
+                  'LEFT JOIN pkg_class USING ( classnum ) '.
+                  'LEFT JOIN cust_main USING ( custnum  ) ';
 
   my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
 
@@ -2703,6 +2787,9 @@ sub order {
 #  my $cust_main = qsearchs('cust_main', { custnum => $custnum });
 #  return "Customer not found: $custnum" unless $cust_main;
 
+  warn "$me order: pkgnums to remove: ". join(',', @$remove_pkgnum). "\n"
+    if $DEBUG;
+
   my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
                          @$remove_pkgnum;
 
@@ -2711,6 +2798,10 @@ sub order {
   my %hash = (); 
   if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
 
+    warn "$me order: changing pkgnum ". $old_cust_pkg[0]->pkgnum.
+         " to pkgpart ". $pkgparts->[0]. "\n"
+      if $DEBUG;
+
     my $err_or_cust_pkg =
       $old_cust_pkg[0]->change( 'pkgpart' => $pkgparts->[0],
                                 'refnum'  => $refnum,
@@ -2722,12 +2813,16 @@ sub order {
     }
 
     push @$return_cust_pkg, $err_or_cust_pkg;
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
     return '';
 
   }
 
   # Create the new packages.
   foreach my $pkgpart (@$pkgparts) {
+
+    warn "$me order: inserting pkgpart $pkgpart\n" if $DEBUG;
+
     my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
                                       pkgpart => $pkgpart,
                                       refnum  => $refnum,
@@ -2746,6 +2841,9 @@ sub order {
   # Transfer services and cancel old packages.
   foreach my $old_pkg (@old_cust_pkg) {
 
+    warn "$me order: transferring services from pkgnum ". $old_pkg->pkgnum. "\n"
+      if $DEBUG;
+
     foreach my $new_pkg (@$return_cust_pkg) {
       $error = $old_pkg->transfer($new_pkg);
       if ($error and $error == 0) {