Merge branch 'master' of git.freeside.biz:/home/git/freeside
authorIvan Kohler <ivan@freeside.biz>
Thu, 13 Sep 2018 20:02:28 +0000 (13:02 -0700)
committerIvan Kohler <ivan@freeside.biz>
Thu, 13 Sep 2018 20:02:28 +0000 (13:02 -0700)
31 files changed:
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Misc/Savepoint.pm
FS/FS/UID.pm
FS/FS/access_user.pm
FS/FS/contact.pm
FS/FS/cust_bill.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Billing_Realtime.pm
FS/FS/cust_payby.pm
FS/FS/part_export/nena2.pm
FS/FS/part_export/saisei.pm
httemplate/edit/cust_main-contacts.html
httemplate/edit/elements/edit.html
httemplate/edit/process/cust_main-contacts.html
httemplate/elements/change_password.html
httemplate/elements/contact.html
httemplate/elements/header.html
httemplate/elements/menu.html
httemplate/elements/validate_password.html
httemplate/elements/validate_password_js.html [new file with mode: 0644]
httemplate/misc/edge_browser_check-fail_notice.html [new file with mode: 0644]
httemplate/misc/edge_browser_check-header.html [new file with mode: 0644]
httemplate/misc/edge_browser_check-iframe.html [new file with mode: 0644]
httemplate/misc/process/change-password.html
httemplate/search/future_autobill.html
httemplate/search/report_future_autobill.html
httemplate/view/cust_main/contacts_new.html
min_selfservice/index.php
min_selfservice/login.php

index 263b311..57d4298 100644 (file)
@@ -184,6 +184,29 @@ sub skin_info {
 
 }
 
+sub get_mac_address {
+  my $p = shift;
+
+## access radius exports acct tables to get mac
+  my @part_export = ();
+  @part_export = (
+    qsearch( 'part_export', { 'exporttype' => 'sqlradius' } ),
+    qsearch( 'part_export', { 'exporttype' => 'sqlradius_withdomain' } ),
+    qsearch( 'part_export', { 'exporttype' => 'broadband_sqlradius' } ),
+  );
+
+  my @sessions;
+  foreach my $part_export (@part_export) {
+    push @sessions, ( @{ $part_export->usage_sessions( {
+      'ip' => $p->{'ip'},
+    } ) } );
+  }
+
+  my $mac = $sessions[0]->{'callingstationid'};
+
+  return { 'mac_address' => $mac, };
+}
+
 sub login_info {
   my $p = shift;
 
@@ -239,8 +262,11 @@ sub login {
 
   } elsif ( $p->{'domain'} eq 'ip_mac' ) {
 
-      my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $p->{'username'} } );
-      return { error => 'IP address not found' }
+      my $mac_address = $p->{'username'};
+      $mac_address =~ s/\://g;
+
+      my $svc_broadband = qsearchs( 'svc_broadband', { 'mac_addr' => $mac_address } );
+      return { error => 'MAC address not found '.$p->{'username'} }
         unless $svc_broadband;
       $svc_x = $svc_broadband;
 
index dcf34fd..db0537c 100644 (file)
@@ -227,6 +227,7 @@ sub ss2clientapi {
   'quotation_add_pkg'         => 'MyAccount/quotation/quotation_add_pkg',
   'quotation_remove_pkg'      => 'MyAccount/quotation/quotation_remove_pkg',
   'quotation_order'           => 'MyAccount/quotation/quotation_order',
+  'get_mac_address'           => 'MyAccount/get_mac_address',
 
   'freesideinc_service'       => 'Freeside/freesideinc_service',
   };
index b15b36d..f8e2c5f 100644 (file)
@@ -55,7 +55,7 @@ Savepoints cannot work while AutoCommit is enabled.
 Savepoint labels must be valid sql identifiers.  If your choice of label
 would not make a valid column name, it probably will not make a valid label.
 
-Savepint labels must be unique within the transaction.
+Savepoint labels must be unique within the transaction.
 
 =cut
 
index 50a9178..693e5d9 100644 (file)
@@ -5,7 +5,7 @@ use strict;
 use vars qw(
   @EXPORT_OK $DEBUG $me $cgi $freeside_uid $conf_dir $cache_dir
   $secrets $datasrc $db_user $db_pass $schema $dbh $driver_name
-  $AutoCommit %callback @callback $callback_hack
+  $AutoCommit $ForceObeyAutoCommit %callback @callback $callback_hack
 );
 use subs qw( getsecrets );
 use Carp qw( carp croak cluck confess );
@@ -26,7 +26,17 @@ $freeside_uid = scalar(getpwnam('freeside'));
 $conf_dir  = "%%%FREESIDE_CONF%%%";
 $cache_dir = "%%%FREESIDE_CACHE%%%";
 
+# Code wanting to issue a COMMIT statement to the database is expected to
+# obey the convention of checking this flag first.  Setting $AutoCommit = 0
+# should (usually) suppress COMMIT statements.
 $AutoCommit = 1; #ours, not DBI
+
+# Not all methods obey $AutoCommit, by design choice.  Setting
+# $ForceObeyAutoCommit = 1 will override that design choice for:
+#   &FS::cust_main::Billing::collect
+#   &FS::cust_main::Billing::do_cust_event
+$ForceObeyAutoCommit = 0;
+
 $callback_hack = 0;
 
 =head1 NAME
index a9fdf5b..f23aa77 100644 (file)
@@ -12,6 +12,7 @@ use FS::Record qw( qsearch qsearchs dbh );
 use FS::agent;
 use FS::cust_main;
 use FS::sales;
+use Carp qw( croak );
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
@@ -814,6 +815,103 @@ sub set_page_pref {
   return $error;
 }
 
+=item get_pref NAME
+
+Fetch the prefvalue column from L<FS::access_user_pref> for prefname NAME
+
+Returns undef when no value has been saved, or when record has expired
+
+=cut
+
+sub get_pref {
+  my ( $self, $prefname ) = @_;
+  croak 'prefname parameter requrired' unless $prefname;
+
+  my $pref_row = $self->get_pref_row( $prefname )
+    or return undef;
+
+  return undef
+    if $pref_row->expiration
+    && $pref_row->expiration < time();
+
+  $pref_row->prefvalue;
+}
+
+=item get_pref_row NAME
+
+Fetch the row object from L<FS::access_user_pref> for prefname NAME
+
+returns undef when no row has been created
+
+=cut
+
+sub get_pref_row {
+  my ( $self, $prefname ) = @_;
+  croak 'prefname parameter required' unless $prefname;
+
+  qsearchs(
+    access_user_pref => {
+      usernum    => $self->usernum,
+      prefname   => $prefname,
+    }
+  );
+}
+
+=item set_pref NAME, VALUE, [EXPIRATION_EPOCH]
+
+Add or update user preference in L<FS::access_user_pref> table
+
+Passing an undefined VALUE will delete the user preference
+
+Returns VALUE
+
+=cut
+
+sub set_pref {
+  my $self = shift;
+  my ( $prefname, $prefvalue, $expiration ) = @_;
+
+  return $self->delete_pref( $prefname )
+    unless defined $prefvalue;
+
+  if ( my $pref_row = $self->get_pref_row( $prefname )) {
+    return $prefvalue
+      if $pref_row->prefvalue eq $prefvalue;
+
+    $pref_row->prefvalue( $prefvalue );
+    $pref_row->expiration( $expiration || '');
+
+    if ( my $error = $pref_row->replace ) { croak $error }
+
+    return $prefvalue;
+  }
+
+  my $pref_row = FS::access_user_pref->new({
+    usernum    => $self->usernum,
+    prefname   => $prefname,
+    prefvalue  => $prefvalue,
+    expiration => $expiration,
+  });
+  if ( my $error = $pref_row->insert ) { croak $error }
+
+  $prefvalue;
+}
+
+=item delete_pref NAME
+
+Delete user preference from L<FS::access_user_pref> table
+
+=cut
+
+sub delete_pref {
+  my ( $self, $prefname ) = @_;
+
+  my $pref_row = $self->get_pref_row( $prefname )
+    or return;
+
+  if ( my $error = $pref_row->delete ) { croak $error }
+}
+
 =back
 
 =head1 BUGS
