implement package changes w/location change, RT#4499
authorivan <ivan>
Sat, 10 Jan 2009 23:56:59 +0000 (23:56 +0000)
committerivan <ivan>
Sat, 10 Jan 2009 23:56:59 +0000 (23:56 +0000)
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
httemplate/edit/process/change-cust_pkg.html [new file with mode: 0644]
httemplate/edit/process/cust_pkg.cgi
httemplate/elements/location.html
httemplate/elements/tr-select-cust_location.html
httemplate/misc/change_pkg.cgi
httemplate/view/cust_main/packages/location.html
httemplate/view/cust_main/packages/package.html

index a7a0b45..1e594e5 100644 (file)
@@ -767,6 +767,14 @@ worry that config_items is freeside-specific and icky.
   },
 
   {
+    'key'         => 'invoice_subject',
+    'section'     => 'billing',
+    'description' => 'Subject: header on email invoices.  Defaults to "Invoice".  The following substitutions are available: $name, $name_short, $invoice_number, and $invoice_date.',
+    'type'        => 'text',
+    'per_agent'   => 1,
+  },
+
+  {
     'key'         => 'invoice_template',
     'section'     => 'billing',
     'description' => 'Text template file for invoices.  Used if no invoice_html template is defined, and also seen by users using non-HTML capable mail clients.  See the <a href="http://www.freeside.biz/mediawiki/index.php/Freeside:1.7:Documentation:Administration#Plaintext_invoice_templates">billing documentation</a> for details.',
index ecf017e..2c3c967 100644 (file)
@@ -661,13 +661,20 @@ sub tables_hashref {
       'primary_key' => 'custnum',
       'unique' => [ [ 'agentnum', 'agent_custid' ] ],
       #'index' => [ ['last'], ['company'] ],
-      'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ],
-                   [ 'daytime' ], [ 'night' ], [ 'fax' ], [ 'refnum' ],
-                   [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
+      'index' => [
+                   [ 'agentnum' ], [ 'refnum' ], [ 'custbatch' ],
+                   [ 'referral_custnum' ],
+                   [ 'payby' ], [ 'paydate' ],
+                   #billing
+                   [ 'last' ], [ 'company' ],
+                   [ 'county' ], [ 'state' ], [ 'country' ],
+                   [ 'zip' ],
+                   [ 'daytime' ], [ 'night' ], [ 'fax' ],
+                   #shipping
                    [ 'ship_last' ], [ 'ship_company' ],
+                   [ 'ship_county' ], [ 'ship_state' ], [ 'ship_country' ],
+                   [ 'ship_zip' ],
                    [ 'ship_daytime' ], [ 'ship_night' ], [ 'ship_fax' ],
-                   [ 'payby' ], [ 'paydate' ],
-                   [ 'agentnum' ], [ 'custbatch' ],
                  ],
     },
 
@@ -975,6 +982,7 @@ sub tables_hashref {
         'change_date',    @date_type,             '', '',
         'change_pkgnum',       'int', 'NULL', '', '', '',
         'change_pkgpart',      'int', 'NULL', '', '', '',
+        'change_locationnum',  'int', 'NULL', '', '', '',
         'manual_flag',        'char', 'NULL',  1, '', '', 
         'quantity',            'int', 'NULL', '', '', '',
       ],
@@ -1994,7 +2002,7 @@ sub tables_hashref {
       ],
       'primary_key' => 'acctid',
       'unique' => [],
-      'index' => [ [ 'calldate' ], [ 'dst' ], [ 'accountcode' ], [ 'freesidestatus' ], [ 'cdrbatch' ], ],
+      'index' => [ [ 'calldate' ], [ 'src' ], [ 'dst' ], [ 'charged_party' ], [ 'accountcode' ], [ 'freesidestatus' ], [ 'cdrbatch' ], ],
     },
 
     'cdr_calltype' => {
@@ -2039,6 +2047,21 @@ sub tables_hashref {
       'index'  => [],
     },
 
+    #'cdr_file' => {
+    #  'columns' => [
+    #    'filenum',    'serial',     '', '', '', '',
+    #    'filename',  'varchar',     '', '', '', '',
+    #    'status',    'varchar', 'NULL', '', '', '',
+    #  ],
+    #  'primary_key' => 'filenum',
+    #  'unique' => [ [ 'filename' ], ], #just change the index if we need to
+    #                                   # agent-virtualize or have a customer
+    #                                   # with dup-filename needs or something
+    #                                   # (only used by cdr.http_and_import for
+    #                                   #  chrissakes)
+    #  'index'  => [],
+    #},
+
     'inventory_item' => {
       'columns' => [
         'itemnum',  'serial',      '',      '', '', '',
index 94280a4..7a350c1 100644 (file)
@@ -659,85 +659,9 @@ sub _copy_skel {
 
 }
 
-=item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
-
-Like the insert method on an existing record, this method orders a package
-and included services atomicaly.  Pass a Tie::RefHash data structure to this
-method containing FS::cust_pkg and FS::svc_I<tablename> objects.  There should
-be a better explanation of this, but until then, here's an example:
-
-  use Tie::RefHash;
-  tie %hash, 'Tie::RefHash'; #this part is important
-  %hash = (
-    $cust_pkg => [ $svc_acct ],
-    ...
-  );
-  $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
-
-Services can be new, in which case they are inserted, or existing unaudited
-services, in which case they are linked to the newly-created package.
-
-Currently available options are: I<depend_jobnum> and I<noexport>.
-
-If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
-on the supplied jobnum (they will not run until the specific job completes).
-This can be used to defer provisioning until some action completes (such
-as running the customer's credit card successfully).
-
-The I<noexport> option is deprecated.  If I<noexport> is set true, no
-provisioning jobs (exports) are scheduled.  (You can schedule them later with
-the B<reexport> method for each cust_pkg object.  Using the B<reexport> method
-on the cust_main object is not recommended, as existing services will also be
-reexported.)
-
-=cut
-
-sub order_pkgs {
-  my $self = shift;
-  my $cust_pkgs = shift;
-  my $seconds = shift;
-  my %options = @_;
-
-  warn "$me order_pkgs called with options ".
-       join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
-    if $DEBUG;
-
-  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;
-
-  local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
-
-  foreach my $cust_pkg ( keys %$cust_pkgs ) {
-
-    my $error = $self->order_pkg( 'cust_pkg'      => $cust_pkg,
-                                  'svcs'          => $cust_pkgs->{$cust_pkg},
-                                  'seconds'       => $seconds,
-                                  'depend_jobnum' => $options{'depend_jobnum'},
-                                );
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-
-  }
-
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-  ''; #no error
-}
-
 =item order_pkg HASHREF | OPTION => VALUE ... 
 
-Orders a single package.  This is the preferred and most flexible method for
-ordering a single package, including the ability to set a (new or existing)
-location as well as insert services.
+Orders a single package.
 
 Options may be passed as a list of key/value pairs or as a hash reference.
 Options are:
@@ -837,6 +761,81 @@ sub order_pkg {
 
 }
 
+=item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
+
+Like the insert method on an existing record, this method orders multiple
+packages and included services atomicaly.  Pass a Tie::RefHash data structure
+to this method containing FS::cust_pkg and FS::svc_I<tablename> objects.
+There should be a better explanation of this, but until then, here's an
+example:
+
+  use Tie::RefHash;
+  tie %hash, 'Tie::RefHash'; #this part is important
+  %hash = (
+    $cust_pkg => [ $svc_acct ],
+    ...
+  );
+  $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
+
+Services can be new, in which case they are inserted, or existing unaudited
+services, in which case they are linked to the newly-created package.
+
+Currently available options are: I<depend_jobnum> and I<noexport>.
+
+If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
+on the supplied jobnum (they will not run until the specific job completes).
+This can be used to defer provisioning until some action completes (such
+as running the customer's credit card successfully).
+
+The I<noexport> option is deprecated.  If I<noexport> is set true, no
+provisioning jobs (exports) are scheduled.  (You can schedule them later with
+the B<reexport> method for each cust_pkg object.  Using the B<reexport> method
+on the cust_main object is not recommended, as existing services will also be
+reexported.)
+
+=cut
+
+sub order_pkgs {
+  my $self = shift;
+  my $cust_pkgs = shift;
+  my $seconds = shift;
+  my %options = @_;
+
+  warn "$me order_pkgs called with options ".
+       join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
+    if $DEBUG;
+
+  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;
+
+  local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+
+  foreach my $cust_pkg ( keys %$cust_pkgs ) {
+
+    my $error = $self->order_pkg( 'cust_pkg'      => $cust_pkg,
+                                  'svcs'          => $cust_pkgs->{$cust_pkg},
+                                  'seconds'       => $seconds,
+                                  'depend_jobnum' => $options{'depend_jobnum'},
+                                );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
 
 Recharges this (existing) customer with the specified prepaid card (see
@@ -3896,7 +3895,7 @@ sub realtime_bop {
       my $templ_hash = { error => $transaction->error_message };
 
       my $error = send_email(
-        'from'    => $conf->config('invoice_from'),
+        'from'    => $conf->config('invoice_from', $self->agentnum ),
         'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
         'subject' => 'Your payment could not be processed',
         'body'    => [ $template->fill_in(HASH => $templ_hash) ],
@@ -5391,6 +5390,35 @@ sub ship_name {
   }
 }
 
+=item name_short
+
+Returns a name string for this customer, either "Company" or "First Last".
+
+=cut
+
+sub name_short {
+  my $self = shift;
+  $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
+}
+
+=item ship_name_short
+
+Returns a name string for this (service/shipping) contact, either "Company"
+or "First Last".
+
+=cut
+
+sub ship_name_short {
+  my $self = shift;
+  if ( $self->get('ship_last') ) { 
+    $self->ship_company !~ /^\s*$/
+      ? $self->ship_company
+      : $self->ship_contact_firstlast;
+  } else {
+    $self->name_company_or_firstlast;
+  }
+}
+
 =item contact
 
 Returns this customer's full (billing) contact name only, "Last, First"
@@ -5415,6 +5443,30 @@ sub ship_contact {
     : $self->contact;
 }
 
+=item contact_firstlast
+
+Returns this customers full (billing) contact name only, "First Last".
+
+=cut
+
+sub contact_firstlast {
+  my $self = shift;
+  $self->first. ' '. $self->get('last');
+}
+
+=item ship_contact_firstlast
+
+Returns this customer's full (shipping) contact name only, "First Last".
+
+=cut
+
+sub ship_contact_firstlast {
+  my $self = shift;
+  $self->get('ship_last')
+    ? $self->first. ' '. $self->get('ship_last')
+    : $self->contact_firstlast;
+}
+
 =item country_full
 
 Returns this customer's full country name
@@ -6717,18 +6769,19 @@ I<$expdate> - the expiration of the customer payment in seconds from epoch
 =cut
 
 sub notify {
-  my ($customer, $template, %options) = @_;
+  my ($self, $template, %options) = @_;
 
   return unless $conf->exists($template);
 
-  my $from = $conf->config('invoice_from') if $conf->exists('invoice_from');
+  my $from = $conf->config('invoice_from', $self->agentnum)
+    if $conf->exists('invoice_from', $self->agentnum);
   $from = $options{from} if exists($options{from});
 
-  my $to = join(',', $customer->invoicing_list_emailonly);
+  my $to = join(',', $self->invoicing_list_emailonly);
   $to = $options{to} if exists($options{to});
   
-  my $subject = "Notice from " . $conf->config('company_name')
-    if $conf->exists('company_name');
+  my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
+    if $conf->exists('company_name', $self->agentnum);
   $subject = $options{subject} if exists($options{subject});
 
   my $notify_template = new Text::Template (TYPE => 'ARRAY',
@@ -6739,16 +6792,17 @@ sub notify {
   $notify_template->compile()
     or die "can't compile template: Text::Template::ERROR";
 
-  $FS::notify_template::_template::company_name = $conf->config('company_name');
+  $FS::notify_template::_template::company_name =
+    $conf->config('company_name', $self->agentnum);
   $FS::notify_template::_template::company_address =
-    join("\n", $conf->config('company_address') ). "\n";
-
-  my $paydate = $customer->paydate || '2037-12-31';
-  $FS::notify_template::_template::first = $customer->first;
-  $FS::notify_template::_template::last = $customer->last;
-  $FS::notify_template::_template::company = $customer->company;
-  $FS::notify_template::_template::payinfo = $customer->mask_payinfo;
-  my $payby = $customer->payby;
+    join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
+
+  my $paydate = $self->paydate || '2037-12-31';
+  $FS::notify_template::_template::first = $self->first;
+  $FS::notify_template::_template::last = $self->last;
+  $FS::notify_template::_template::company = $self->company;
+  $FS::notify_template::_template::payinfo = $self->mask_payinfo;
+  my $payby = $self->payby;
   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
 
@@ -6848,10 +6902,10 @@ sub generate_letter {
                      );
     if ( length($retadd) ) {
       $letter_data{returnaddress} = $retadd;
-    } elsif ( grep /\S/, $conf->config('company_address') ) {
+    } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
       $letter_data{returnaddress} =
         join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
-                          $conf->config('company_address')
+                          $conf->config('company_address', $self->agentnum)
         );
     } else {
       $letter_data{returnaddress} = '~';
@@ -6860,7 +6914,7 @@ sub generate_letter {
 
   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
 
-  $letter_data{company_name} = $conf->config('company_name');
+  $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
 
   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
@@ -6910,6 +6964,8 @@ sub print {
   do_print [ $self->print_ps($template) ];
 }
 
+#these three subs should just go away once agent stuff is all config overrides
+
 sub agent_template {
   my $self = shift;
   $self->_agent_plandata('agent_templatename');
index 25985ce..e2ca871 100644 (file)
@@ -174,6 +174,10 @@ Previous pkgnum
 
 Previous pkgpart
 
+=item change_locationnum
+
+Previous locationnum
+
 =back
 
 Note: setup, last_bill, bill, adjourn, susp, expire, cancel and change_date
@@ -620,7 +624,7 @@ sub cancel {
   if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
     my $conf = new FS::Conf;
     my $error = send_email(
-      'from'    => $conf->config('invoice_from'),
+      'from'    => $conf->config('invoice_from', $self->cust_main->agentnum),
       'to'      => \@invoicing_list,
       'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
       'body'    => [ map "$_\n", $conf->config('cancelmessage') ],
@@ -803,7 +807,8 @@ sub suspend {
     if ( $conf->config('suspend_email_admin') ) {
  
       my $error = send_email(
-        'from'    => $conf->config('invoice_from'), #??? well as good as any
+        'from'    => $conf->config('invoice_from', $self->cust_main->agentnum),
+                                   #invoice_from ??? well as good as any
         'to'      => $conf->config('suspend_email_admin'),
         'subject' => 'FREESIDE NOTIFICATION: Customer package suspended',
         'body'    => [
@@ -1001,6 +1006,148 @@ sub unadjourn {
 
 }
 
+
+=item change HASHREF | OPTION => VALUE ... 
+
+Changes this package: cancels it and creates a new one, with a different
+pkgpart or locationnum or both.  All services are transferred to the new
+package (no change will be made if this is not possible).
+
+Options may be passed as a list of key/value pairs or as a hash reference.
+Options are:
+
+=over 4
+
+=item locaitonnum
+
+New locationnum, to change the location for this package.
+
+=item cust_location
+
+New FS::cust_location object, to create a new location and assign it
+to this package.
+
+=item pkgpart
+
+New pkgpart (see L<FS::part_pkg>).
+
+=item refnum
+
+New refnum (see L<FS::part_referral>).
+
+=back
+
+At least one option must be specified (otherwise, what's the point?)
+
+Returns either the new FS::cust_pkg object or a scalar error.
+
+For example:
+
+  my $err_or_new_cust_pkg = $old_cust_pkg->change
+
+=cut
+
+#some false laziness w/order
+sub change {
+  my $self = shift;
+  my $opt = ref($_[0]) ? shift : { @_ };
+
+#  my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg, $refnum) = @_;
+#    
+
+  my $conf = new FS::Conf;
+
+  # Transactionize this whole mess
+  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;
+
+  my $error;
+
+  my %hash = (); 
+
+  my $time = time;
+
+  #$hash{$_} = $self->$_() foreach qw( last_bill bill );
+    
+  #$hash{$_} = $self->$_() foreach qw( setup );
+
+  $hash{'setup'} = $time if $self->setup;
+
+  $hash{'change_date'} = $time;
+  $hash{"change_$_"}  = $self->$_()
+    foreach qw( pkgnum pkgpart locationnum );
+
+  if ( $opt->{'cust_location'} &&
+       ( ! $opt->{'locationnum'} || $opt->{'locationnum'} == -1 ) ) {
+    $error = $opt->{'cust_location'}->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting cust_location (transaction rolled back): $error";
+    }
+    $opt->{'locationnum'} = $opt->{'cust_location'}->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  ),
+    %hash,
+  };
+
+  $error = $cust_pkg->insert( 'change' => 1 );
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  # Transfer services and cancel old package.
+
+  $error = $self->transfer($cust_pkg);
+  if ($error and $error == 0) {
+    # $old_pkg->transfer failed.
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
+    warn "trying transfer again with change_svcpart option\n" if $DEBUG;
+    $error = $self->transfer($cust_pkg, 'change_svcpart'=>1 );
+    if ($error and $error == 0) {
+      # $old_pkg->transfer failed.
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ($error > 0) {
+    # Transfers were successful, but we still had services left on the old
+    # package.  We can't change the package under this circumstances, so abort.
+    $dbh->rollback if $oldAutoCommit;
+    return "Unable to transfer all services from package ". $self->pkgnum;
+  }
+
+  #Good to go, cancel old package.
+  $error = $self->cancel( quiet=>1 );
+  if ($error) {
+    $dbh->rollback;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  $cust_pkg;
+
+}
+
 =item last_bill
 
 Returns the last bill date, or if there is no last bill date, the setup date.
@@ -2246,8 +2393,8 @@ sub order {
   my $dbh = dbh;
 
   my $error;
-  my $cust_main = qsearchs('cust_main', { custnum => $custnum });
-  return "Customer not found: $custnum" unless $cust_main;
+#  my $cust_main = qsearchs('cust_main', { custnum => $custnum });
+#  return "Customer not found: $custnum" unless $cust_main;
 
   my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
                          @$remove_pkgnum;
@@ -2257,15 +2404,19 @@ sub order {
   my %hash = (); 
   if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
 
-    my $time = time;
+    my $err_or_cust_pkg =
+      $old_cust_pkg[0]->change( 'pkgpart' => $pkgparts->[0],
+                                'refnum'  => $refnum,
+                              );
 
-    #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
-    
-    #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
-    $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
+    unless (ref($err_or_cust_pkg)) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_cust_pkg;
+    }
+
+    push @$return_cust_pkg, $err_or_cust_pkg;
+    return '';
 
-    $hash{'change_date'} = $time;
-    $hash{"change_$_"}  = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
   }
 
   # Create the new packages.
@@ -2328,8 +2479,10 @@ sub order {
 
 =item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
 
+A bulk change method to change packages for multiple customers.
+
 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
-L<FS::part_pkg>) to order for this customer.  Duplicates are of course
+L<FS::part_pkg>) to order for each customer.  Duplicates are of course
 permitted.
 
 REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
diff --git a/httemplate/edit/process/change-cust_pkg.html b/httemplate/edit/process/change-cust_pkg.html
new file mode 100644 (file)
index 0000000..7356e61
--- /dev/null
@@ -0,0 +1,46 @@
+% if ($error) {
+%   $cgi->param('error', $error);
+%   $cgi->redirect(popurl(3). 'misc/change_pkg.cgi?'. $cgi->query_string );
+% } else {
+
+    <% header("Package changed") %>
+      <SCRIPT TYPE="text/javascript">
+        window.top.location.reload();
+      </SCRIPT>
+    </BODY>
+    </HTML>
+
+% }
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Change customer package');
+
+my $cust_pkg = qsearchs({
+  #'select'    => 'cust_pkg.*',
+  'table'     => 'cust_pkg',
+  'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+  'hashref'   => { 'pkgnum' => scalar($cgi->param('pkgnum')), },
+  'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die 'unknown pkgnum' unless $cust_pkg;
+
+my %change = map { $_ => scalar($cgi->param($_)) }
+                 qw( locationnum pkgpart );
+
+if ( $cgi->param('locationnum') == -1 ) {
+  my $cust_location = new FS::cust_location {
+    'custnum' => $cust_pkg->custnum,
+    map { $_ => scalar($cgi->param($_)) }
+        qw( address1 address2 city county state zip country )
+  };
+  $change{'cust_location'} = $cust_location;
+}
+
+my $pkg_or_error = $cust_pkg->change( \%change );
+
+my $error = ref($pkg_or_error) ? '' : $pkg_or_error;
+
+</%init>
index 272ddcc..94c084e 100755 (executable)
@@ -1,22 +1,15 @@
 % if ($error) {
 %   $cgi->param('error', $error);
-%   $cgi->redirect(popurl(3). $error_redirect. '?'. $cgi->query_string );
-% } elsif ( $action eq 'change' ) {
-
-    <% header("Package changed") %>
-      <SCRIPT TYPE="text/javascript">
-        window.top.location.reload();
-      </SCRIPT>
-    </BODY>
-    </HTML>
-
-% } elsif ( $action eq 'bulk' ) {
-<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum") %>
+%   $cgi->redirect(popurl(3). 'edit/cust_pkg.cgi?'. $cgi->query_string );
 % } else {
-%   die "guru exception #5: action is neither change nor bulk!";
-% }
+<% $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum") %>
 <%init>
 
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('Bulk change customer packages');
+
 my $error = '';
 
 #untaint custnum
@@ -28,46 +21,19 @@ my @remove_pkgnums = map {
   $1;
 } $cgi->param('remove_pkg');
 
-my $curuser = $FS::CurrentUser::CurrentUser;
-
 my( $action, $error_redirect ) = ( '', '' );
 my @pkgparts = ();
-if ( $cgi->param('action') eq 'change' ) { #came from misc/change_pkg.cgi
-
-  $action = 'change';
-  $error_redirect = "misc/change_pkg.cgi";
 
-  die "access denied"
-    unless $curuser->access_right('Change customer package');
-
-  if ( $cgi->param('new_pkgpart') =~ /^(\d+)$/ ) {
-    @pkgparts = ($1);
-  } else {
-    $error = 'Select a new package';
-  }
-
-} elsif ( $cgi->param('action') eq 'bulk' ) { #came from edit/cust_pkg.cgi
-
-  $action = 'bulk';
-  $error_redirect = "edit/cust_pkg.cgi";
-
-  die "access denied"
-    unless $curuser->access_right('Bulk change customer packages');
-
-  foreach my $pkgpart ( map /^pkg(\d+)$/ ? $1 : (), $cgi->param ) {
-    if ( $cgi->param("pkg$pkgpart") =~ /^(\d+)$/ ) {
-      my $num_pkgs = $1;
-      while ( $num_pkgs-- ) {
-        push @pkgparts,$pkgpart;
-      }
-    } else {
-      $error = "Illegal quantity";
-      last;
+foreach my $pkgpart ( map /^pkg(\d+)$/ ? $1 : (), $cgi->param ) {
+  if ( $cgi->param("pkg$pkgpart") =~ /^(\d+)$/ ) {
+    my $num_pkgs = $1;
+    while ( $num_pkgs-- ) {
+      push @pkgparts,$pkgpart;
     }
+  } else {
+    $error = "Illegal quantity";
+    last;
   }
-
-} else {
-  die "guru exception #5: action is neither change nor bulk!";
 }
 
 $error ||= FS::cust_pkg::order($custnum,\@pkgparts,\@remove_pkgnums);
index f21b8ad..b0b30d3 100644 (file)
@@ -22,7 +22,7 @@ Example:
            NAME     = "<%$pre%>address1"
            ID       = "<%$pre%>address1"
            VALUE    = "<% $object->get($pre.'address1') |h %>"
-           SIZE     = 70
+           SIZE     = 58
            onChange = "<% $onchange %>"
            <% $disabled %>
            <% $style %>
@@ -37,7 +37,7 @@ Example:
            NAME     = "<%$pre%>address2"
            ID       = "<%$pre%>address2"
            VALUE    = "<% $object->get($pre.'address2') |h %>"
-           SIZE     = 70
+           SIZE     = 58
            onChange = "<% $onchange %>"
            <% $disabled %>
            <% $style %>
@@ -56,7 +56,7 @@ Example:
            <% $disabled %>
            <% $style %>
     >
 </TD>
+ </TD>
   <TH ALIGN="right" ID="<%$pre%>countylabel" <%$county_style%>><%$r%>County</TH>
   <TD>
     <% include('/elements/select-county.html', %select_hash ) %>
index b62c65c..5b023f1 100644 (file)
@@ -118,7 +118,7 @@ Example:
 </SCRIPT>
 
 <TR>
-  <TH ALIGN="right">Service location</TH>
+  <TH ALIGN="right">Service&nbsp;location</TH>
   <TD COLSPAN=7>
     <SELECT NAME="locationnum" onChange="locationnum_changed(this);">
       <OPTION VALUE="">(default service address)
@@ -166,7 +166,7 @@ if ( $locationnum && $locationnum != -1 ) {
     or die "unknown locationnum";
 } else {
   $cust_location = new FS::cust_location;
-  if ( $cgi->param('error') && $locationnum == -1 ) {
+  if ( $locationnum == -1 ) {
     $cust_location->$_( $cgi->param($_) ) foreach @location_fields;
   } else {
     $cust_location->$_( $cust_main->$_() ) foreach @location_fields;
index d6a24fb..c4dfca2 100755 (executable)
@@ -2,35 +2,37 @@
 
 <% include('/elements/error.html') %>
 
-<FORM ACTION="<% $p %>edit/process/cust_pkg.cgi" METHOD=POST>
-<INPUT TYPE="hidden" NAME="action" VALUE="change">
-<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
-<INPUT TYPE="hidden" NAME="remove_pkg" VALUE="<% $pkgnum %>">
+<FORM ACTION="<% $p %>edit/process/change-cust_pkg.html" METHOD=POST>
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $pkgnum %>">
 
 <% ntable('#cccccc') %>
 
   <TR>
-    <TD>Current package:&nbsp;</TD>
-    <TD>
-      <B><% $part_pkg->pkgpart %>: <% $part_pkg->pkg %> - <% $part_pkg->comment %></B>
+    <TH ALIGN="right">Current package</TH>
+    <TD COLSPAN=7>
+      <% $curuser->option('show_pkgnum') ? $cust_pkg->pkgnum.': ' : '' %><B><% $part_pkg->pkg |h %></B> - <% $part_pkg->comment |h %>
     </TD>
   </TR>
   
   <TR>
-    <TD>New package: </TD>
-    <TD><% include('/elements/select-cust-part_pkg.html',
-                     'cust_main'    => $cust_main,
-                     'element_name' => 'new_pkgpart',
-                     'extra_sql'    => ' AND pkgpart != '. $cust_pkg->pkgpart,
-                     'curr_value'   => ( $cgi->param('error')
-                                           ? scalar($cgi->param('new_pkgpart'))
-                                           : ''
-                                       ),
-                  )
-        %>
+    <TH ALIGN="right">New package</TH>
+    <TD COLSPAN=7>
+      <% include('/elements/select-cust-part_pkg.html',
+                   'cust_main'    => $cust_main,
+                   'element_name' => 'pkgpart',
+                   #'extra_sql'    => ' AND pkgpart != '. $cust_pkg->pkgpart,
+                   'curr_value'   => scalar($cgi->param('pkgpart')),
+                )
+      %>
     </TD>
   </TR>
 
+  <% include('/elements/tr-select-cust_location.html',
+               'cgi'       => $cgi,
+               'cust_main' => $cust_main,
+            )
+  %>
+
 </TABLE>
 
 <BR>
 
 <%init>
 
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Change customer package');
-
-my $pkgnum;
-if ( $cgi->param('error') ) {
-  $pkgnum = ($cgi->param('remove_pkg'))[0];
-} else {
-  $pkgnum = $cgi->param('pkgnum');
-}
+  unless $curuser->access_right('Change customer package');
+
+my $pkgnum = scalar($cgi->param('pkgnum'));
 $pkgnum =~ /^(\d+)$/ or die "illegal pkgnum $pkgnum";
 $pkgnum = $1;
 
-my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } )
-  or die "unknown pkgnum $pkgnum";
-my $custnum = $cust_pkg->custnum;
-
-my $conf = new FS::Conf;
+my $cust_pkg =
+  qsearchs({
+    'table'     => 'cust_pkg',
+    'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
+    'hashref'   => { 'pkgnum' => $pkgnum },
+    'extra_sql' => ' AND '. $curuser->agentnums_sql,
+  }) or die "unknown pkgnum $pkgnum";
 
 my $cust_main = $cust_pkg->cust_main
   or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
          " ( pkgnum ". cust_pkg->pkgnum. ")";
-my $agent = $cust_main->agent;
 
 my $part_pkg = $cust_pkg->part_pkg;
 
index 0f58b36..3c64130 100644 (file)
 
   </I>
 
+% if ($FS::CurrentUser::CurrentUser->access_right('Change customer package')) {
+  <FONT SIZE=-1>
+    (&nbsp;<%pkg_change_location_link($cust_pkg)%>&nbsp;)
+  </FONT>
+% } 
+
 </TD>
 <%init>
 
 my %opt = @_;
 
+my $conf = new FS::Conf;
+
 my $bgcolor        = $opt{'bgcolor'};
 my $cust_pkg       = $opt{'cust_pkg'};
 my $part_pkg       = $opt{'part_pkg'};
 my $conf           = new FS::Conf;
 my $countrydefault = $conf->config('countrydefault') || 'US';
+my $statedefault   = $conf->config('statedefault')
+                     || ($countrydefault eq 'US' ? 'CA' : '');
 
 my $loc = $cust_pkg->cust_location_or_main;
 
+sub pkg_change_location_link {
+  my $cust_pkg = shift;
+  my $pkgpart = $cust_pkg->pkgpart;
+  include( '/elements/popup_link-cust_pkg.html',
+    'action'      => $p. "misc/change_pkg.cgi?locationnum=-1;pkgpart=$pkgpart;".
+                     "address1=;address2=;city=;county=;state=$statedefault;".
+                     "zip=;country=$countrydefault",
+    'label'       => 'Change&nbsp;location',
+    'actionlabel' => 'Change',
+    'cust_pkg'    => $cust_pkg,
+  );
+}
+
 </%init>
index 4311d8c..b07e1af 100644 (file)
@@ -184,12 +184,14 @@ sub pkg_link {
 }
 
 sub pkg_change_link {
+  my $cust_pkg = shift;
+  my $locationnum = $cust_pkg->locationnum;
   include( '/elements/popup_link-cust_pkg.html',
-             'action'      => $p. 'misc/change_pkg.cgi?dummy=value',
-             'label'       => 'Change&nbsp;package',
-             'actionlabel' => 'Change',
-             'cust_pkg'    => shift,
-         )
+    'action'      => $p. "misc/change_pkg.cgi?locationnum=$locationnum",
+    'label'       => 'Change&nbsp;package',
+    'actionlabel' => 'Change',
+    'cust_pkg'    => $cust_pkg,
+  );
 }
 
 sub pkg_dates_link { pkg_link('edit/REAL_cust_pkg', 'Edit&nbsp;dates', @_ ); }