Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Mon, 9 Jul 2012 05:45:58 +0000 (22:45 -0700)
committerIvan Kohler <ivan@freeside.biz>
Mon, 9 Jul 2012 05:45:58 +0000 (22:45 -0700)
Conflicts:
httemplate/misc/process/cancel_pkg.html

1  2 
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Packages.pm
FS/FS/cust_pkg.pm
FS/FS/domain_record.pm
bin/23commit
fs_selfservice/FS-SelfService/cgi/myaccount.html
fs_selfservice/FS-SelfService/cgi/provision_list.html
httemplate/edit/radius_group.html
httemplate/misc/cancel_pkg.html
httemplate/misc/process/cancel_pkg.html
httemplate/view/cust_main/packages/status.html

@@@ -721,6 -721,11 +721,11 @@@ jurisdictions (i.e. Texas) have tax exe
  sub calculate_taxes {
    my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
  
+   # $taxlisthash is a hashref
+   # keys are identifiers, values are arrayrefs
+   # each arrayref starts with a tax object (cust_main_county or tax_rate)
+   # then any cust_bill_pkg objects the tax applies to
    local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
  
    warn "$me calculate_taxes\n"
    my %tax_rate_location = ();
  
    foreach my $tax ( keys %$taxlisthash ) {
+     # $tax is a tax identifier
      my $tax_object = shift @{ $taxlisthash->{$tax} };
+     # $tax_object is a cust_main_county or tax_rate 
+     # (with pkgnum and locationnum set)
+     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg objects
      warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
      warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
+     # taxline calculates the tax on all cust_bill_pkgs in the 
+     # first (arrayref) argument
      my $hashref_or_error =
        $tax_object->taxline( $taxlisthash->{$tax},
                              'custnum'      => $self->custnum,
  
      $tax{ $tax } += $amount;
  
+     # link records between cust_main_county/tax_rate and cust_location
      $tax_location{ $tax } ||= [];
-     if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
+     $tax_rate_location{ $tax } ||= [];
+     if ( ref($tax_object) eq 'FS::cust_main_county' ) {
        push @{ $tax_location{ $tax }  },
          {
            'taxnum'      => $tax_object->taxnum, 
            'amount'      => sprintf('%.2f', $amount ),
          };
      }
-     $tax_rate_location{ $tax } ||= [];
-     if ( ref($tax_object) eq 'FS::tax_rate' ) {
+     elsif ( ref($tax_object) eq 'FS::tax_rate' ) {
        my $taxratelocationnum =
          $tax_object->tax_rate_location->taxratelocationnum;
        push @{ $tax_rate_location{ $tax }  },
@@@ -877,7 -888,7 +888,7 @@@ sub _make_lines 
  
    my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
    my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
-   my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
+   my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
    my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
    my $total_setup = $params{setup} or die "no setup accumulator specified";
    my $total_recur = $params{recur} or die "no recur accumulator specified";
    # bill recurring fee
    ### 
  
-   #XXX unit stuff here too
    my $recur = 0;
    my $unitrecur = 0;
    my @recur_discounts = ();
    my $sdate;
    if (     ! $cust_pkg->start_date
 -       and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) )
 +       and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1)
 +                               || ( $part_pkg->option('suspend_bill', 1) )
 +                                     && ! $cust_pkg->option('no_suspend_bill',1)
 +                                  )
         and
              ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
           || ( $part_pkg->plan eq 'voip_cdr'
      return "$@ running $method for $cust_pkg\n"
        if ( $@ );
  
+     #base_cancel???
+     $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh
      if ( $increment_next_bill ) {
  
        my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
@@@ -1181,7 -1191,11 +1194,11 @@@ sub _handle_taxes 
    push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
    push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
  
-   if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
+   my $exempt = $conf->exists('cust_class-tax_exempt')
+                  ? ( $self->cust_class ? $self->cust_class->tax : '' )
+                  : $self->tax;
+   if ( $exempt !~ /Y/i && $self->payby ne 'COMP' ) {
  
      if ( $conf->exists('enable_taxproducts')
           && ( scalar($part_pkg->part_pkg_taxoverride)
      } else {
  
        my @loc_keys = qw( district city county state country );
-       my %taxhash;
-       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
-         my $cust_location = $cust_pkg->cust_location;
-         %taxhash = map { $_ => $cust_location->$_()    } @loc_keys;
-       } else {
-         my $prefix = 
-           ( $conf->exists('tax-ship_address') && length($self->ship_last) )
-           ? 'ship_'
-           : '';
-         %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
-       }
+       my $location = $cust_pkg->tax_location;
+       my %taxhash = map { $_ => $location->$_ } @loc_keys;
  
        $taxhash{'taxclass'} = $part_pkg->taxclass;
  
-       my @taxes = ();
+       my @taxes = (); # entries are cust_main_county objects
        my %taxhash_elim = %taxhash;
        my @elim = qw( district city county state );
        do { 
                      @taxes
          if $self->cust_main_exemption; #just to be safe
  
-       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
-         foreach (@taxes) {
-           $_->set('pkgnum',      $cust_pkg->pkgnum );
-           $_->set('locationnum', $cust_pkg->locationnum );
-         }
+       # all packages now have a locationnum and should get a 
+       # cust_bill_pkg_tax_location record.  The tax_locationnum
+       # may be the package's locationnum, or the customer's bill 
+       # or service location.
+       foreach (@taxes) {
+         $_->set('pkgnum',      $cust_pkg->pkgnum);
+         $_->set('locationnum', $cust_pkg->tax_locationnum);
        }
  
        $taxes{''} = [ @taxes ];
  
    my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
    foreach my $key (keys %tax_cust_bill_pkg) {
+     # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
+     # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of 
+     # the line item.
+     # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
+     # apply to $key-class charges.
      my @taxes = @{ $taxes{$key} || [] };
      my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
  
      my %localtaxlisthash = ();
      foreach my $tax ( @taxes ) {
  
+       # this is the tax identifier, not the taxname
        my $taxname = ref( $tax ). ' '. $tax->taxnum;
  #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
  #                  ' locationnum'. $cust_pkg->locationnum
  #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
  
+       # $taxlisthash: keys are "setup", "recur", and usage classes
+       # values are arrayrefs, first the tax object (cust_main_county
+       # or tax_rate) and then any cust_bill_pkg objects that the 
+       # tax applies to
        $taxlisthash->{ $taxname } ||= [ $tax ];
        push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
  
@@@ -1524,17 -1541,23 +1544,23 @@@ sub retry_realtime 
      cust_bill_batch
    );
  
-   my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
-                                                   @realtime_events
-                                      ).
-                           ' ) ';
+   my $is_realtime_event =
+     ' part_event.action IN ( '.
+         join(',', map "'$_'", @realtime_events ).
+     ' ) ';
+   my $batch_or_statustext =
+     "( part_event.action = 'cust_bill_batch'
+        OR ( statustext IS NOT NULL AND statustext != '' )
+      )";
  
    my @cust_event = qsearch({
      'table'     => 'cust_event',
      'select'    => 'cust_event.*',
      'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
      'hashref'   => { 'status' => 'done' },
-     'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
+     'extra_sql' => " AND $batch_or_statustext ".
                     " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
    });
  
@@@ -40,7 -40,8 +40,8 @@@ FS::cust_pkg objec
  
  =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
  
@@@ -105,6 -106,9 +106,9 @@@ sub order_pkg 
      }
      $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
    }
+   else {
+     $cust_pkg->locationnum($self->ship_locationnum);
+   }
  
    $cust_pkg->custnum( $self->custnum );
  
@@@ -351,6 -355,7 +355,7 @@@ Returns all suspended packages (see L<F
  
  sub suspended_pkgs {
    my $self = shift;
+   return $self->num_suspended_pkgs unless wantarray;
    grep { $_->susp } $self->ncancelled_pkgs;
  }
  
@@@ -377,6 -382,7 +382,7 @@@ this customer
  
  sub unsuspended_pkgs {
    my $self = shift;
+   return $self->num_unsuspended_pkgs unless wantarray;
    grep { ! $_->susp } $self->ncancelled_pkgs;
  }
  
@@@ -406,11 -412,7 +412,11 @@@ sub billing_pkgs 
    my $self = shift;
    grep { my $part_pkg = $_->part_pkg;
           $part_pkg->freq ne '' && $part_pkg->freq ne '0'
 -           && ( ! $_->susp || $part_pkg->option('suspend_bill', 1) );
 +           && ( ! $_->susp || $_->option('suspend_bill',1)
 +                           || ( $part_pkg->option('suspend_bill', 1)
 +                                  && ! $_->option('no_suspend_bill',1)
 +                              )
 +              );
         }
         $self->ncancelled_pkgs;
  }
@@@ -442,6 -444,16 +448,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 : '';
diff --combined FS/FS/cust_pkg.pm
@@@ -10,9 -10,9 +10,9 @@@ use List::Util qw(max)
  use Tie::IxHash;
  use Time::Local qw( timelocal timelocal_nocheck );
  use MIME::Entity;
- use FS::UID qw( getotaker dbh );
+ use FS::UID qw( getotaker dbh driver_name );
  use FS::Misc qw( send_email );
- use FS::Record qw( qsearch qsearchs );
+ use FS::Record qw( qsearch qsearchs fields );
  use FS::CurrentUser;
  use FS::cust_svc;
  use FS::part_pkg;
@@@ -879,6 -879,154 +879,154 @@@ sub cancel_if_expired 
    '';
  }
  
+ =item uncancel
+ "Un-cancels" this package: Orders a new package with the same custnum, pkgpart,
+ locationnum, (other fields?).  Attempts to re-provision cancelled services
+ using history information (errors at this stage are not fatal).
+ cust_pkg: pass a scalar reference, will be filled in with the new cust_pkg object
+ svc_fatal: service provisioning errors are fatal
+ svc_errors: pass an array reference, will be filled in with any provisioning errors
+ =cut
+ sub uncancel {
+   my( $self, %options ) = @_;
+   #in case you try do do $uncancel-date = $cust_pkg->uncacel 
+   return '' unless $self->get('cancel');
+   ##
+   # Transaction-alize
+   ##
+   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;
+   ##
+   # insert the new package
+   ##
+   my $cust_pkg = new FS::cust_pkg {
+     last_bill       => ( $options{'last_bill'} || $self->get('last_bill') ),
+     bill            => ( $options{'bill'}      || $self->get('bill')      ),
+     uncancel        => time,
+     uncancel_pkgnum => $self->pkgnum,
+     map { $_ => $self->get($_) } qw(
+       custnum pkgpart locationnum
+       setup
+       susp adjourn resume expire start_date contract_end dundate
+       change_date change_pkgpart change_locationnum
+       manual_flag no_auto quantity agent_pkgid recur_show_zero setup_show_zero
+     ),
+   };
+   my $error = $cust_pkg->insert(
+     'change' => 1, #supresses any referral credit to a referring customer
+   );
+   if ($error) {
+     $dbh->rollback if $oldAutoCommit;
+     return $error;
+   }
+   ##
+   # insert services
+   ##
+   #find historical services within this timeframe before the package cancel
+   # (incompatible with "time" option to cust_pkg->cancel?)
+   my $fuzz = 2 * 60; #2 minutes?  too much?   (might catch separate unprovision)
+                      #            too little? (unprovisioing export delay?)
+   my($end, $start) = ( $self->get('cancel'), $self->get('cancel') - $fuzz );
+   my @h_cust_svc = $self->h_cust_svc( $end, $start );
+   my @svc_errors;
+   foreach my $h_cust_svc (@h_cust_svc) {
+     my $h_svc_x = $h_cust_svc->h_svc_x( $end, $start );
+     #next unless $h_svc_x; #should this happen?
+     (my $table = $h_svc_x->table) =~ s/^h_//;
+     require "FS/$table.pm";
+     my $class = "FS::$table";
+     my $svc_x = $class->new( {
+       'pkgnum'  => $cust_pkg->pkgnum,
+       'svcpart' => $h_cust_svc->svcpart,
+       map { $_ => $h_svc_x->get($_) } fields($table)
+     } );
+     # radius_usergroup
+     if ( $h_svc_x->isa('FS::h_svc_Radius_Mixin') ) {
+       $svc_x->usergroup( [ $h_svc_x->h_usergroup($end, $start) ] );
+     }
+     my $svc_error = $svc_x->insert;
+     if ( $svc_error && $options{svc_fatal} ) {
+       $dbh->rollback if $oldAutoCommit;
+       return $svc_error;
+     } else {
+       my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_x->svcnum });
+       if ( $cust_svc ) {
+         my $cs_error = $cust_svc->delete;
+         if ( $cs_error ) {
+           $dbh->rollback if $oldAutoCommit;
+           return $cs_error;
+         }
+       }
+     }
+     push @svc_errors, $svc_error if $svc_error;
+   }
+   #these are pretty rare, but should handle them
+   # - dsl_device (mac addresses)
+   # - phone_device (mac addresses)
+   # - dsl_note (ikano notes)
+   # - domain_record (i.e. restore DNS information w/domains)
+   # - inventory_item(?) (inventory w/un-cancelling service?)
+   # - nas (svc_broaband nas stuff)
+   #this stuff is unused in the wild afaik
+   # - mailinglistmember
+   # - router.svcnum?
+   # - svc_domain.parent_svcnum?
+   # - acct_snarf (ancient mail fetching config)
+   # - cgp_rule (communigate)
+   # - cust_svc_option (used by our Tron stuff)
+   # - acct_rt_transaction (used by our time worked stuff)
+   ##
+   # also move over any services that didn't unprovision at cancellation
+   ## 
+   foreach my $cust_svc ( qsearch('cust_svc', { pkgnum => $self->pkgnum } ) ) {
+     $cust_svc->pkgnum( $cust_pkg->pkgnum );
+     my $error = $cust_svc->replace;
+     if ( $error ) {
+       $dbh->rollback if $oldAutoCommit;
+       return $error;
+     }
+   }
+   ##
+   # Finish
+   ##
+   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+   ${ $options{cust_pkg} }   = $cust_pkg   if ref($options{cust_pkg});
+   @{ $options{svc_errors} } = @svc_errors if ref($options{svc_errors});
+   '';
+ }
  =item unexpire
  
  Cancels any pending expiration (sets the expire field to null).
@@@ -1041,13 -1189,8 +1189,13 @@@ sub suspend 
      $hash{'resume'} = $resume_date;
    }
  
 +  $options{options} ||= {};
 +
    my $new = new FS::cust_pkg ( \%hash );
 -  $error = $new->replace( $self, options => { $self->options } );
 +  $error = $new->replace( $self, options => { $self->options,
 +                                              %{ $options{options} },
 +                                            }
 +                        );
    if ( $error ) {
      $dbh->rollback if $oldAutoCommit;
      return $error;
@@@ -1244,6 -1387,8 +1392,8 @@@ sub unsuspend 
    
    } #if $date 
  
