backporting, #72101
[freeside.git] / FS / FS / access_user.pm
index 8cc8b64..366ae7e 100644 (file)
@@ -1,18 +1,17 @@
 package FS::access_user;
 
 use strict;
 package FS::access_user;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $htpasswd_file );
+use base qw( FS::m2m_Common FS::option_Common ); 
+use vars qw( $DEBUG $me $conf $htpasswd_file );
 use FS::UID;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::UID;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh );
-use FS::m2m_Common;
-use FS::option_Common;
 use FS::access_user_pref;
 use FS::access_usergroup;
 use FS::agent;
 use FS::access_user_pref;
 use FS::access_usergroup;
 use FS::agent;
-
-@ISA = qw( FS::m2m_Common FS::option_Common FS::Record );
-#@ISA = qw( FS::m2m_Common FS::option_Common );
+use FS::cust_main;
+use FS::sales;
+use FS::sched_item;
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
 
 $DEBUG = 0;
 $me = '[FS::access_user]';
@@ -135,9 +134,8 @@ sub insert {
 
 sub htpasswd_kludge {
   my $self = shift;
 
 sub htpasswd_kludge {
   my $self = shift;
-  
-  #awful kludge to skip setting htpasswd for fs_* users
-  return '' if $self->username =~ /^fs_/;
+
+  return '' if $self->is_system_user;
 
   unshift @_, '-c' unless -e $htpasswd_file;
   if ( 
 
   unshift @_, '-c' unless -e $htpasswd_file;
   if ( 
@@ -175,7 +173,8 @@ sub delete {
   my $dbh = dbh;
 
   my $error =
   my $dbh = dbh;
 
   my $error =
-       $self->SUPER::delete(@_)
+       $self->delete_password_history
+    || $self->SUPER::delete(@_)
     || $self->htpasswd_kludge('-D')
   ;
 
     || $self->htpasswd_kludge('-D')
   ;
 
@@ -220,6 +219,9 @@ sub replace {
       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
       return $error;
     }
       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
       return $error;
     }
+  } elsif ( $old->disabled && !$new->disabled
+              && $new->_password =~ /changeme/i ) {
+    return "Must change password when enabling this account";
   }
 
   my $error = $new->SUPER::replace($old, @_);
   }
 
   my $error = $new->SUPER::replace($old, @_);
@@ -254,6 +256,8 @@ sub check {
     || $self->ut_text('_password')
     || $self->ut_text('last')
     || $self->ut_text('first')
     || $self->ut_text('_password')
     || $self->ut_text('last')
     || $self->ut_text('first')
+    || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
+    || $self->ut_foreign_keyn('report_salesnum', 'sales', 'salesnum')
     || $self->ut_enum('disabled', [ '', 'Y' ] )
   ;
   return $error if $error;
     || $self->ut_enum('disabled', [ '', 'Y' ] )
   ;
   return $error if $error;
@@ -269,7 +273,33 @@ Returns a name string for this user: "Last, First".
 
 sub name {
   my $self = shift;
 
 sub name {
   my $self = shift;
-  $self->get('last'). ', '. $self->first;
+  return $self->username
+    if $self->get('last') eq 'Lastname' && $self->first eq 'Firstname';
+  return $self->get('last'). ', '. $self->first;
+}
+
+=item user_cust_main
+
+Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
+user.
+
+=cut
+
+sub user_cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->user_custnum } );
+}
+
+=item report_sales
+
+Returns the FS::sales object (see L<FS::sales>), if any, for this
+user.
+
+=cut
+
+sub report_sales {
+  my $self = shift;
+  qsearchs( 'sales', { 'salesnum' => $self->report_salesnum } );
 }
 
 =item access_usergroup
 }
 
 =item access_usergroup
@@ -300,6 +330,22 @@ sub access_usergroup {
 #
 #}
 
 #
 #}
 
+=item num_agents
+
+Returns the number of agents this user can view (via group membership).
+
+=cut
+
+sub num_agents {
+  my $self = shift;
+  $self->scalar_sql(
+    'SELECT COUNT(DISTINCT agentnum) FROM access_usergroup
+                                     JOIN access_groupagent USING ( groupnum )
+       WHERE usernum = ?',
+    $self->usernum,
+  );
+}
+
 =item agentnums 
 
 Returns a list of agentnums this user can view (via group membership).
 =item agentnums 
 
 Returns a list of agentnums this user can view (via group membership).
@@ -350,6 +396,11 @@ user has the provided access right
 Optional table name in which agentnum is being checked.  Sometimes required to
 resolve 'column reference "agentnum" is ambiguous' errors.
 
 Optional table name in which agentnum is being checked.  Sometimes required to
 resolve 'column reference "agentnum" is ambiguous' errors.
 
+=item viewall_right
+
+All agents will be viewable if the current user has the provided access right.
+Defaults to 'View customers of all agents'.
+
 =back
 
 =cut
 =back
 
 =cut
@@ -360,16 +411,23 @@ sub agentnums_sql {
 
   my $agentnum = $opt{'table'} ? $opt{'table'}.'.agentnum' : 'agentnum';
 
 
   my $agentnum = $opt{'table'} ? $opt{'table'}.'.agentnum' : 'agentnum';
 
-#  my @agentnums = map { "$agentnum = $_" } $self->agentnums;
-  my @agentnums = ();
-  push @agentnums, "$agentnum IN (". join(',', $self->agentnums). ')';
+  my @or = ();
+
+  my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
+  if ( $self->access_right($viewall_right) ) {
+    push @or, "$agentnum IS NOT NULL";
+  } else {
+    my @agentnums = $self->agentnums;
+    push @or, "$agentnum IN (". join(',', @agentnums). ')'
+      if @agentnums;
+  }
 
 
-  push @agentnums, "$agentnum IS NULL"
+  push @or, "$agentnum IS NULL"
     if $opt{'null'}
     || ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) );
 
     if $opt{'null'}
     || ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) );
 
