bill usage when cancelling package
[freeside.git] / FS / FS / cust_pkg.pm
index 78f4bed..f564023 100644 (file)
@@ -6,6 +6,7 @@ use Carp qw(cluck);
 use Scalar::Util qw( blessed );
 use List::Util qw(max);
 use Tie::IxHash;
+use MIME::Entity;
 use FS::UID qw( getotaker dbh );
 use FS::Misc qw( send_email );
 use FS::Record qw( qsearch qsearchs );
@@ -121,6 +122,10 @@ Billing item definition (see L<FS::part_pkg>)
 
 Optional link to package location (see L<FS::location>)
 
+=item start_date
+
+date
+
 =item setup
 
 date
@@ -229,6 +234,14 @@ If set true, supresses any referral credit to a referring customer.
 
 cust_pkg_option records will be created
 
+=item ticket_subject
+
+a ticket will be added to this customer with this subject
+
+=item ticket_queue
+
+an optional queue name for ticket additions
+
 =back
 
 =cut
@@ -271,6 +284,29 @@ sub insert {
 
   my $conf = new FS::Conf;
 
+  if ( $conf->config('ticket_system') && $options{ticket_subject} ) {
+    eval '
+      use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" );
+      use RT;
+    ';
+    die $@ if $@;
+
+    RT::LoadConfig();
+    RT::Init();
+    my $q = new RT::Queue($RT::SystemUser);
+    $q->Load($options{ticket_queue}) if $options{ticket_queue};
+    my $t = new RT::Ticket($RT::SystemUser);
+    my $mime = new MIME::Entity;
+    $mime->build( Type => 'text/plain', Data => $options{ticket_subject} );
+    $t->Create( $options{ticket_queue} ? (Queue => $q) : (),
+                Subject => $options{ticket_subject},
+                MIMEObj => $mime,
+              );
+    $t->AddLink( Type   => 'MemberOf',
+                 Target => 'freeside://freeside/cust_main/'. $self->custnum,
+               );
+  }
+
   if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) {
     my $queue = new FS::queue {
       'job'     => 'FS::cust_main::queueable_print',
@@ -447,6 +483,7 @@ sub check {
     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
     || $self->ut_numbern('pkgpart')
     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
+    || $self->ut_numbern('start_date')
     || $self->ut_numbern('setup')
     || $self->ut_numbern('bill')
     || $self->ut_numbern('susp')
@@ -480,10 +517,10 @@ sub check {
     unless ( $disable_agentcheck ) {
       my $agent =
         qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
-      my $pkgpart_href = $agent->pkgpart_hashref;
-      return "agent ". $agent->agentnum.
+      return "agent ". $agent->agentnum. ':'. $agent->agent.
              " can't purchase pkgpart ". $self->pkgpart
-        unless $pkgpart_href->{ $self->pkgpart };
+        unless $agent->pkgpart_hashref->{ $self->pkgpart }
+            || $agent->agentnum == $self->part_pkg->agentnum;
     }
 
     $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
@@ -523,6 +560,8 @@ Available options are:
 
 =item date - can be set to a unix style timestamp to specify when to cancel (expire)
 
+=item nobill - can be set true to skip billing if it might otherwise be done.
+
 =back
 
 If there is an error, returns the error, otherwise returns false.
@@ -533,6 +572,8 @@ sub cancel {
   my( $self, %options ) = @_;
   my $error;
 
+  my $conf = new FS::Conf;
+
   warn "cust_pkg::cancel called with options".
        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
     if $DEBUG;
@@ -558,6 +599,19 @@ sub cancel {
   my $date = $options{date} if $options{date}; # expire/cancel later
   $date = '' if ($date && $date <= time);      # complain instead?
 
+  #race condition: usage could be ongoing until unprovisioned
+  #resolved by performing a change package instead (which unprovisions) and
+  #later cancelling
+  if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) {
+      my $error =
+        $self->cust_main->bill( pkg_list => [ $self ], cancel => 1 );
+      warn "Error billing during cancel, custnum ".
+        #$self->cust_main->custnum. ": $error"
+        ": $error"
+        if $error;
+  }
+
+
   my $cancel_time = $options{'time'} || time;
 
   if ( $options{'reason'} ) {
@@ -1415,13 +1469,15 @@ services.
 sub cust_svc {
   my $self = shift;
 
-  cluck "cust_pkg->cust_svc called" if $DEBUG > 1;
+  return () unless $self->num_cust_svc(@_);
 
   if ( @_ ) {
     return qsearch( 'cust_svc', { 'pkgnum'  => $self->pkgnum,
                                   'svcpart' => shift,          } );
   }
 
+  cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
+
   #if ( $self->{'_svcnum'} ) {
   #  values %{ $self->{'_svcnum'}->cache };
   #} else {
@@ -1494,8 +1550,12 @@ sub num_cust_svc {
   my $self = shift;
 
   return $self->{'_num_cust_svc'}
-    if !@_ && exists($self->{'_num_cust_svc'})
-           && $self->{'_num_cust_svc'} =~ /\d/;
+    if !scalar(@_)
+       && exists($self->{'_num_cust_svc'})
+       && $self->{'_num_cust_svc'} =~ /\d/;
+
+  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 @_;
@@ -1677,8 +1737,8 @@ tie my %statuscolor, 'Tie::IxHash',
 
 sub statuses {
   my $self = shift; #could be class...
-  grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
-                                      # mayble split btw one-time vs. recur
+  #grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
+  #                                    # mayble split btw one-time vs. recur
     keys %statuscolor;
 }
 
@@ -2081,6 +2141,18 @@ sub active_sql { "
   AND ( cust_pkg.susp   IS NULL OR cust_pkg.susp   = 0 )
 "; }
 
+=item not_yet_billed_sql
+
+Returns an SQL expression identifying packages which have not yet been billed.
+
+=cut
+
+sub not_yet_billed_sql { "
+      ( cust_pkg.setup  IS NULL OR 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 )
+"; }
+
 =item inactive_sql
 
 Returns an SQL expression identifying inactive packages (one-time packages
@@ -2090,6 +2162,7 @@ that are otherwise unsuspended/uncancelled).
 
 sub inactive_sql { "
   ". $_[0]->onetime_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 )
 "; }
@@ -2142,6 +2215,10 @@ active, inactive, suspended, cancel (or cancelled)
 
 active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
 
+=item custom
+
+ boolean selects custom packages
+
 =item classnum
 
 =item pkgpart
@@ -2214,8 +2291,13 @@ sub search_sql {
 
     push @where, FS::cust_pkg->active_sql();
 
-  } elsif (    $params->{'magic'}  eq 'inactive'
-            || $params->{'status'} eq 'inactive' ) {
+  } elsif (    $params->{'magic'}  eq 'not yet billed'
+            || $params->{'status'} eq 'not yet billed' ) {
+
+    push @where, FS::cust_pkg->not_yet_billed_sql();
+
+  } elsif (    $params->{'magic'}  =~ /^(one-time charge|inactive)/
+            || $params->{'status'} =~ /^(one-time charge|inactive)/ ) {
 
     push @where, FS::cust_pkg->inactive_sql();
 
@@ -2229,10 +2311,6 @@ sub search_sql {
 
     push @where, FS::cust_pkg->cancelled_sql();
 
-  } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
-
-    push @where, FS::cust_pkg->inactive_sql();
-
   }
 
   ###
@@ -2269,6 +2347,44 @@ sub search_sql {
   #eslaf
 
   ###
+  # parse package report options
+  ###
+
+  my @report_option = ();
+  if ( exists($params->{'report_option'})
+       && $params->{'report_option'} =~ /^([,\d]*)$/
+     )
+  {
+    @report_option = split(',', $1);
+  }
+
+  if (@report_option) {
+    # this will result in the empty set for the dangling comma case as it should
+    push @where, 
+      map{ "0 < ( SELECT count(*) FROM part_pkg_option
+                    WHERE part_pkg_option.pkgpart = part_pkg.pkgpart
+                    AND optionname = 'report_option_$_'
+                    AND optionvalue = '1' )"
+         } @report_option;
+  }
+
+  #eslaf
+
+  ###
+  # parse custom
+  ###
+
+  push @where,  "part_pkg.custom = 'Y'" if $params->{custom};
+
+  ###
+  # parse censustract
+  ###
+
+  if ( $params->{'censustract'} =~ /^([.\d]+)$/ and $1 ) {
+    push @where,  "cust_main.censustract = '". $params->{censustract}. "'";
+  }
+
+  ###
   # parse part_pkg
   ###