+   my @labels = ();
    foreach my $cust_svc (
      qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
    ) {
          $dbh->rollback if $oldAutoCommit;
          return $error;
        }
+       my( $label, $value ) = $cust_svc->label;
+       push @labels, "$label: $value";
      }
  
    }
      return $error;
    }
  
+   if ( $conf->config('unsuspend_email_admin') ) {
+  
+     my $error = send_email(
+       'from'    => $conf->config('invoice_from', $self->cust_main->agentnum),
+                                  #invoice_from ??? well as good as any
+       'to'      => $conf->config('unsuspend_email_admin'),
+       'subject' => 'FREESIDE NOTIFICATION: Customer package unsuspended',       'body'    => [
+         "This is an automatic message from your Freeside installation\n",
+         "informing you that the following customer package has been unsuspended:\n",
+         "\n",
+         'Customer: #'. $self->custnum. ' '. $self->cust_main->name. "\n",
+         'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n",
+         ( map { "Service : $_\n" } @labels ),
+       ],
+     );
+     if ( $error ) {
+       warn "WARNING: can't send unsuspension admin email (unsuspending anyway): ".
+            "$error\n";
+     }
+   }
    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
  
    ''; #no errors
@@@ -1900,7 -2070,7 +2075,7 @@@ sub cust_svc 
    }
    if ( $opt{'svcdb'} ) {
      $search{addl_from} = ' LEFT JOIN part_svc USING ( svcpart ) ';
-     $search{hashref}->{svcdb} = $opt{'svcdb'};
+     $search{extra_sql} = ' AND svcdb = '. dbh->quote( $opt{'svcdb'} );
    }
  
    cluck "cust_pkg->cust_svc called" if $DEBUG > 2;