-  return ' 1 = 0 ' unless scalar(@agentnums);
-  '( '. join( ' OR ', @agentnums ). ' )';
+  return ' 1 = 0 ' unless scalar(@or);
+  '( '. join( ' OR ', @or ). ' )';
 
 }
 
 
 }
 
@@ -377,23 +435,30 @@ sub agentnums_sql {
 
 Returns true if the user can view the specified agent.
 
 
 Returns true if the user can view the specified agent.
 
+Also accepts optional hashref cache, to avoid redundant database calls.
+
 =cut
 
 sub agentnum {
 =cut
 
 sub agentnum {
-  my( $self, $agentnum ) = @_;
+  my( $self, $agentnum, $cache ) = @_;
+  $cache ||= {};
+  return $cache->{$self->usernum}->{$agentnum}
+    if $cache->{$self->usernum}->{$agentnum};
   my $sth = dbh->prepare(
     "SELECT COUNT(*) FROM access_usergroup
                      JOIN access_groupagent USING ( groupnum )
        WHERE usernum = ? AND agentnum = ?"
   ) or die dbh->errstr;
   $sth->execute($self->usernum, $agentnum) or die $sth->errstr;
   my $sth = dbh->prepare(
     "SELECT COUNT(*) FROM access_usergroup
                      JOIN access_groupagent USING ( groupnum )
        WHERE usernum = ? AND agentnum = ?"
   ) or die dbh->errstr;
   $sth->execute($self->usernum, $agentnum) or die $sth->errstr;
-  $sth->fetchrow_arrayref->[0];
+  $cache->{$self->usernum}->{$agentnum} = $sth->fetchrow_arrayref->[0];
+  $sth->finish;
+  return $cache->{$self->usernum}->{$agentnum};
 }
 
 }
 
-=item agents
+=item agents [ HASHREF | OPTION => VALUE ... ]
 
 Returns the list of agents this user can view (via group membership), as
 
 Returns the list of agents this user can view (via group membership), as
-FS::agent objects.
+FS::agent objects.  Accepts the same options as the agentnums_sql method.
 
 =cut
 
 
 =cut
 
@@ -402,10 +467,109 @@ sub agents {
   qsearch({
     'table'     => 'agent',
     'hashref'   => { disabled=>'' },
   qsearch({
     'table'     => 'agent',
     'hashref'   => { disabled=>'' },
-    'extra_sql' => ' AND '. $self->agentnums_sql,
+    'extra_sql' => ' AND '. $self->agentnums_sql(@_),
+    'order_by'  => 'ORDER BY agent',
   });
 }
 
   });
 }
 
