Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / cust_main / Packages.pm
index 4b44fdd..152c496 100644 (file)
@@ -4,9 +4,11 @@ use strict;
 use vars qw( $DEBUG $me );
 use List::Util qw( min );
 use FS::UID qw( dbh );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch qsearchs );
 use FS::cust_pkg;
 use FS::cust_svc;
+use FS::contact;       # for attach_pkgs
+use FS::cust_location; #
 
 $DEBUG = 0;
 $me = '[FS::cust_main::Packages]';
@@ -17,7 +19,7 @@ FS::cust_main::Packages - Packages mixin for cust_main
 
 =head1 SYNOPSIS
 
-=head1 DESRIPTION
+=head1 DESCRIPTION
 
 These methods are available on FS::cust_main objects;
 
@@ -29,6 +31,9 @@ These methods are available on FS::cust_main objects;
 
 Orders a single package.
 
+Note that if the package definition has supplemental packages, those will
+be ordered as well.
+
 Options may be passed as a list of key/value pairs or as a hash reference.
 Options are:
 
@@ -40,7 +45,8 @@ FS::cust_pkg object
 
 =item cust_location
 
-Optional FS::cust_location object
+Optional FS::cust_location object.  If not specified, the customer's 
+ship_location will be used.
 
 =item svcs
 