@@@ -2034,11 -2204,14 +2209,14 @@@ field, I<num_avail>, which specifies th
  
  sub available_part_svc {
    my $self = shift;
+   my $pkg_quantity = $self->quantity || 1;
    grep { $_->num_avail > 0 }
      map {
            my $part_svc = $_->part_svc;
            $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
-             $_->quantity - $self->num_cust_svc($_->svcpart);
+             $pkg_quantity * $_->quantity - $self->num_cust_svc($_->svcpart);
  
          # more evil encapsulation breakage
          if($part_svc->{'Hash'}{'num_avail'} > 0) {
@@@ -2080,6 -2253,8 +2258,8 @@@ sub part_svc 
    my $self = shift;
    my %opt = @_;
  
+   my $pkg_quantity = $self->quantity || 1;
    #XXX some sort of sort order besides numeric by svcpart...
    my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
      my $pkg_svc = $_;
      my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
      $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
      $part_svc->{'Hash'}{'num_avail'}    =
-       max( 0, $pkg_svc->quantity - $num_cust_svc );
+       max( 0, $pkg_quantity * $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} > 0
@@@ -2446,6 -2621,39 +2626,39 @@@ Returns the label of the location objec
  
  #end of subs in location_Mixin.pm now... unfortunately the POD doesn't mixin
  
+ =item tax_locationnum
+ Returns the foreign key to a L<FS::cust_location> object for calculating  
+ tax on this package, as determined by the C<tax-pkg_address> and 
+ C<tax-ship_address> configuration flags.
+ =cut
+ sub tax_locationnum {
+   my $self = shift;
+   my $conf = FS::Conf->new;
+   if ( $conf->exists('tax-pkg_address') ) {
+     return $self->locationnum;
+   }
+   elsif ( $conf->exists('tax-ship_address') ) {
+     return $self->cust_main->ship_locationnum;
+   }
+   else {
+     return $self->cust_main->bill_locationnum;
+   }
+ }
+ =item tax_location
+ Returns the L<FS::cust_location> object for tax_locationnum.
+ =cut
+ sub tax_location {
+   my $self = shift;
+   FS::cust_location->by_key( $self->tax_locationnum )
+ }
  =item seconds_since TIMESTAMP
  
  Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
@@@ -3305,7 -3513,7 +3518,7 @@@ sub search 
        "NOT (".FS::cust_pkg->onetime_sql . ")";
    }
    else {
-     foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end cancel )) {
+     foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) {
  
        next unless exists($params->{$field});
  
@@@ -3432,6 -3640,25 +3645,25 @@@ sub fcc_477_count 
  
  }
  
+ =item tax_locationnum_sql
+ Returns an SQL expression for the tax location for a package, based
+ on the settings of 'tax-pkg_address' and 'tax-ship_address'.
+ =cut
+ sub tax_locationnum_sql {
+   my $conf = FS::Conf->new;
+   if ( $conf->exists('tax-pkg_address') ) {
+     'cust_pkg.locationnum';
+   }
+   elsif ( $conf->exists('tax-ship_address') ) {
+     'cust_main.ship_locationnum';
+   }
+   else {
+     'cust_main.bill_locationnum';
+   }
+ }
  
  =item location_sql
  
@@@ -3450,7 -3677,13 +3682,13 @@@ sub location_sql 
  
    # '?' placeholders in _location_sql_where
    my $x = $ornull ? 3 : 2;
-   my @bill_param = ( ('city')x3, ('county')x$x, ('state')x$x, 'country' );
+   my @bill_param = ( 
+     ('district')x3,
+     ('city')x3, 
+     ('county')x$x,
+     ('state')x$x,
+     'country'
+   );
  
    my $main_where;
    my @main_param;
@@@ -3509,16 -3742,19 +3747,19 @@@ sub _location_sql_where 
  
    $ornull = $ornull ? ' OR ? IS NULL ' : '';
  
-   my $or_empty_city   = " OR ( ? = '' AND $table.${prefix}city   IS NULL ) ";
-   my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL ) ";
-   my $or_empty_state =  " OR ( ? = '' AND $table.${prefix}state  IS NULL ) ";
+   my $or_empty_city     = " OR ( ? = '' AND $table.${prefix}city     IS NULL )";
+   my $or_empty_county   = " OR ( ? = '' AND $table.${prefix}county   IS NULL )";
+   my $or_empty_state    = " OR ( ? = '' AND $table.${prefix}state    IS NULL )";
+   my $text = (driver_name =~ /^mysql/i) ? 'char' : 'text';
  
  #        ( $table.${prefix}city    = ? $or_empty_city   $ornull )
    "