+=item access_users [ HASHREF | OPTION => VALUE ... ]
+
+Returns an array of FS::access_user objects, one for each non-disabled 
+access_user in the system that shares an agent (via group membership) with 
+the invoking object.  Regardless of options and agents, will always at
+least return the invoking user and any users who have viewall_right.
+
+Accepts the following options:
+
+=over 4
+
+=item table
+
+Only return users who appear in the usernum field of this table
+
+=item disabled
+
+Include disabled users if true (defaults to false)
+
+=item viewall_right
+
+All users will be returned if the current user has the provided 
+access right, regardless of agents (other filters still apply.)  
+Defaults to 'View customers of all agents'
+
+=cut
+
+#Leaving undocumented until such time as this functionality is actually used
+#
+#=item null
+#
+#Users with no agents will be returned.
+#
+#=item null_right
+#
+#Users with no agents will be returned if the current user has the provided
+#access right.
+
+sub access_users {
+  my $self = shift;
+  my %opt = ref($_[0]) ? %{$_[0]} : @_;
+  my $table = $opt{'table'};
+  my $search = { 'table' => 'access_user' };
+  $search->{'hashref'} = $opt{'disabled'} ? {} : { 'disabled' => '' };
+  $search->{'addl_from'} = "INNER JOIN $table ON (access_user.usernum = $table.usernum)"
+    if $table;
+  my @access_users = qsearch($search);
+  my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
+  return @access_users if $self->access_right($viewall_right);
+  #filter for users with agents $self can view
+  my @out;
+  my $agentnum_cache = {};
+ACCESS_USER:
+  foreach my $access_user (@access_users) {
+    # you can always view yourself, regardless of agents,
+    # and you can always view someone who can view you, 
+    # since they might have affected your customers
+    if ( ($self->usernum eq $access_user->usernum) 
+         || $access_user->access_right($viewall_right)
+    ) {
+      push(@out,$access_user);
+      next;
+    }
+    # if user has no agents, you need null or null_right to view
+    my @agents = $access_user->agents('viewall_right'=>'NONE'); #handled viewall_right above
+    if (!@agents) {
+      if ( $opt{'null'} ||
+           ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) )
+      ) {
+        push(@out,$access_user);
+      }
+      next;
+    }
+    # otherwise, you need an agent in common
+    foreach my $agent (@agents) {
+      if ($self->agentnum($agent->agentnum,$agentnum_cache)) {
+        push(@out,$access_user);
+        next ACCESS_USER;
+      }
+    }
+  }
+  return @out;
+}
+
+=item access_users_hashref  [ HASHREF | OPTION => VALUE ... ]
+
+Accepts same options as L</access_users>.  Returns a hashref of
+users, with keys of usernum and values of username.
+
+=cut
+
+sub access_users_hashref {
+  my $self = shift;
+  my %access_users = map { $_->usernum => $_->username } 
+                       $self->access_users(@_);
+  return \%access_users;
+}
+
 =item access_right RIGHTNAME | LISTREF
 
 Given a right name or a list reference of right names, returns true if this
 =item access_right RIGHTNAME | LISTREF
 
 Given a right name or a list reference of right names, returns true if this
@@ -430,7 +594,7 @@ sub access_right {
     unless ( grep !exists($self->{_ACLcache}{$_}), @$rightname ) {
       warn "$me ACL cache hit for ". join(', ', @$rightname). "\n"
         if $DEBUG;
     unless ( grep !exists($self->{_ACLcache}{$_}), @$rightname ) {
       warn "$me ACL cache hit for ". join(', ', @$rightname). "\n"
         if $DEBUG;
-      return grep $self->{_ACLcache}{$_}, @$rightname
+      return scalar( grep $self->{_ACLcache}{$_}, @$rightname );
     }
 
     warn "$me ACL cache miss for ". join(', ', @$rightname). "\n"
     }
 
     warn "$me ACL cache miss for ". join(', ', @$rightname). "\n"
@@ -470,11 +634,48 @@ sub access_right {
 
 }
 
 
 }
 
+=item refund_rights PAYBY
+
+Accepts payment $payby (BILL,CASH,MCRD,MCHK,CARD,CHEK) and returns a
+list of the refund rights associated with that $payby.
+
+Returns empty list if $payby wasn't recognized.
+
+=cut
+
+sub refund_rights {
+  my $self = shift;
+  my $payby = shift;
+  my @rights = ();
+  push @rights, 'Post refund'                if $payby =~ /^(BILL|CASH|MCRD|MCHK)$/;
+  push @rights, 'Post check refund'          if $payby eq 'BILL';
+  push @rights, 'Post cash refund '          if $payby eq 'CASH';
+  push @rights, 'Refund payment'             if $payby =~ /^(CARD|CHEK)$/;
+  push @rights, 'Refund credit card payment' if $payby eq 'CARD';
+  push @rights, 'Refund Echeck payment'      if $payby eq 'CHEK';
+  return @rights;
+}
+
+=item refund_access_right PAYBY
+
+Returns true if user has L</access_right> for any L</refund_rights>
+for the specified payby.
+
+=cut
+
+sub refund_access_right {
+  my $self = shift;
+  my $payby = shift;
+  my @rights = $self->refund_rights($payby);
+  return '' unless @rights;
+  return $self->access_right(\@rights);
+}
+
 =item default_customer_view
 
 Returns the default customer view for this user, from the 
 "default_customer_view" user preference, the "cust_main-default_view" config,
 =item default_customer_view
 
 Returns the default customer view for this user, from the 
 "default_customer_view" user preference, the "cust_main-default_view" config,
-or the hardcoded default, "jumbo" (may change to "basics" in the near future).
+or the hardcoded default, "basics" (formerly "jumbo" prior to 3.0).
 
 =cut
 
 
 =cut
 