index fa047f5..81dfdbc 100644 (file)
@@ -199,8 +199,6 @@ sub insert {
 
   }
 
-  $error ||= $self->insert_password_history;
-
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -302,6 +300,15 @@ sub insert {
     }
   }
 
+  if ( $self->get('password') ) {
+    my $error = $self->is_password_allowed($self->get('password'))
+          ||  $self->change_password($self->get('password'));
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   '';
@@ -811,7 +818,7 @@ sub authenticate_password {
 
     $hash eq $check_hash;
 
-  } else { 
+  } else {
 
     return 0 if $self->_password eq '';
 
index 47f71c4..7158cb2 100644 (file)
@@ -41,6 +41,7 @@ use FS::cust_bill_void;
 use FS::reason;
 use FS::reason_type;
 use FS::L10N;
+use FS::Misc::Savepoint;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
@@ -974,6 +975,9 @@ sub apply_payments_and_credits {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  my $savepoint_label = 'cust_bill__apply_payments_and_credits';
+  savepoint_create( $savepoint_label );
+
   $self->select_for_update; #mutex
 
   my @payments = grep { $_->unapplied > 0 }
@@ -1062,6 +1066,7 @@ sub apply_payments_and_credits {
 
     my $error = $app->insert(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
     }
@@ -1069,6 +1074,7 @@ sub apply_payments_and_credits {
 
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index ea524da..2e8fe81 100644 (file)
@@ -79,6 +79,7 @@ use FS::sales;
 use FS::cust_payby;
 use FS::contact;
 use FS::reason;
+use FS::Misc::Savepoint;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -2212,11 +2213,15 @@ sub cancel_pkgs {
   my( $self, %opt ) = @_;
 
   # we're going to cancel services, which is not reversible
+  #   unless exports are suppressed
   die "cancel_pkgs cannot be run inside a transaction"
-    if $FS::UID::AutoCommit == 0;
+    if !$FS::UID::AutoCommit && !$FS::svc_Common::noexport_hack;
 
+  my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
 
+  savepoint_create('cancel_pkgs');
+
   return ( 'access denied' )
     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
@@ -2233,7 +2238,8 @@ sub cancel_pkgs {
       my $ban = new FS::banned_pay $cust_payby->_new_banned_pay_hashref;
       my $error = $ban->insert;
       if ($error) {
-        dbh->rollback;
+        savepoint_rollback_and_release('cancel_pkgs');
+        dbh->rollback if $oldAutoCommit;
         return ( $error );
       }
 
@@ -2253,11 +2259,13 @@ sub cancel_pkgs {
                              'time'     => $cancel_time );
     if ($error) {
       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
-      dbh->rollback;
+      savepoint_rollback_and_release('cancel_pkgs');
+      dbh->rollback if $oldAutoCommit;
       return ( "Error billing during cancellation: $error" );
     }
   }
-  dbh->commit;
+  savepoint_release('cancel_pkgs');
+  dbh->commit if $oldAutoCommit;
 
   my @errors;
   # try to cancel each service, the same way we would for individual packages,
@@ -2271,17 +2279,22 @@ sub cancel_pkgs {
   warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
     $self->custnum."\n"
     if $DEBUG;
+  my $i = 0;
   foreach my $cust_svc (@sorted_cust_svc) {
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     my $part_svc = $cust_svc->part_svc;
     next if ( defined($part_svc) and $part_svc->preserve );
     # immediate cancel, no date option
     # transactionize individually
     my $error = try { $cust_svc->cancel } catch { $_ };
     if ( $error ) {
-      dbh->rollback;
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
       push @errors, $error;
     } else {
-      dbh->commit;
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
     }
   }
   if (@errors) {
@@ -2297,8 +2310,11 @@ sub cancel_pkgs {
     @cprs = @{ delete $opt{'cust_pkg_reason'} };
   }
   my $null_reason;
+  $i = 0;
   foreach (@pkgs) {
     my %lopt = %opt;
+    my $savepoint = 'cancel_pkgs_'.$i++;
+    savepoint_create( $savepoint );
     if (@cprs) {
       my $cpr = shift @cprs;
       if ( $cpr ) {
@@ -2319,10 +2335,12 @@ sub cancel_pkgs {
     }
     my $error = $_->cancel(%lopt);
     if ( $error ) {
-      dbh->rollback;
+      savepoint_rollback_and_release( $savepoint );
+      dbh->rollback if $oldAutoCommit;
       push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
     } else {
-      dbh->commit;
+      savepoint_release( $savepoint );
+      dbh->commit if $oldAutoCommit;
     }
   }
 
index 71d5c9b..1be7d39 100644 (file)
@@ -26,6 +26,7 @@ use FS::pkg_category;
 use FS::FeeOrigin_Mixin;
 use FS::Log;
 use FS::TaxEngine;
+use FS::Misc::Savepoint;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -1753,7 +1754,10 @@ sub collect {
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
   #never want to roll back an event just because it returned an error
-  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+  # unless $FS::UID::ForceObeyAutoCommit is set
+  local $FS::UID::AutoCommit = 1
+    unless !$oldAutoCommit
+        && $FS::UID::ForceObeyAutoCommit;
 
   $self->do_cust_event(
     'debug'      => ( $options{'debug'} || 0 ),
@@ -1961,9 +1965,13 @@ sub do_cust_event {
   }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
   #never want to roll back an event just because it or a different one
   # returned an error
-  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+  # unless $FS::UID::ForceObeyAutoCommit is set
+  local $FS::UID::AutoCommit = 1
+    unless !$oldAutoCommit
+        && $FS::UID::ForceObeyAutoCommit;
 
   foreach my $cust_event ( @$due_cust_event ) {
 
@@ -2288,16 +2296,21 @@ sub apply_payments_and_credits {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  my $savepoint_label = 'Billing__apply_payments_and_credits';
+  savepoint_create( $savepoint_label );
+
   $self->select_for_update; #mutex
 
   foreach my $cust_bill ( $self->open_cust_bill ) {
     my $error = $cust_bill->apply_payments_and_credits(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error applying: $error";
     }
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
index d286f63..714a2e6 100644 (file)
@@ -16,6 +16,7 @@ use FS::cust_bill_pay;
 use FS::cust_refund;
 use FS::banned_pay;
 use FS::payment_gateway;
+use FS::Misc::Savepoint;
 
 $realtime_bop_decline_quiet = 0;
 
@@ -27,6 +28,7 @@ $me = '[FS::cust_main::Billing_Realtime]';
 
 our $BOP_TESTING = 0;
 our $BOP_TESTING_SUCCESS = 1;
+our $BOP_TESTING_TIMESTAMP = '';
 
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
@@ -405,7 +407,7 @@ sub realtime_bop {
 
   confess "Can't call realtime_bop within another transaction ".
           '($FS::UID::AutoCommit is false)'
-    unless $FS::UID::AutoCommit;
+    unless $FS::UID::AutoCommit || $BOP_TESTING;
 
   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
 
@@ -682,7 +684,7 @@ sub realtime_bop {
   my $cust_pay_pending = new FS::cust_pay_pending {
     'custnum'           => $self->custnum,
     'paid'              => $options{amount},
-    '_date'             => '',
+    '_date'             => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
     'payby'             => $bop_method2payby{$options{method}},
     'payinfo'           => $options{payinfo},
     'paymask'           => $options{paymask},
@@ -757,7 +759,7 @@ sub realtime_bop {
     return { reference => $cust_pay_pending->paypendingnum,
              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
 
-  } elsif ( $transaction->is_success() && $action2 ) {
+  } elsif ( !$BOP_TESTING && $transaction->is_success() && $action2 ) {
 
     $cust_pay_pending->status('authorized');
     my $cpp_authorized_err = $cust_pay_pending->replace;
@@ -946,7 +948,7 @@ sub _realtime_bop_result {
        'custnum'  => $self->custnum,
        'invnum'   => $options{'invnum'},
        'paid'     => $cust_pay_pending->paid,
-       '_date'    => '',
+       '_date'    => $BOP_TESTING ? $BOP_TESTING_TIMESTAMP : '',
        'payby'    => $cust_pay_pending->payby,
        'payinfo'  => $options{'payinfo'},
        'paymask'  => $options{'paymask'} || $cust_pay_pending->paymask,
@@ -967,12 +969,16 @@ sub _realtime_bop_result {
     local $FS::UID::AutoCommit = 0;
     my $dbh = dbh;
 
+    my $savepoint_label = '_realtime_bop_result';
+    savepoint_create( $savepoint_label );
+
     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
 
     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
 
     if ( $error ) {
-      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      savepoint_rollback( $savepoint_label );
+
       $cust_pay->invnum(''); #try again with no specific invnum
       $cust_pay->paynum('');
       my $error2 = $cust_pay->insert( $options{'manual'} ?
@@ -981,7 +987,8 @@ sub _realtime_bop_result {
       if ( $error2 ) {
         # gah.  but at least we have a record of the state we had to abort in
         # from cust_pay_pending now.
-        $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+        savepoint_rollback_and_release( $savepoint_label );
+
         my $e = "WARNING: $options{method} captured but payment not recorded -".
                 " error inserting payment (". $payment_gateway->gateway_module.
                 "): $error2".
@@ -996,9 +1003,10 @@ sub _realtime_bop_result {
     my $jobnum = $cust_pay_pending->jobnum;
     if ( $jobnum ) {
        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
-      
+
        unless ( $placeholder ) {
-         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+         savepoint_rollback_and_release( $savepoint_label );
+
          my $e = "WARNING: $options{method} captured but job $jobnum not ".
              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
          warn $e;
@@ -1008,7 +1016,8 @@ sub _realtime_bop_result {
        $error = $placeholder->delete;
 
        if ( $error ) {
-         $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+        savepoint_rollback_and_release( $savepoint_label );
+
          my $e = "WARNING: $options{method} captured but could not delete ".
               "job $jobnum for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $error\n";
@@ -1030,8 +1039,8 @@ sub _realtime_bop_result {
     my $cpp_done_err = $cust_pay_pending->replace;
 
     if ( $cpp_done_err ) {
+      savepoint_rollback_and_release( $savepoint_label );
 
-      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
       my $e = "WARNING: $options{method} captured but payment not recorded - ".
               "error updating status for paypendingnum ".
               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
@@ -1039,7 +1048,7 @@ sub _realtime_bop_result {
       return $e;
 
     } else {
-
+      savepoint_release( $savepoint_label );
       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
 
       if ( $options{'apply'} ) {
index c497059..9d8be12 100644 (file)
@@ -1,5 +1,6 @@
 package FS::cust_payby;
 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
+use feature 'state';
 
 use strict;
 use Scalar::Util qw( blessed );
@@ -914,8 +915,81 @@ sub search_sql {
 
 =back
 
+=item has_autobill_cards
+
+Returns the number of unexpired cards configured for autobill
+
+=cut
+
+sub has_autobill_cards {
+  scalar FS::Record::qsearch({
+    table     => 'cust_payby',
+    addl_from => 'JOIN cust_main USING (custnum)',
+    order_by  => 'LIMIT 1',
+    hashref   => {
+        paydate => { op => '>', value => DateTime->now->ymd },
+        weight  => { op => '>',  value => 0 },
+    },
+    extra_sql =>
+      "AND payby IN ('CARD', 'DCRD') ".
+      'AND '.
+      $FS::CurrentUser::CurrentUser->agentnums_sql( table => 'cust_main' ),
+  });
+}
+
+=item has_autobill_checks
+
+Returns the number of check accounts configured for autobill
+
+=cut
+
+sub has_autobill_checks {
+  scalar FS::Record::qsearch({
+    table     => 'cust_payby',
+    addl_from => 'JOIN cust_main USING (custnum)',
+    order_by  => 'LIMIT 1',
+    hashref   => {
+        weight  => { op => '>',  value => 0 },
+    },
+    extra_sql =>
+      "AND payby IN ('CHEK','DCHEK','DCHK') ".
+      'AND '.
+      $FS::CurrentUser::CurrentUser->agentnums_sql( table => 'cust_main' ),
+  });
+}
+
+=item future_autobill_report_title
+
+Determine if the future_autobill report should be available.
+If so, return a dynamic title for it
+
 =cut
 
+sub future_autobill_report_title {
+  # Perhaps this function belongs somewhere else
+  state $title;
+  return $title if defined $title;
+
+  # Report incompatible with tax engines
+  return $title = '' if FS::TaxEngine->new->info->{batch};
+
+  my $has_cards  = has_autobill_cards();
+  my $has_checks = has_autobill_checks();
+  my $_title = 'Future %s transactions';
+
+  if ( $has_cards && $has_checks ) {
+    $title = sprintf $_title, 'credit card and electronic check';
+  } elsif ( $has_cards ) {
+    $title = sprintf $_title, 'credit card';
+  } elsif ( $has_checks ) {
+    $title = sprintf $_title, 'electronic check';
+  } else {
+    $title = '';
+  }
+
+  $title;
+}
+
 sub _upgrade_data {
 
   my $class = shift;
index f6a730e..cc4069c 100644 (file)
@@ -10,6 +10,7 @@ use Date::Format qw(time2str);
 use Parse::FixedLength;
 use File::Temp qw(tempfile);
 use vars qw(%info %options $initial_load_hack $DEBUG);
+use Carp qw( carp );
 
 my %upload_targets;
 
@@ -396,6 +397,13 @@ sub process {
   my $self = shift;
   my $batch = shift;
   local $DEBUG = $self->option('debug');
+
+  if ( $FS::svc_Common::noexport_hack ) {
+    carp 'FS::part_export::nena2::process() suppressed by noexport_hack'
+      if $DEBUG;
+    return;
+  }
+
   local $FS::UID::AutoCommit = 0;
   my $error;
 
index 1b7295d..6db43c1 100644 (file)
@@ -201,12 +201,28 @@ sub _export_insert {
     my $accesspoint = process_sector($self, $sector_opt);
     return $self->api_error if $self->{'__saisei_error'};
 
+## get custnum and pkgpart from cust_pkg for virtual access point
+    my $cust_pkg = FS::Record::qsearchs({
+      'table'     => 'cust_pkg',
+      'hashref'   => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
+    });
+    my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
+
+    my $virtual_ap_opt = {
+      'virtual_name'           => $virtual_ap_name,
+      'sector_name'            => $sector_name,
+      'virtual_uprate_limit'   => $svc_broadband->{Hash}->{speed_up},
+      'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
+    };
+    my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
+    return $self->api_error if $self->{'__saisei_error'};
+
     ## tie host to user add sector name as access point.
     $self->api_add_host_to_user(
       $user->{collection}->[0]->{name},
       $rateplan->{collection}->[0]->{name},
       $svc_broadband->{Hash}->{ip_addr},
-      $accesspoint->{collection}->[0]->{name},
+      $virtual_ap->{collection}->[0]->{name},
     ) unless $self->{'__saisei_error'};
   }
 
@@ -216,8 +232,8 @@ sub _export_insert {
 
 sub _export_replace {
   my ($self, $svc_broadband) = @_;
-  $self->_export_insert($svc_broadband);
-  return '';
+  my $error = $self->_export_insert($svc_broadband);
+  return $error;
 }
 
 sub _export_delete {
@@ -817,6 +833,44 @@ sub process_sector {
   return $accesspoint;
 }
 
+sub process_virtual_ap {
+  my ($self, $opt) = @_;
+
+  my $existing_virtual_ap;
+  my $virtual_name = $opt->{virtual_name};
+
+  #check if sector has been set up as an access point.
+  $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
+
+  # modify the existing virtual accesspoint if changing it. this should never happen
+  $self->api_modify_existing_accesspoint (
+    $virtual_name,
+    $opt->{sector_name},
+    $opt->{virtual_uprate_limit},
+    $opt->{virtual_downrate_limit},
+  ) if $existing_virtual_ap && $opt->{modify_existing};
+
+  #if virtual ap does not exist as an access point create it.
+  $self->api_create_accesspoint(
+    $virtual_name,
+    $opt->{virtual_uprate_limit},
+    $opt->{virtual_downrate_limit},
+  ) unless $existing_virtual_ap;
+
+my $update_sector;
+if ($existing_virtual_ap && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
+  $update_sector = 1;
+}
+
+  # Attach newly created virtual ap to tower sector ap or if sector has changed.
+  $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
+
+  # set access point to existing one or newly created one.
+  my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
+
+  return $accesspoint;
+}
+
 sub export_provisioned_services {
   my $job = shift;
   my $param = shift;
index 3783cb9..abef750 100644 (file)
@@ -5,18 +5,20 @@ this one isn't being maintained well.  :/
 
 </%doc>
 
-  <SCRIPT>
-function checkPasswordValidation(fieldid)  {
-  var validationResult = document.getElementById(fieldid+'_result').innerHTML;
-  if (validationResult.match(/Password valid!/)) {
-    return true;
+<SCRIPT>
+  function checkPasswordValidation(fieldid)  {
+    var validationResult = document.getElementById(fieldid+'_result').innerHTML;
+    if (validationResult.match(/Password valid!/)) {
+      return true;
+    }
+    else {
+      return false;
+    }
   }
-  else {
-    return false;
-  }
-}
 </SCRIPT>
 
+<& '/elements/validate_password_js.html', &>
+
 <& elements/edit.html,
      'name_singular'    => 'customer contacts', #yes, we're editing all of them
      'table'            => 'cust_main',
@@ -58,6 +60,13 @@ function checkPasswordValidation(fieldid)  {
 my $curuser = $FS::CurrentUser::CurrentUser;
 my $conf = new FS::Conf;
 
+if ( $cgi->param('redirect') ) {
+  my $session = $cgi->param('redirect');
+  my $pref = $curuser->option("redirect$session");
+  die "unknown redirect session $session\n" unless length($pref);
+  $cgi = new CGI($pref);
+}
+
 my $custnum;
 if ( $cgi->param('error') ) {
   $custnum = scalar($cgi->param('custnum'));
index 8ba703a..b7f2e7a 100644 (file)
@@ -669,7 +669,7 @@ Example:
           var newrow =  <% include(@layer_opt, html_only=>1) |js_string %>;
 
 %         #until the rest have html/js_only
-%         if ( ($type eq 'selectlayers') || ($type eq 'selectlayersx') || ($type =~ /^select-cgp_rule_/) ) {
+%         if ( ($type eq 'selectlayers') || ($type eq 'selectlayersx') || ($type =~ /^select-cgp_rule_/) || ($type eq 'contact') ) {
             var newfunc = <% include(@layer_opt, js_only=>1) |js_string %>;
 %         } else {
             var newfunc = '';
index 5b8319f..6b7f1c2 100644 (file)
@@ -8,7 +8,7 @@
 </%doc>
 <% include('elements/process.html',
      'table'          => 'cust_main',
-     'error_redirect' => popurl(3). 'edit/cust_main-contacts.html?',
+     'error_redirect' => popurl(3). 'edit/cust_main-contacts.html',
      'agent_virt'     => 1,
      'skip_process'   => 1, #we don't want to make any changes to cust_main
      'precheck_callback' => $precheck_callback,
index 7d95e19..65b7d85 100644 (file)
@@ -11,9 +11,9 @@
 % if (!$opt{'no_label_display'}) {
 <A ID="<%$pre%>link" HREF="javascript:void(0)" onclick="<%$pre%>toggle(true)">(<% emt( $change_title ) %>)</A>
 % }
-<DIV ID="<%$pre%>form" CLASS="passwordbox">
+<DIV ID="<%$pre%>div" CLASS="passwordbox">
 % if (!$opt{'noformtag'}) {
-  <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html" onsubmit="return checkPasswordValidation()">
+  <FORM ID="<%$pre%>form" METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html" onsubmit="return <%$pre%>checkPasswordValidation()">
 % }
 
     <% $change_id_input %>
@@ -44,11 +44,8 @@ function <%$pre%>toggle(toggle, clear) {
   if (clear) {
     document.getElementById('<%$pre%>password').value = '';
     document.getElementById('<%$pre%>password_result').innerHTML = '';
-% if ($opt{'contact_num'}) {
-    document.getElementById('<% $opt{'pre_pwd_field_label'} %>selfservice_access').value = 'Y';
-% }
 }
-  document.getElementById('<%$pre%>form').style.display =
+  document.getElementById('<%$pre%>div').style.display =
     toggle ? 'inline-block' : 'none';
 % if (!$opt{'no_label_display'}) {
   document.getElementById('<%$pre%>link').style.display =
@@ -56,7 +53,7 @@ function <%$pre%>toggle(toggle, clear) {
 % }
 }
 
-function checkPasswordValidation()  {
+function <%$pre%>checkPasswordValidation(resultId)  {
   var validationResult = document.getElementById('<%$pre%>password_result').innerHTML;
   if (validationResult.match(/Password valid!/)) {
     return true;
@@ -83,8 +80,8 @@ if ($opt{'svc_acct'}) {
 }
 elsif ($opt{'contact_num'}) {
   $change_id_input = '
-    <INPUT TYPE="hidden" NAME="'.$opt{'pre_pwd_field_label'}.'contactnum" VALUE="' . $opt{'contact_num'} . '">
-    <INPUT TYPE="hidden" NAME="'.$opt{'pre_pwd_field_label'}.'custnum" VALUE="' . $opt{'custnum'} . '">
+    <INPUT TYPE="hidden" NAME="contactnum" VALUE="' . $opt{'contact_num'} . '">
+    <INPUT TYPE="hidden" NAME="custnum" VALUE="' . $opt{'custnum'} . '">
   ';
   $pre .= $opt{'pre_pwd_field_label'};
 }
index 7b6c853..909ff78 100644 (file)
@@ -1,4 +1,6 @@
-% unless ( $opt{'js_only'} ) {
+% if ( $opt{'js_only'} ) {
+<% $js %>
+% } else {
 
   <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
 
                    VALUE = ""
                    placeholder = "<% $value |h %>"
             >
-%           my $contactnum = $curr_value ? $curr_value : '0';
-            <& '/elements/validate_password.html',
-             'fieldid'    => "changepw".$id."_password",
-             'svcnum'     => '',
-             'contactnum' => $contactnum,
-             'submitid'   => "submit",
-           &>
-
-            <SCRIPT TYPE="text/javascript">
-                    var selfService = document.getElementById("<%$id%>_selfservice_access").value;
-
-                    if (selfService !== "Y") { document.getElementById("changepw<%$id%>_password").disabled = 'true'; }
-                    document.getElementById("<%$id%>_selfservice_access").onchange = function() {
-                      if (this.value == "P" || this.value == "E" || this.value =="Y") {
-                        document.getElementById("changepw<%$id%>_password").disabled = '';
-                      }
-                      else { document.getElementById("changepw<%$id%>_password").disabled = 'true'; }
-                      return false;
-                    }
+            <SCRIPT>
+              <% $js %>
             </SCRIPT>
 %         } elsif ( $field eq 'invoice_dest' || $field eq 'message_dest' ) {
 %           my $curr_value = $cgi->param($name . '_' . $field);
           <BR>
           <FONT SIZE="-1"><% $label{$field} %></FONT>
 %       if ( $field eq 'password' ) {
-          <div id="changepw<%$id%>_<%$field%>_result"></div>
+          <DIV ID="changepw<%$id%>_<%$field%>_result" STYLE="font-size: smaller"></DIV>
 %       }
         </TD>
 %     }
@@ -138,6 +123,7 @@ my $name = $opt{'element_name'} || $opt{'field'} || 'contactnum';
 my $id = $opt{'id'} || 'contactnum';
 
 my $curr_value = $opt{'curr_value'} || $opt{'value'};
+my $contactnum = $curr_value ? $curr_value : '0';
 
 my $onchange = '';
 if ( $opt{'onchange'} ) {
@@ -205,4 +191,19 @@ $label{'comment'} = 'Comment';
 
 my @fields = $opt{'name_only'} ? qw( first last ) : keys %label;
 
+my $js = qq(
+    add_password_validation('changepw$id\_password', 'submit', '', '$contactnum');
+
+    var selfService = document.getElementById("$id\_selfservice_access").value;
+
+    if (selfService !== "Y") { document.getElementById("changepw$id\_password").disabled = 'true'; }
+    document.getElementById("$id\_selfservice_access").onchange = function() {
+      if (this.value == "P" || this.value == "E" || this.value =="Y") {
+        document.getElementById("changepw$id\_password").disabled = '';
+      }
+      else { document.getElementById("changepw$id\_password").disabled = 'true'; }
+      return false;
+    }
+);
+
 </%init>
index c6b10e3..6df45fb 100644 (file)
@@ -4,3 +4,4 @@
 % } else {
 <& header-full.html, @_ &>
 % }
+<& /misc/edge_browser_check-header.html &>
index eb065b6..cae0cdb 100644 (file)
@@ -418,7 +418,9 @@ if( $curuser->access_right('Financial reports') ) {
 
   $report_financial{'Customer Accounting Summary'} = [ $fsurl.'search/report_customer_accounting_summary.html', 'Customer accounting summary report' ];
 
-  $report_financial{'Upcoming Auto-Bill Transactions'} = [ $fsurl.'search/report_future_autobill.html', 'Upcoming auto-bill transactions' ];
+  if ( my $report_title = FS::cust_payby->future_autobill_report_title ) {
+    $report_financial{$report_title} = [ $fsurl.'search/report_future_autobill.html', "$report_title for customers with automatic payment methods (by date)" ];
+  }
 
 } elsif($curuser->access_right('Receivables report')) {
 
index 73c0db2..6aada2f 100644 (file)
@@ -14,59 +14,10 @@ should be the input id plus '_result'.
 
 </%doc>
 
-<& '/elements/xmlhttp.html',
-    'url'  => $p.'misc/xmlhttp-validate_password.html',
-    'subs' => [ 'validate_password' ],
-    'method' => 'POST', # important not to put passwords in url
-&>
-<SCRIPT>
-function add_password_validation (fieldid, submitid) {
-  var inputfield = document.getElementById(fieldid);
-  inputfield.onkeydown = function(e) {
-    var key;
-    if (window.event) { key = window.event.keyCode; }
-    else { key = e.which; } // for ff browsers
-    // some browsers allow the enter key to submit a form even if the submit button is disabled
-    // below prevents enter key from submiting form if password has not been validated.
-    if (key == '13') {
-      var check = checkPasswordValidation(fieldid);
-      return check;
-    }
-  }
-  inputfield.onkeyup = function () {
-    var fieldid = this.id+'_result';
-    var resultfield = document.getElementById(fieldid);
-    if (this.value) {
-      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
-      validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','contactnum','<% $opt{'contactnum'} %>','password',this.value,
-        function (result) {
-          result = JSON.parse(result);
-          var resultfield = document.getElementById(result.fieldid);
-          if (resultfield) {
-            var errorimg = '<IMG SRC="<% $p %>images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
-            var validimg = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
-            if (result.valid) {
-              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
-              if (submitid){ document.getElementById(submitid).disabled = false; }
-            } else if (result.error) {
-              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
-              if (submitid){ document.getElementById(submitid).disabled = true; }
-            } else {
-              result.syserror = result.syserror || 'Server error';
-              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
-              if (submitid){ document.getElementById(submitid).disabled = true; }
-            }
-          }
-        }
-      );
-    } else {
-      resultfield.innerHTML = '';
-      if (submitid){ document.getElementById(submitid).disabled = false; }
-    }
-  };
-}
+<& '/elements/validate_password_js.html', %opt &>
 
-add_password_validation('<% $opt{'fieldid'} %>', '<% $opt{'submitid'} %>');
+<SCRIPT>
+  add_password_validation('<% $opt{'fieldid'} %>', '<% $opt{'submitid'} %>', '<% $opt{'svcnum'} %>', '<% $opt{'contactnum'} %>');
 </SCRIPT>
 
 <%init>
diff --git a/httemplate/elements/validate_password_js.html b/httemplate/elements/validate_password_js.html
new file mode 100644 (file)
index 0000000..64db0a9
--- /dev/null
@@ -0,0 +1,71 @@
+<%doc>
+
+JavaScript to perform password validation
+
+  <& '/elements/validate_password_js.html', 
+     contactnum  => $contactnum,
+     svcnum      => $svcnum
+  &>
+
+The ID of the input field can be anything;  the ID of the DIV in which to display results
+should be the input id plus '_result'.
+
+</%doc>
+
+<& '/elements/xmlhttp.html',
+    'url'  => $p.'misc/xmlhttp-validate_password.html',
+    'subs' => [ 'validate_password' ],
+    'method' => 'POST', # important not to put passwords in url
+&>
+<SCRIPT>
+function add_password_validation (fieldid, submitid, svcnum, contactnum) {
+  var inputfield = document.getElementById(fieldid);
+  inputfield.onkeydown = function(e) {
+    var key;
+    if (window.event) { key = window.event.keyCode; }
+    else { key = e.which; } // for ff browsers
+    // some browsers allow the enter key to submit a form even if the submit button is disabled
+    // below prevents enter key from submiting form if password has not been validated.
+    if (key == '13') {
+      var check = checkPasswordValidation(fieldid);
+      return check;
+    }
+  }
+  inputfield.onkeyup = function () {
+    var fieldid = this.id+'_result';
+    var resultfield = document.getElementById(fieldid);
+    if (this.value) {
+      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      validate_password('fieldid',fieldid,'svcnum','<% $opt{'svcnum'} %>','contactnum', contactnum,'password',this.value,
+        function (result) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(result.fieldid);
+          if (resultfield) {
+            var errorimg = '<IMG SRC="<% $p %>images/error.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            var validimg = '<IMG SRC="<% $p %>images/tick.png" style="width: 1em; display: inline-block; padding-right: .5em">';
+            if (result.valid) {
+              resultfield.innerHTML = validimg+'<SPAN STYLE="color: green;">Password valid!</SPAN>';
+              if (submitid){ document.getElementById(submitid).disabled = false; }
+            } else if (result.error) {
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+              if (submitid){ document.getElementById(submitid).disabled = true; }
+            } else {
+              result.syserror = result.syserror || 'Server error';
+              resultfield.innerHTML = errorimg+'<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+              if (submitid){ document.getElementById(submitid).disabled = true; }
+            }
+          }
+        }
+      );
+    } else {
+      resultfield.innerHTML = '';
+      if (submitid){ document.getElementById(submitid).disabled = false; }
+    }
+  };
+}
+
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+</%init>
\ No newline at end of file
diff --git a/httemplate/misc/edge_browser_check-fail_notice.html b/httemplate/misc/edge_browser_check-fail_notice.html
new file mode 100644 (file)
index 0000000..fb42ffe
--- /dev/null
@@ -0,0 +1,25 @@
+<& /elements/header.html, "Edge browser bug" &>
+
+<div id="edgebug" style="border: solid 1px #888; border-radius: 4px; margin: 5em; max-width: 400px; text-align: left; padding: 0 1em; background-color: #ffe; box-shadow: 2px 2px 4px">
+  <div style="text-align: center; font-size: 3em; color: #933; text-shadow: 1px 1px 2px black;">
+    &#9888;
+  </div>
+  <h4 style="border-bottom: solid 1px #888; margin: 1em 0; text-align: center;">
+    Edge Browser Bug
+  </h4>
+  <p>
+    Your copy of Microsoft Edge has a data corrupting bug.
+  </p>
+  <p>
+    Microsoft fixed this bug with the <b>July RS4 Windows 10 Update</b>.
+    Please update your copy of Windows.
+  </p>
+  <p>
+    Alternatively, you may choose to use
+    <a href="https://mozilla.org/en-US/firefox/new/">Mozilla Firefox</a>
+    or <a href="https://chrome.google.com">Google Chrome</a>. They
+    are not affected by this bug.
+  </p>
+</div>
+
+<& /elements/footer.html &>
\ No newline at end of file
diff --git a/httemplate/misc/edge_browser_check-header.html b/httemplate/misc/edge_browser_check-header.html
new file mode 100644 (file)
index 0000000..a88962b
--- /dev/null
@@ -0,0 +1,36 @@
+% if ( $force_redirect ) {
+  <script type="text/javascript">
+    if ( <% $DEBUG %> || /Edge\/17\.17134/.test( navigator.userAgent )) {
+      if ( window.location.href.indexOf("fail_notice") == -1 ) {
+        window.location.href = "<% $fsurl %>misc/edge_browser_check-fail_notice.html";
+      }
+    }
+  </script>
+% } elsif ( $do_check ) {
+  <iframe id="edge_browser_check_iframe" style="display:none;"></iframe>
+  <script type="text/javascript">
+    if ( <% $DEBUG %> || /Edge\/17\.17134/.test( navigator.userAgent )) {
+      $("#edge_browser_check_iframe").attr(
+        'src',
+        '<% $fsurl %>misc/edge_browser_check-iframe.html?edge_browser_check=1'
+      );
+    }
+  </script>
+% }
+<%init>
+my $curuser    = $FS::CurrentUser::CurrentUser;
+my $session    = $FS::CurrentUser::CurrentSession;
+my $sessionkey = $session->sessionkey if $session;
+
+my $cgi = FS::UID::cgi();
+my $DEBUG = 0;
+
+my $do_check = 0;
+$do_check = 1
+  if $curuser
+  && !$cgi->param('edge_browser_check')
+  && $sessionkey
+  && $curuser->get_pref('edge_bug_vulnerable') ne $sessionkey;
+
+my $force_redirect = $curuser->get_pref('edge_bug_vulnerable') eq 'Y' ? 1 : 0;
+</%init>
diff --git a/httemplate/misc/edge_browser_check-iframe.html b/httemplate/misc/edge_browser_check-iframe.html
new file mode 100644 (file)
index 0000000..61ae9a0
--- /dev/null
@@ -0,0 +1,34 @@
+<form id="canary-form" action="<% $fsurl %>misc/edge_browser_check-iframe.html" method="POST">
+<input type="text" id="canary-result" value="<% scalar $cgi->param('edge_browser_canary') %>">
+<select name="edge_browser_canary">
+  <option>test
+  <option>test
+</select>
+<input id="canary-submit" type="submit">
+</form>
+
+<script type="text/javascript" src="<% $fsurl %>elements/jquery.js"></script>
+<script type="text/javascript">
+  $( function() {
+    if ( ! $("#canary-result").val() ) {
+      $("#canary-form").submit();
+    }
+  });
+</script>
+
+<%init>
+my $cgi = FS::UID::cgi();
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $session = $FS::CurrentUser::CurrentSession;
+my $sessionkey = $session->sessionkey if $session;
+
+if ( $curuser ) {
+  my $canary = $cgi->param('edge_browser_canary');
+  $curuser->set_pref(
+    'edge_bug_vulnerable',
+
+    $canary eq 'test' ? $sessionkey : 'Y',
+  );
+}
+
+</%init>
\ No newline at end of file
index a3e0601..37ad6d9 100644 (file)
@@ -18,7 +18,7 @@
         <% $cgi->redirect($fsurl.'view/svc_acct.cgi?'.$cgi->query_string) %>
 %   }
 %   elsif ($contactnum) { 
-        <% $cgi->redirect($fsurl.'edit/cust_main-contacts.html?'.$cgi->param('custnum')) %>
+        <% $cgi->redirect($fsurl.'view/cust_main.cgi?'.$cgi->param('custnum')) %>
 %   }
 % }
 
@@ -34,6 +34,10 @@ my $curuser = $FS::CurrentUser::CurrentUser;
 $cgi->param('svcnum') =~ /^(\d+)$/ or die "illegal svcnum" if $cgi->param('svcnum');
 my $svcnum = $1;
 
+foreach my $prefix (grep /^(.*)(password)$/, $cgi->param) {
+     $cgi->param('password' => $cgi->param($prefix));
+}
+
 $cgi->param('contactnum') =~ /^(\d+)$/ or die "illegal contactnum" if $cgi->param('contactnum');
 my $contactnum = $1;
 
index 711a25f..d4ad8e5 100644 (file)
@@ -2,20 +2,18 @@
 
 Report listing upcoming auto-bill transactions
 
-Spec requested the ability to run this report with a longer date range,
-and see which charges will process on which day.  Checkbox multiple_billing_dates
-enables this functionality.
+For every customer with a valid auto-bill payment method,
+report runs bill_and_collect() for each customer, for each
+day, from today through the report target date.  After
+recording the results, all operations are rolled back.
 
-Performance:
-This is a dynamically generated report.  The time this report takes to run
-will depends on the number of customers.  Installations with a high number
-of auto-bill customers may find themselves unable to run this report
-because of browser timeout.  Report could be implemented as a queued job if
-necessary, to solve the performance problem.
+This report relies on the ability to safely run bill_and_collect(),
+with all exports and messaging disabled, and then to roll back the
+results.
 
 </%doc>
 <& elements/grid-report.html,
-  title => 'Upcoming auto-bill transactions',
+  title => $report_title,
   rows => \@rows,
   cells => \@cells,
   table_width => "",
@@ -32,11 +30,16 @@ necessary, to solve the performance problem.
 &>
 
 <%init>
+  use FS::UID qw( dbh );
 
-use FS::UID qw( dbh myconnect );
+  die "access denied"
+    unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
 
-die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
+  my $DEBUG = $cgi->param('DEBUG') || 0;
+
+  my $report_title = FS::cust_payby->future_autobill_report_title;
+  my $agentnum = $cgi->param('agentnum')
+    if $cgi->param('agentnum') =~ /^\d+/;
 
   my $target_dt;
   my @target_dates;
@@ -45,14 +48,13 @@ die "access denied"
   my %noon = (
     hour   => 12,
     minute => 0,
-    second => 0
+    second => 0,
   );
-
   my $now_dt = DateTime->now;
   $now_dt = DateTime->new(
-    month => $now_dt->month,
-    day   => $now_dt->day,
-    year  => $now_dt->year,
+    month  => $now_dt->month,
+    day    => $now_dt->day,
+    year   => $now_dt->year,
     %noon,
   );
 
@@ -60,9 +62,9 @@ die "access denied"
   if ($cgi->param('target_date')) {
     my ($mm, $dd, $yy) = split /[\-\/]/,$cgi->param('target_date');
     $target_dt = DateTime->new(
-      month => $mm,
-      day   => $dd,
-      year  => $yy,
+      month  => $mm,
+      day    => $dd,
+      year   => $yy,
       %noon,
     ) if $mm && $dd & $yy;
 
@@ -72,18 +74,12 @@ die "access denied"
 
   # without a target date, default to tomorrow
   unless ($target_dt) {
-    $target_dt = DateTime->from_epoch( epoch => time() + 86400) ;
-    $target_dt = DateTime->new(
-      month => $target_dt->month,
-      day   => $target_dt->day,
-      year  => $target_dt->year,
-      %noon
-    );
+    $target_dt = $now_dt->clone->add( days => 1 );
   }
 
-  # If multiple_billing_dates checkbox selected, create a range of dates
-  # from today until the given report date.  Otherwise, use target date only.
-  if ($cgi->param('multiple_billing_dates')) {
+  # Create a range of dates from today until the given report date
+  #   (leaving the probably useless 'quick-report' mode, but disabled)
+  if ( 1 || $cgi->param('multiple_billing_dates')) {
     my $walking_dt = DateTime->from_epoch(epoch => $now_dt->epoch);
     until ($walking_dt->epoch > $target_dt->epoch) {
      push @target_dates, $walking_dt->epoch;
@@ -93,80 +89,128 @@ die "access denied"
     push @target_dates, $target_dt->epoch;
   }
 
-  # List all customers with an auto-bill method
-  #
-  # my %cust_payby = map {$_->custnum => $_} qsearch({
-  #   table => 'cust_payby',
-  #   hashref => {
-  #     weight  => { op => '>', value => '0' },
-  #     paydate => { op => '>', value => $target_dt->ymd },
-  #   },
-  #   order_by => " ORDER BY weight DESC ",
-  # });
-
   # List all customers with an auto-bill method that's not expired
   my %cust_payby = map {$_->custnum => $_} qsearch({
-    table => 'cust_payby',
-    hashref => {
-      weight  => { op => '>', value => '0' },
-    },
-    order_by => " ORDER BY weight DESC ",
-    extra_sql => " AND ( payby = 'CHEK' OR ( paydate > '".$target_dt->ymd."')) ",
+    table     => 'cust_payby',
+    addl_from => 'JOIN cust_main USING (custnum)',
+    hashref   => {  weight  => { op => '>', value => '0' }},
+    order_by  => " ORDER BY weight DESC ",
+    extra_sql =>
+      "AND (
+        payby IN ('CHEK','DCHK','DCHEK')
+        OR ( paydate > '".$target_dt->ymd."')
+      )
+      AND " . $FS::CurrentUser::CurrentUser->agentnums_sql
+      . ($agentnum ? "AND cust_main.agentnum = $agentnum" : ''),
   });
 
+  my $fakebill_time = time();
   my %abreport;
   my @rows;
 
   local $@;
   local $SIG{__DIE__};
-  my $temp_dbh = myconnect();
-  eval { # Creating sandbox dbh where all connections are to be rolled back
-    local $FS::UID::dbh = $temp_dbh;
+
+  eval { # Sandbox
+
+    # Supress COMMIT statements
+    my $oldAutoCommit = $FS::UID::AutoCommit;
     local $FS::UID::AutoCommit = 0;
+    local $FS::UID::ForceObeyAutoCommit = 1;
+
+    # Suppress notices generated by billing events
+    local $FS::Misc::DISABLE_ALL_NOTICES = 1;
+
+    # Bypass payment processing, recording a fake payment
+    local $FS::cust_main::Billing_Realtime::BOP_TESTING = 1;
+    local $FS::cust_main::Billing_Realtime::BOP_TESTING_SUCCESS = 1;
 
-    # Generate report data into @rows
+    warn sprintf "Report involves %s customers", scalar keys %cust_payby
+      if $DEBUG;
+
+    # Run bill_and_collect(), for each customer with an autobill payment method,
+    # for each day represented in the report
     for my $custnum (keys %cust_payby) {
       my $cust_main = qsearchs('cust_main', {custnum => $custnum});
 
+      warn "-- Processing custnum $custnum\n"
+        if $DEBUG;
+
       # walk forward through billing dates
       for my $query_epoch (@target_dates) {
+        $FS::cust_main::Billing_Realtime::BOP_TESTING_TIMESTAMP = $query_epoch;
         my $return_bill = [];
 
-        eval { # Don't let an error on one customer crash the report
-          my $error = $cust_main->bill(
-            time           => $query_epoch,
-            return_bill    => $return_bill,
-            no_usage_reset => 1,
-          );
-          die "$error (simulating future billing)" if $error;
-        };
-        warn ("$@: (future_autobill custnum:$custnum)");
-
-        if (@{$return_bill}) {
-          my $inv = $return_bill->[0];
-          push @rows,{
-            name => $cust_main->name,
-            _date => $inv->_date,
-            cells => [
-              { class => 'gridreport', value => $custnum },
-              { class => 'gridreport',
-                value => '<a href="/view/cust_main.cgi?"'.$custnum.'">'.$cust_main->name.'</a>',
-                bypass_filter => 1,
-              },
-              { class => 'gridreport', value => $inv->charged, format => 'money' },
-              { class => 'gridreport', value => DateTime->from_epoch(epoch=>$inv->_date)->ymd },
-              { class => 'gridreport', value => ($cust_payby{$custnum}->payby || $cust_payby{$custnum}->paytype) },
-              { class => 'gridreport', value => $cust_payby{$custnum}->paymask },
-            ]
-          };
-        }
+        warn "---- Set billtime to ".
+             DateTime->from_epoch( epoch => $query_epoch )."\n"
+                if $DEBUG;
+
+        my $error = $cust_main->bill_and_collect(
+          time           => $query_epoch,
+          return_bill    => $return_bill,
+          no_usage_reset => 1,
+          fake           => 1,
+        );
 
+        warn "!!! $error (simulating future billing)\n" if $error;
       }
-      $temp_dbh->rollback;
-    } # /foreach $custnum
 
+      # Generate report rows from recorded payments in cust_pay
+      for my $cust_pay (
+        qsearch( cust_pay => {
+          custnum => $custnum,
+          _date   => { op => '>=', value => $fakebill_time },
+        })
+      ) {
+        push @rows,{
+          name  => $cust_main->name,
+          _date => $cust_pay->_date,
+          cells => [
+
+            # Customer number
+            { class => 'gridreport', value => $custnum },
+
+            # Customer name / customer link
+            { class => 'gridreport',
+              value =>  qq{<a href="${fsurl}view/cust_main.cgi?${custnum}">} . encode_entities( $cust_main->name ). '</a>',
+              bypass_filter => 1
+            },
+
+            # Amount
+            { class => 'gridreport',
+              value => $cust_pay->paid,
+              format => 'money'
+            },
+
+            # Transaction Date
+            { class => 'gridreport',
+              value => DateTime->from_epoch( epoch => $cust_pay->_date )->ymd
+            },
+
+            # Payment Method
+            { class => 'gridreport',
+              value => encode_entities( $cust_pay->paycardtype || $cust_pay->payby ),
+            },
+
+            # Masked Payment Instrument
+            { class => 'gridreport',
+              value => encode_entities( $cust_pay->paymask ),
+            },
+          ]
+        };
+
+      } # /foreach payment
+
+      # Roll back database at the end of each customer
+      # Makes the report slighly slower, but ensures only one customer row
+      #   locked at a time
+
+      warn "-- custnum $custnum -- rollback()\n" if $DEBUG;
+      dbh->rollback if $oldAutoCommit;
+
+    } # /foreach $custnum
   }; # /eval
-  warn("$@") if $@;
+  warn("future_autobill.html report generated error $@") if $@;
 
   # Sort output by date, and format for output to grid-report.html
   my @cells = [
index 1a0c9f4..ccde299 100644 (file)
@@ -3,40 +3,55 @@
 Display date selector for the future_autobill.html report
 
 </%doc>
-<% include('/elements/header.html', 'Future Auto-Bill Transactions' ) %>
+<% include('/elements/header.html', $report_title ) %>
 
 
-<FORM ACTION="future_autobill.html" METHOD="GET">
-<TABLE>
-<& /elements/tr-input-date-field.html,
-  {
-    name     => 'target_date',
-    value    => $target_date,
-    label    => emt('Target billing date').': ',
-    required => 1
-  }
-&>
+% if ( FS::TaxEngine->new->info->{batch} ) {
 
-<& /elements/tr-checkbox.html,
-     'label' => emt('Multiple billing dates (slow)').': ',
-     'field' => 'multiple_billing_dates',
-     'value' => '1',
-&>
+  <div style="font-color: red">
+  NOTE: This report is disabled due to tax engine configuration
+  </div>
 
-</TABLE>
+% } else {
 
-<BR>
-<INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+  <FORM ACTION="future_autobill.html" METHOD="GET">
+  <TABLE>
+  <& /elements/tr-input-date-field.html,
+    {
+      name     => 'target_date',
+      value    => $target_date,
+      label    => emt('Target billing date').': ',
+      required => 1
+    }
+  &>
 
-</FORM>
+  <% include('/elements/tr-select-agent.html',
+              'label'         => 'For agent: ',
+              'disable_empty' => 0,
+            )
+  %>
+
+  </TABLE>
+
+  <BR>
+
+  <INPUT TYPE="submit" VALUE="<% mt('Get Report') |h %>">
+
+  </FORM>
+
+% }
 
 <% include('/elements/footer.html') %>
 
 <%init>
+use FS::cust_payby;
+use FS::CurrentUser;
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
 
-my $target_date = DateTime->from_epoch(epoch=>(time()+86400))->mdy('/');
+my $target_date = DateTime->now->add(days => 1)->mdy('/');
+my $report_title = FS::cust_payby->future_autobill_report_title;
 
 </%init>
+
index fe412cc..9252b21 100644 (file)
@@ -22,6 +22,7 @@
 %   my $bgcolor1 = '#ffffff';
 %   my $bgcolor2 = '#eeeeee';
 %   my $bgcolor = $bgcolor2;
+%   my $count = 0;
 %   foreach my $cust_contact ( @cust_contacts ) {
 %     my $contact = $cust_contact->contact;
 %     my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
             Enabled
 %#            <FONT SIZE="-1"><A HREF="XXX">disable</A>
 %#                            <A HREF="XXX">re-email</A></FONT>
+            <FONT SIZE="-1">
+              <& /elements/change_password.html,
+                'contact_num'      => $cust_contact->contactnum,
+                'custnum'          => $cust_contact->custnum,
+                'no_label_display' => '',
+                'label'            => 'change password',
+                'curr_value'       => '',
+                'pre_pwd_field_label' => 'contact'.$count.'_',
+              &>
+            </FONT>
 %         } else {
             Disabled
 %#            <FONT SIZE="-1"><A HREF="XXX">enable</A></FONT>
@@ -63,6 +74,7 @@
 %      } else {
 %        $bgcolor = $bgcolor1;
 %      }
+%     $count++;
 %   }
 </TABLE>
 %}
@@ -80,6 +92,6 @@ my @cust_contacts = $cust_main->cust_contact;
 
 # residential customers have a default "invisible" contact, but if they
 # somehow get more than one contact, show them
-my $display = scalar(@cust_contacts) > 1;
+my $display = scalar(@cust_contacts) > 0;
 
 </%init>
index c7e20c5..25ec334 100644 (file)
@@ -10,7 +10,7 @@
     <TABLE BORDER=0 CELLSPACING=2 CELLPADDING=0>
       <TR>
         <TD>
-          Sorry we were unable to locate your account with ip <? echo $username; ?>  .
+          Sorry we were unable to locate your account with MAC address <? echo $username; ?>  .
         </TD>
       </TR>
     </TABLE>
index 91e19cd..b4e2b26 100644 (file)
@@ -4,16 +4,14 @@ require('freeside.class.php');
 $freeside = new FreesideSelfService();
 
 $ip = $_SERVER['REMOTE_ADDR'];
-# need a routine here to get mac address from radius account table based on ip address.  Every else should be good to go.
-$mac_addr = '1234567890FF';
+
+$mac_addr = $freeside->get_mac_address( array('ip' => $ip, ) );
 
 $response = $freeside->login( array( 
-  'username' => $mac_addr
+  'username' => $mac_addr['mac_address'],
   'domain'   => 'ip_mac',
 ) );
 
-#error_log("[login] received response from freeside: $response");
-
 $error = $response['error'];
 
 if ( $error ) {