-         ( $table.${prefix}city    = ? OR ? = '' OR CAST(? AS text) IS NULL )
-     AND ( $table.${prefix}county  = ? $or_empty_county $ornull )
-     AND ( $table.${prefix}state   = ? $or_empty_state  $ornull )
-     AND   $table.${prefix}country = ?
+         ( $table.district = ? OR ? = '' OR CAST(? AS $text) IS NULL )
+     AND ( $table.${prefix}city     = ? OR ? = '' OR CAST(? AS $text) IS NULL )
+     AND ( $table.${prefix}county   = ? $or_empty_county $ornull )
+     AND ( $table.${prefix}state    = ? $or_empty_state  $ornull )
+     AND   $table.${prefix}country  = ?
    ";
  }
  
diff --combined FS/FS/domain_record.pm
@@@ -3,8 -3,8 +3,7 @@@ package FS::domain_record
  use strict;
  use vars qw( @ISA $noserial_hack $DEBUG $me );
  use FS::Conf;
--#use FS::Record qw( qsearch qsearchs );
--use FS::Record qw( qsearchs dbh );
++use FS::Record qw( qsearchs dbh ); #qsearch
  use FS::svc_domain;
  use FS::svc_www;
  
diff --combined bin/23commit
@@@ -19,7 -19,7 +19,7 @@@ die "no files!" unless @ARGV
  #print <<END;
  system join('',
    "( cd /home/$USER/freeside2.3/$prefix; git pull ) && ",
--  "git diff -u @ARGV | ( cd /home/$USER/freeside2.3/$prefix; patch  ) ",
++  "git diff -u @ARGV | ( cd /home/$USER/freeside2.3/$prefix; patch -p1 ) ",
    " && ( ( git pull && git commit  -m $desc @ARGV && git push ); ",
    "( cd /home/$USER/freeside2.3/$prefix; git commit -m $desc @ARGV && git push ) )"
  );