@@ -483,8 +684,158 @@ sub default_customer_view {
 
   $self->option('default_customer_view')
     || $conf->config('cust_main-default_view')
 
   $self->option('default_customer_view')
     || $conf->config('cust_main-default_view')
-    || 'jumbo'; #'basics' in 1.9.1?
+    || 'basics'; #s/jumbo/basics/ starting with 3.0
+
+}
+
+=item spreadsheet_format [ OVERRIDE ]
+
+Returns a hashref of this user's Excel spreadsheet download settings:
+'extension' (xls or xlsx), 'class' (Spreadsheet::WriteExcel or
+Excel::Writer::XLSX), and 'mime_type'.  If OVERRIDE is 'XLS' or 'XLSX',
+use that instead of the user's setting.
+
+=cut
+
+# is there a better place to put this?
+my %formats = (
+  XLS => {
+    extension => '.xls',
+    class => 'Spreadsheet::WriteExcel',
+    mime_type => 'application/vnd.ms-excel',
+  },
+  XLSX => {
+    extension => '.xlsx',
+    class => 'Excel::Writer::XLSX',
+    mime_type => # it's on wikipedia, it must be true
+      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+  }
+);
+
+sub spreadsheet_format {
+  my $self = shift;
+  my $override = shift;
+
+  my $f =  $override
+        || $self->option('spreadsheet_format') 
+        || $conf->config('spreadsheet_format')
+        || 'XLS';
+
+  $formats{$f};
+}
+
+=item is_system_user
+
+Returns true if this user has the name of a known system account.  These 
+users will not appear in the htpasswd file and can't have passwords set.
+
+=cut
+
+sub is_system_user {
+  my $self = shift;
+  return grep { $_ eq $self->username } ( qw(
+    fs_queue
+    fs_daily
+    fs_selfservice
+    fs_signup
+    fs_bootstrap
+    fs_selfserv
+    fs_api
+) );
+}
 
 
+sub sched_item {
+  my $self = shift;
+  qsearch( 'sched_item', { 'usernum' => $self->usernum } );
+}
+
+=item locale
+
+=cut
+
+sub locale {
+  my $self = shift;
+  return $self->{_locale} if exists($self->{_locale});
+  $self->{_locale} = $self->option('locale');
+}
+
+=item get_page_pref PATH, NAME, TABLENUM
+
+Returns the user's page preference named NAME for the page at PATH. If the
+page is a view or edit page or otherwise shows a single record at a time,
+it should use TABLENUM to tell which record the preference is for.
+
+=cut
+
+sub get_page_pref {
+  my $self = shift;
+  my ($path, $prefname, $tablenum) = @_;
+  $tablenum ||= '';
+  
+  my $access_user_page_pref = qsearchs('access_user_page_pref', {
+      path      => $path,
+      usernum   => $self->usernum,
+      tablenum  => $tablenum,
+      prefname  => $prefname,
+  }); 
+  $access_user_page_pref ? $access_user_page_pref->prefvalue : '';
+} 
+
+=item set_page_pref PATH, NAME, TABLENUM, VALUE
+
+Sets the user's page preference named NAME for the page at PATH. Use TABLENUM
+as for get_page_pref.
+
+=cut
+
+sub set_page_pref {
+  my $self = shift;
+  my ($path, $prefname, $tablenum, $prefvalue) = @_;
+  $tablenum ||= '';
+  
+  my $error;
+  my $access_user_page_pref = qsearchs('access_user_page_pref', {
+      path      => $path,
+      usernum   => $self->usernum,
+      tablenum  => $tablenum,
+      prefname  => $prefname,
+  });
+  if ( $access_user_page_pref ) { 
+    if ( $prefvalue eq $access_user_page_pref->get('prefvalue') ) {
+      return '';
+    }
+    if ( length($prefvalue) > 0 ) {
+      $access_user_page_pref->set('prefvalue', $prefvalue);
+      $error = $access_user_page_pref->replace;
+      $error .= " (updating $prefname)" if $error;
+    } else { 
+      $error = $access_user_page_pref->delete;
+      $error .= " (removing $prefname)" if $error;
+    }
+  } else {
+    if ( length($prefvalue) > 0 ) {
+      $access_user_page_pref = FS::access_user_page_pref->new({
+          path      => $path,
+          usernum   => $self->usernum,
+          tablenum  => $tablenum,
+          prefname  => $prefname,
+          prefvalue => $prefvalue,
+      });
+      $error = $access_user_page_pref->insert;
+      $error .= " (creating $prefname)" if $error;
+    } else { 
+      return '';
+    }
+  }
+
+  return $error;
+}
+
+#3.x
+
+sub saved_search {
+  my $self = shift;
+  qsearch('saved_search', { 'usernum' => $self->usernum });
 }
 
 =back
 }
 
 =back