@@ -57,7 +63,7 @@ action completes (such as running the customer's credit card successfully).
 
 Optional subject for a ticket created and attached to this customer
 
-=item ticket_subject
+=item ticket_queue
 
 Optional queue name for ticket additions
 
@@ -83,7 +89,7 @@ sub order_pkg {
     if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
 
   my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
-                          qw( ticket_subject ticket_queue );
+                          qw( ticket_subject ticket_queue allow_pkgpart );
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -96,14 +102,45 @@ sub order_pkg {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  if ( $opt->{'cust_location'} &&
-       ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
-    my $error = $opt->{'cust_location'}->insert;
+  if ( $opt->{'contactnum'} and $opt->{'contactnum'} != -1 ) {
+
+    $cust_pkg->contactnum($opt->{'contactnum'});
+
+  } elsif ( $opt->{'contact'} ) {
+
+    if ( ! $opt->{'contact'}->contactnum ) {
+      # not inserted yet
+      my $error = $opt->{'contact'}->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "inserting contact (transaction rolled back): $error";
+      }
+    }
+    $cust_pkg->contactnum($opt->{'contact'}->contactnum);
+
+  #} else {
+  #
+  #  $cust_pkg->contactnum();
+
+  }
+
+  if ( $opt->{'locationnum'} and $opt->{'locationnum'} != -1 ) {
+
+    $cust_pkg->locationnum($opt->{'locationnum'});
+
+  } elsif ( $opt->{'cust_location'} ) {
+
+    my $error = $opt->{'cust_location'}->find_or_insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "inserting cust_location (transaction rolled back): $error";
     }
     $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
+
+  } else {
+
+    $cust_pkg->locationnum($self->ship_locationnum);
+
   }
 
   $cust_pkg->custnum( $self->custnum );
@@ -137,6 +174,35 @@ sub order_pkg {
     }
   }
 
+  # add supplemental packages, if any are needed
+  my $part_pkg = FS::part_pkg->by_key($cust_pkg->pkgpart);
+  foreach my $link ($part_pkg->supp_part_pkg_link) {
+    #warn "inserting supplemental package ".$link->dst_pkgpart;
+    my $pkg = FS::cust_pkg->new({
+        'pkgpart'       => $link->dst_pkgpart,
+        'pkglinknum'    => $link->pkglinknum,
+        'custnum'       => $self->custnum,
+        'main_pkgnum'   => $cust_pkg->pkgnum,
+        # try to prevent as many surprises as possible
+        'pkgbatch'      => $cust_pkg->pkgbatch,
+        'start_date'    => $cust_pkg->start_date,
+        'order_date'    => $cust_pkg->order_date,
+        'expire'        => $cust_pkg->expire,
+        'adjourn'       => $cust_pkg->adjourn,
+        'contract_end'  => $cust_pkg->contract_end,
+        'refnum'        => $cust_pkg->refnum,
+        'discountnum'   => $cust_pkg->discountnum,
+        'waive_setup'   => $cust_pkg->waive_setup,
+        'allow_pkgpart' => $opt->{'allow_pkgpart'},
+    });
+    $error = $self->order_pkg('cust_pkg' => $pkg,
+                              'locationnum' => $cust_pkg->locationnum);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting supplemental package: $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
@@ -224,6 +290,108 @@ sub order_pkgs {
   ''; #no error
 }
 
+=item attach_pkgs 
+
+Merges this customer's package's into the target customer and then cancels them.
+
+=cut
+
+sub attach_pkgs {
+  my( $self, $new_custnum ) = @_;
+
+  #mostly false laziness w/ merge
+
+  return "Can't attach packages to self" if $self->custnum == $new_custnum;
+
+  my $new_cust_main = qsearchs( 'cust_main', { 'custnum' => $new_custnum } )
+    or return "Invalid new customer number: $new_custnum";
+
+  return 'Access denied: "Merge customer across agents" access right required to merge into a customer of a different agent'
+    if $self->agentnum != $new_cust_main->agentnum 
+    && ! $FS::CurrentUser::CurrentUser->access_right('Merge customer across agents');
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a master agent customer";
+  }
+
+  #use FS::access_user
+  if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a master employee customer";
+  }
+
+  if ( qsearch('cust_pay_pending', { 'custnum' => $self->custnum,
+                                     'status'  => { op=>'!=', value=>'done' },
+                                   }
+              )
+  ) {
+     $dbh->rollback if $oldAutoCommit;
+     return "Can't merge a customer with pending payments";
+  }
+
+  #end of false laziness
+
+  #pull in contact
+
+  my %contact_hash = ( 'first'    => $self->first,
+                       'last'     => $self->get('last'),
+                       'custnum'  => $new_custnum,
+                       'disabled' => '',
+                     );
+
+  my $contact = qsearchs(  'contact', \%contact_hash)
+                 || new FS::contact   \%contact_hash;
+  unless ( $contact->contactnum ) {
+    my $error = $contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+
+    my $cust_location = $cust_pkg->cust_location || $self->ship_location;
+    my %loc_hash = $cust_location->hash;
+    $loc_hash{'locationnum'} = '';
+    $loc_hash{'custnum'}     = $new_custnum;
+    $loc_hash{'disabled'}    = '';
+    my $new_cust_location = qsearchs(  'cust_location', \%loc_hash)
+                             || new FS::cust_location   \%loc_hash;
+
+    my $pkg_or_error = $cust_pkg->change( {
+      'keep_dates'    => 1,
+      'cust_main'     => $new_cust_main,
+      'contactnum'    => $contact->contactnum,
+      'cust_location' => $new_cust_location,
+    } );
+
+    my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
+}
+
 =item all_pkgs [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
 
 Returns all packages (see L<FS::cust_pkg>) for this customer.
@@ -351,6 +519,7 @@ Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
 
 sub suspended_pkgs {
   my $self = shift;
+  return $self->num_suspended_pkgs unless wantarray;
   grep { $_->susp } $self->ncancelled_pkgs;
 }
 
@@ -377,6 +546,7 @@ this customer.
 
 sub unsuspended_pkgs {
   my $self = shift;
+  return $self->num_unsuspended_pkgs unless wantarray;
   grep { ! $_->susp } $self->ncancelled_pkgs;
 }
 
@@ -395,16 +565,36 @@ sub active_pkgs {
        $self->unsuspended_pkgs;
 }
 
+=item billing_pkgs
+
+Returns active packages, and also any suspended packages which are set to
+continue billing while suspended.
+
+=cut
+
+sub billing_pkgs {
+  my $self = shift;
+  grep { my $part_pkg = $_->part_pkg;
+         $part_pkg->freq ne '' && $part_pkg->freq ne '0'
+           && ( ! $_->susp || $_->option('suspend_bill',1)
+                           || ( $part_pkg->option('suspend_bill', 1)
+                                  && ! $_->option('no_suspend_bill',1)
+                              )
+              );
+       }
+       $self->ncancelled_pkgs;
+}
+
 =item next_bill_date
 
 Returns the next date this customer will be billed, as a UNIX timestamp, or
-undef if no active package has a next bill date.
+undef if no billing package has a next bill date.
 
 =cut
 
 sub next_bill_date {
   my $self = shift;
-  min( map $_->get('bill'), grep $_->get('bill'), $self->active_pkgs );
+  min( map $_->get('bill'), grep $_->get('bill'), $self->billing_pkgs );
 }
 
 =item num_cancelled_pkgs
@@ -422,6 +612,16 @@ sub num_ncancelled_pkgs {
   shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
 }
 
+sub num_suspended_pkgs {
+  shift->num_pkgs("     ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+                    AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0   ");
+}
+
+sub num_unsuspended_pkgs {
+  shift->num_pkgs("     ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+                    AND ( cust_pkg.susp   IS NULL OR cust_pkg.susp   = 0 ) ");
+}
+
 sub num_pkgs {
   my( $self ) = shift;
   my $sql = scalar(@_) ? shift : '';