@@@ -2,9 -2,11 +2,11 @@@
  <%= include('header', 'My Account') %>
  
  Hello <%= $name %>!<BR><BR>
- <%= $small_custview %>
+ <%= include('small_custview') %>
  <BR>
 -<%= if ( $access_pkgnum ) {
 +<%= unless ( $access_pkgnum ) {
        $OUT .= qq!Balance: <B>\$$balance</B><BR><BR>!;
      }
      '';
      $OUT .= '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=2 BGCOLOR="#eeeeee">'.
              '<TR><TH BGCOLOR="#ff6666" COLSPAN=5>Open Invoices</TH></TR>';
      my $link = qq!<A HREF="<%= $url %>myaccount!;
-     my $col1 = "ffffff";
-     my $col2 = "dddddd";
+     my $col1 = $stripe1_bgcolor || '#ffffff';
+     my $col2 = $stripe2_bgcolor || '#dddddd';
      my $col = $col1;
  
      foreach my $invoice ( @open_invoices ) {
-       my $td = qq!<TD BGCOLOR="#$col">!;
+       my $td = qq!<TD BGCOLOR="$col">!;
        my $a=qq!<A HREF="${url}view_invoice;invnum=!. $invoice->{'invnum'}. '">';
        $OUT .=
          "<TR>$td${a}Invoice #". $invoice->{'invnum'}. "</A></TD>$td</TD>".
          "$td$a". $invoice->{'date'}. "</A></TD>$td</TD>".
-         qq!<TD BGCOLOR="#$col" ALIGN="right">$a\$!. $invoice->{'owed'}.
+         qq!<TD BGCOLOR="$col" ALIGN="right">$a\$!. $invoice->{'owed'}.
            '</A></TD>'.
          '</TR>';
        $col = $col eq $col1 ? $col2 : $col1;
              '<TR><TH BGCOLOR="#ff6666" COLSPAN="3">Support Time Remaining</TH>'.
              '</TR><TR><TH>Package</TH><TH></TH>'.
              '<TH>Time Remaining</TH></TR>';
-     my $col1 = "ffffff";
-     my $col2 = "dddddd";
+     my $col1 = $stripe1_bgcolor || '#ffffff';
+     my $col2 = $stripe2_bgcolor || '#dddddd';
      my $col = $col1;
  
      foreach my $support ( @support_services ) {
-       my $td = qq!<TD BGCOLOR="#$col">!;
+       my $td = qq!<TD BGCOLOR="$col">!;
        my $a = qq!<A HREF="${url}view_support_details;svcnum=!.
                $support->{'svcnum'}. '">';
        $OUT .=
@@@ -10,7 -10,7 +10,6 @@@ foreach my $pkg 
           } @cust_pkg
      ) {
    my $susp = $pkg->{'susp'} || '';
--  warn $pkg->{'pkg'}. ' '.$susp."\n";
    my @pkg_actions = ( [ 'customer_change_pkg' => 'change' ] );
    push @pkg_actions, [ 'process_suspend_pkg' => 'suspend' ] 
      if $self_suspend_reason and !$susp;
@@@ -8,6 -8,6 +8,7 @@@
      'attrnum'   => 'Attribute',
      'priority'  => 'Priority',
    },
++  'viewall_dir' => 'browse',
    'menubar' => \@menubar,
    'edit_callback' => $edit_callback,
    'error_callback' => $edit_callback,
  % my $date_init = 0;
  % if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') {
  %   $submit =~ /^(\w*)\s/;
- <& /elements/tr-input-date-field.html, {
-     'name'    => 'date',
-     'value'   => $date,
-     'label'   => mt("$1 package on"),
-     'format'  => $date_format,
- } &>
  <& /elements/tr-input-date-field.html, {
+       'name'    => 'date',
+       'value'   => $date,
+       'label'   => mt("$1 package on"),
+       'format'  => $date_format,
  } &>
  %   $date_init = 1;
  % }
  
- % unless ( $method eq 'resume' ) { #the only one that doesn't need a reason
- <& /elements/tr-select-reason.html,
-      'field'          => 'reasonnum',
-      'reason_class'   => $class,
-      'curr_value'     => $reasonnum,
-      'control_button' => "document.getElementById('confirm_cancel_pkg_button')",
- &>
+ % if ($method eq 'uncancel' ) {
+ %
+ % #XXX customer also requested setup
+ % # setup: what usefulness is changing or blanking this?  re-charge setup fee?
+ % #        an option that says that would be better if that's what we want to do
+ % # last_bill: isn't this informational?  what good would editing it do?
+ % #            something about invoice display?
+   <& /elements/tr-input-date-field.html, {
+       'name'    => 'last_bill',
+       'value'   => ( $cgi->param('last_bill') || $cust_pkg->get('last_bill') ),
+       'label'   => mt("Last bill date"),
+       'format'  => $date_format,
+   } &>
+   <& /elements/tr-input-date-field.html, {
+       'name'    => 'bill',
+       'value'   => ( $cgi->param('bill') || $cust_pkg->get('bill') ),
+       'label'   => mt("Next bill date"),
+       'format'  => $date_format,
+   } &>
+   <& /elements/tr-checkbox.html,
+        'label'  => mt("Uncancel even if a service can't be re-provisioned"),
+        'field'  => 'svc_not_fatal',
+        'value'  => 'Y',
+   &>
+ %   $date_init = 1;
+ % }
+ % unless ( $method eq 'resume' || $method eq 'uncancel' ) {
+   <& /elements/tr-select-reason.html,
+        field          => 'reasonnum',
+        reason_class   => $class,
+        curr_value     => $reasonnum,
+        control_button => "document.getElementById('confirm_cancel_pkg_button')",
+   &>
  % }
  
 -% if ( ( $method eq 'adjourn' or $method eq 'suspend' ) and 
 +% if ( $method eq 'adjourn' || $method eq 'suspend' ) {
 +    <TR><TD COLSPAN=2>
 +%   if ( $part_pkg->option('suspend_bill', 1) ) {
 +      <& /elements/checkbox.html, name=>'no_suspend_bill', value=>'Y' &>
 +      Disable recurring billing while suspended
 +%   } else {
 +      <& /elements/checkbox.html, name=>'suspend_bill', value=>'Y' &>
 +      Continue recurring billing while suspended
 +%   }
 +    </TD></TR>
 +% }
 +
 +% if ( ( $method eq 'adjourn' || $method eq 'suspend' ) and 
  %      $curuser->access_right('Unsuspend customer package') )  { #later?
  %   my $resume_date = $cgi->param('error') 
  %                     ? str2time($cgi->param('resume_date'))
  %                     : $cust_pkg->get('resume');
  
- <& /elements/tr-input-date-field.html, {
-     'name'    => 'resume_date',
-     'value'   => $resume_date,
-     'label'   => mt('Unsuspend on'),
-     'format'  => $date_format,
-     'noinit'  => $date_init,
- } &>
  <& /elements/tr-input-date-field.html, {
+       'name'    => 'resume_date',
+       'value'   => $resume_date,
+       'label'   => mt('Unsuspend on'),
+       'format'  => $date_format,
+       'noinit'  => $date_init,
  } &>
  % }
  </TABLE>
  
  <BR>
  <INPUT TYPE="submit" NAME="submit" ID="confirm_cancel_pkg_button" 
    VALUE="<% mt($submit) |h %>"
-   <% $method ne 'resume' ? 'DISABLED' : '' %>>
+   <% $method !~ /^(resume|uncancel)$/ ? 'DISABLED' : '' %>>
  
  </FORM>
  </BODY>
  </HTML>
  
  <%init>
+ use Date::Parse qw(str2time);
  
  my $conf = new FS::Conf;
  my $date_format = $conf->config('date_format') || '%m/%d/%Y';
@@@ -111,6 -131,10 +143,10 @@@ if ($method eq 'cancel') 
    $class  = '';
    $submit = 'Unsuspend Later';
    $right  = 'Unsuspend customer package'; #later?
+ } elsif ( $method eq 'uncancel') {
+   $class  = '';
+   $submit = 'Un-Cancel';
+   $right  = 'Un-cancel customer package'; #later?
  } else {
    die 'illegal query (unknown method param)';
  }
@@@ -119,6 -143,7 +155,7 @@@ my $curuser = $FS::CurrentUser::Current
  die "access denied" unless $curuser->access_right($right);
  
  my $title = ucfirst($method) . ' Package';
+ $title =~ s/Uncancel/Un-cancel/;
  
  my $cust_pkg = qsearchs('cust_pkg', {'pkgnum' => $pkgnum})
    or die "Unknown pkgnum: $pkgnum";
@@@ -6,19 -6,21 +6,21 @@@
  </HTML>
  <%once>
  
- my %past = ( 'cancel'  => 'cancelled',
-              'expire'  => 'expired',
-              'suspend' => 'suspended',
-              'adjourn' => 'adjourned',
-              'resume'  => 'scheduled to resume',
+ my %past = ( 'cancel'   => 'cancelled',
+              'expire'   => 'expired',
+              'suspend'  => 'suspended',
+              'adjourn'  => 'adjourned',
+              'resume'   => 'scheduled to resume',
+              'uncancel' => 'un-cancelled',
             );
  
  #i'm sure this is false laziness with somewhere, at least w/misc/cancel_pkg.html
- my %right = ( 'cancel'  => 'Cancel customer package immediately',
-               'expire'  => 'Cancel customer package later',
-               'suspend' => 'Suspend customer package',
-               'adjourn' => 'Suspend customer package later',
-               'resume'  => 'Unsuspend customer package', #later?
+ my %right = ( 'cancel'   => 'Cancel customer package immediately',
+               'expire'   => 'Cancel customer package later',
+               'suspend'  => 'Suspend customer package',
+               'adjourn'  => 'Suspend customer package later',
+               'resume'   => 'Unsuspend customer package', #later?
+               'uncancel' => 'Un-cancel customer package',
              );
  
  </%once>
@@@ -26,7 -28,8 +28,8 @@@
  
  #untaint method
  my $method = $cgi->param('method');
- $method =~ /^(cancel|expire|suspend|adjourn|resume)$/ or die "Illegal method";
+ $method =~ /^(cancel|expire|suspend|adjourn|resume|uncancel)$/
+   or die "Illegal method";
  $method = $1;
  my $past_method = $past{$method};
  
@@@ -39,7 -42,7 +42,7 @@@ $pkgnum =~ /^(\d+)$/ or die "Illegal pk
  $pkgnum = $1;
  
  my $date = time;
- if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume'){
+ if ($method eq 'expire' || $method eq 'adjourn' || $method eq 'resume') {
    #untaint date
    $date = $cgi->param('date'); #huh?
    parse_datetime($cgi->param('date')) =~ /^(\d+)$/ or die "Illegal date";
    $method = 'unsuspend' if $method eq 'resume';
  }
  
 -my $resume_date;
 +my $resume_date = '';
 +my $options = '';
  if ( $method eq 'suspend' ) { #or 'adjourn'
    $resume_date = parse_datetime($cgi->param('resume_date'))
      if $cgi->param('resume_date');
 +
 +  $options = { map scalar($cgi->param($_)),
 +                 suspend_bill no_suspend_bill
 +             };
  }
  
  my $cust_pkg = qsearchs( 'cust_pkg', {'pkgnum'=>$pkgnum} );
  
  #untaint reasonnum
  my $reasonnum = $cgi->param('reasonnum');
- if ( $method ne 'unsuspend' ) { #i.e. 'resume'
+ if ( $method !~ /^(unsuspend|uncancel)$/ ) {
    $reasonnum =~ /^(-?\d+)$/ or die "Illegal reasonnum";
    $reasonnum = $1;
  
    }
  }
  
+ #for uncancel
+ my $last_bill =
+   $cgi->param('last_bill') ? parse_datetime($cgi->param('last_bill')) : '';
+ my $bill =
+   $cgi->param('bill')      ? parse_datetime($cgi->param('bill'))      : '';
+ my $svc_fatal = ( $cgi->param('svc_not_fatal') ne 'Y' );
  my $error = $cust_pkg->$method( 'reason'      => $reasonnum,
                                  'date'        => $date,
                                  'resume_date' => $resume_date,
+                                 'last_bill'   => $last_bill,
+                                 'bill'        => $bill,
+                                 'svc_fatal'   => $svc_fatal,
 +                                'options'     => $options,
                                );
  
  if ($error) {
  
  %   } 
  %
- % } else { 
+ %   if ( $part_pkg->freq ) { #?
+       <TR>
+         <TD COLSPAN=<%$colspan%>>
+           <FONT SIZE=-1>
+ %           if ( $curuser->access_right('Un-cancel customer package') ) { 
+               (&nbsp;<% pkg_uncancel_link($cust_pkg) %>&nbsp;)
+ %           } 
+           <FONT>
+         </TD>
+       </TR>
+ %   }
+ %
+ % } else {
  %
  %   if ( $cust_pkg->get('susp') ) { #status: suspended
  %     my $cpr = $cust_pkg->last_cust_pkg_reason('susp');
        <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt ) %>
  %   } 
  
+     <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
      <% pkg_status_row_changed( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
      <% pkg_status_row_if( $cust_pkg, $last_bill_or_renewed, 'last_bill', %opt, curuser=>$curuser ) %>
 -%   if ( $part_pkg->option('suspend_bill', 1) ) {
 +%   if ( $cust_pkg->option('suspend_bill', 1)
 +%        || ( $part_pkg->option('suspend_bill', 1)
 +%               && ! $cust_pkg->option('no_suspend_bill',1)
 +%           )
 +%      )
 +%   {
        <% pkg_status_row_if( $cust_pkg, emt('Next bill'), 'bill', %opt, curuser=>$curuser ) %>
  %   }
      <% pkg_status_row_if( $cust_pkg, emt('Will resume'), 'resume', %opt, curuser=>$curuser ) %>
               )
            %>
  
+           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
            <TR>
              <TD COLSPAN=<%$colspan%>>
                <FONT SIZE=-1>
            <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
  
            <% pkg_status_row_if($cust_pkg, emt('Start billing'), 'start_date', %opt) %>
+           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
  
  %       } 
  %
  
            <% pkg_status_row_discount( $cust_pkg, %opt, 'colspan'=>$colspan ) %>
  
+           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
  %       } else { 
  %
  %         my $num_cust_svc = $cust_pkg->num_cust_svc;
  
            <% pkg_status_row($cust_pkg, emt('Setup'), 'setup', %opt) %>
  
+           <% pkg_status_row_if($cust_pkg, emt('Un-cancelled'), 'uncancel', %opt ) %>
  %       } 
  %
  %     } 
@@@ -472,6 -489,16 +494,16 @@@ sub pkg_cancel_link 
           )
  }
  
+ sub pkg_uncancel_link {
+   include( '/elements/popup_link-cust_pkg.html',
+              'action'      => $p. 'misc/cancel_pkg.html?method=uncancel',
+              'label'       => emt('Un-cancel'),
+              'actionlabel' => emt('Un-cancel'),
+              #'color'       =>  #?
+              'cust_pkg'    => shift,
+          )
+ }
  sub pkg_expire_link {
    include( '/elements/popup_link-cust_pkg.html',
               'action'      => $p. 'misc/cancel_pkg.html?method=expire',