add skip_dcontext_suffix to skip CDRs with dcontext ending in a definable string...
[freeside.git] / FS / FS / access_user.pm
1 package FS::access_user;
2 use base qw( FS::Password_Mixin
3              FS::m2m_Common
4              FS::option_Common ); 
5
6 use strict;
7 use vars qw( $DEBUG $me );
8 use FS::UID;
9 use FS::Auth;
10 use FS::Conf;
11 use FS::Record qw( qsearch qsearchs dbh );
12 use FS::agent;
13 use FS::cust_main;
14 use FS::sales;
15
16 $DEBUG = 0;
17 $me = '[FS::access_user]';
18
19 =head1 NAME
20
21 FS::access_user - Object methods for access_user records
22
23 =head1 SYNOPSIS
24
25   use FS::access_user;
26
27   $record = new FS::access_user \%hash;
28   $record = new FS::access_user { 'column' => 'value' };
29
30   $error = $record->insert;
31
32   $error = $new_record->replace($old_record);
33
34   $error = $record->delete;
35
36   $error = $record->check;
37
38 =head1 DESCRIPTION
39
40 An FS::access_user object represents an internal access user.  FS::access_user
41 inherits from FS::Record.  The following fields are currently supported:
42
43 =over 4
44
45 =item usernum
46
47 primary key
48
49 =item username
50
51 =item _password
52
53 =item _password_encoding
54
55 Empty or bcrypt
56
57 =item last
58
59 Last name
60
61 =item first
62
63 First name
64
65 =item user_custnum
66
67 Master customer for this employee (for commissions)
68
69 =item report_salesnum
70
71 Default sales person for this employee (for reports)
72
73 =item disabled
74
75 Empty or 'Y'
76
77 =back
78
79 =head1 METHODS
80
81 =over 4
82
83 =item new HASHREF
84
85 Creates a new internal access user.  To add the user to the database, see L<"insert">.
86
87 Note that this stores the hash reference, not a distinct copy of the hash it
88 points to.  You can ask the object for a copy with the I<hash> method.
89
90 =cut
91
92 # the new method can be inherited from FS::Record, if a table method is defined
93
94 sub table { 'access_user'; }
95
96 sub _option_table    { 'access_user_pref'; }
97 sub _option_namecol  { 'prefname'; }
98 sub _option_valuecol { 'prefvalue'; }
99
100 =item insert
101
102 Adds this record to the database.  If there is an error, returns the error,
103 otherwise returns false.
104
105 =cut
106
107 sub insert {
108   my $self = shift;
109
110   my $error = $self->check;
111   return $error if $error;
112
113   local $SIG{HUP} = 'IGNORE';
114   local $SIG{INT} = 'IGNORE';
115   local $SIG{QUIT} = 'IGNORE';
116   local $SIG{TERM} = 'IGNORE';
117   local $SIG{TSTP} = 'IGNORE';
118   local $SIG{PIPE} = 'IGNORE';
119
120   my $oldAutoCommit = $FS::UID::AutoCommit;
121   local $FS::UID::AutoCommit = 0;
122   my $dbh = dbh;
123
124   if ( $error ) {
125     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
126     return $error;
127   }
128
129   $error = $self->SUPER::insert(@_);
130   if ( $self->_password ) {
131     $error ||= $self->insert_password_history;
132   }
133
134   if ( $error ) {
135     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
136     return $error;
137   } else {
138     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
139     '';
140   }
141
142 }
143
144 =item delete
145
146 Delete this record from the database.
147
148 =cut
149
150 sub delete {
151   my $self = shift;
152
153   local $SIG{HUP} = 'IGNORE';
154   local $SIG{INT} = 'IGNORE';
155   local $SIG{QUIT} = 'IGNORE';
156   local $SIG{TERM} = 'IGNORE';
157   local $SIG{TSTP} = 'IGNORE';
158   local $SIG{PIPE} = 'IGNORE';
159
160   my $oldAutoCommit = $FS::UID::AutoCommit;
161   local $FS::UID::AutoCommit = 0;
162   my $dbh = dbh;
163
164   my $error = $self->delete_password_history
165            || $self->SUPER::delete(@_);
166
167   if ( $error ) {
168     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
169     return $error;
170   } else {
171     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
172     '';
173   }
174
175 }
176
177 =item replace OLD_RECORD
178
179 Replaces the OLD_RECORD with this one in the database.  If there is an error,
180 returns the error, otherwise returns false.
181
182 =cut
183
184 sub replace {
185   my $new = shift;
186
187   my $old = ( ref($_[0]) eq ref($new) )
188               ? shift
189               : $new->replace_old;
190
191   local $SIG{HUP} = 'IGNORE';
192   local $SIG{INT} = 'IGNORE';
193   local $SIG{QUIT} = 'IGNORE';
194   local $SIG{TERM} = 'IGNORE';
195   local $SIG{TSTP} = 'IGNORE';
196   local $SIG{PIPE} = 'IGNORE';
197
198   my $oldAutoCommit = $FS::UID::AutoCommit;
199   local $FS::UID::AutoCommit = 0;
200   my $dbh = dbh;
201
202   return "Must change password when enabling this account"
203     if $old->disabled && !$new->disabled
204     && (      $new->_password =~ /changeme/i
205            || $new->_password eq 'notyet'
206        );
207
208   my $error = $new->SUPER::replace($old, @_);
209   if ( $old->_password ne $new->_password ) {
210     $error ||= $new->insert_password_history;
211   }
212
213   if ( $error ) {
214     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
215     return $error;
216   } else {
217     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218     '';
219   }
220
221 }
222
223 =item check
224
225 Checks all fields to make sure this is a valid internal access user.  If there is
226 an error, returns the error, otherwise returns false.  Called by the insert
227 and replace methods.
228
229 =cut
230
231 # the check method should currently be supplied - FS::Record contains some
232 # data checking routines
233
234 sub check {
235   my $self = shift;
236
237   my $error = 
238     $self->ut_numbern('usernum')
239     || $self->ut_alpha_lower('username')
240     || $self->ut_textn('_password')
241     || $self->ut_textn('last')
242     || $self->ut_textn('first')
243     || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
244     || $self->ut_foreign_keyn('report_salesnum', 'sales', 'salesnum')
245     || $self->ut_enum('disabled', [ '', 'Y' ] )
246   ;
247   return $error if $error;
248
249   $self->SUPER::check;
250 }
251
252 =item name
253
254 Returns a name string for this user: "Last, First".
255
256 =cut
257
258 sub name {
259   my $self = shift;
260   return $self->username
261     if $self->get('last') eq 'Lastname' && $self->first eq 'Firstname'
262     or $self->get('last') eq ''         && $self->first eq '';
263   return $self->get('last'). ', '. $self->first;
264 }
265
266 =item user_cust_main
267
268 Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
269 user.
270
271 =cut
272
273 sub user_cust_main {
274   my $self = shift;
275   qsearchs( 'cust_main', { 'custnum' => $self->user_custnum } );
276 }
277
278 =item report_sales
279
280 Returns the FS::sales object (see L<FS::sales>), if any, for this
281 user.
282
283 =cut
284
285 sub report_sales {
286   my $self = shift;
287   qsearchs( 'sales', { 'salesnum' => $self->report_salesnum } );
288 }
289
290 =item access_usergroup
291
292 Returns links to the the groups this user is a part of, as FS::access_usergroup
293 objects (see L<FS::access_usergroup>).
294
295 =item num_agents
296
297 Returns the number of agents this user can view (via group membership).
298
299 =cut
300
301 sub num_agents {
302   my $self = shift;
303   $self->scalar_sql(
304     'SELECT COUNT(DISTINCT agentnum) FROM access_usergroup
305                                      JOIN access_groupagent USING ( groupnum )
306        WHERE usernum = ?',
307     $self->usernum,
308   );
309 }
310
311 =item agentnums 
312
313 Returns a list of agentnums this user can view (via group membership).
314
315 =cut
316
317 sub agentnums {
318   my $self = shift;
319   my $sth = dbh->prepare(
320     "SELECT DISTINCT agentnum FROM access_usergroup
321                               JOIN access_groupagent USING ( groupnum )
322        WHERE usernum = ?"
323   ) or die dbh->errstr;
324   $sth->execute($self->usernum) or die $sth->errstr;
325   map { $_->[0] } @{ $sth->fetchall_arrayref };
326 }
327
328 =item agentnums_href
329
330 Returns a hashref of agentnums this user can view.
331
332 =cut
333
334 sub agentnums_href {
335   my $self = shift;
336   scalar( { map { $_ => 1 } $self->agentnums } );
337 }
338
339 =item agentnums_sql [ HASHREF | OPTION => VALUE ... ]
340
341 Returns an sql fragement to select only agentnums this user can view.
342
343 Options are passed as a hashref or a list.  Available options are:
344
345 =over 4
346
347 =item null
348
349 The frament will also allow the selection of null agentnums.
350
351 =item null_right
352
353 The fragment will also allow the selection of null agentnums if the current
354 user has the provided access right
355
356 =item table
357
358 Optional table name in which agentnum is being checked.  Sometimes required to
359 resolve 'column reference "agentnum" is ambiguous' errors.
360
361 =item viewall_right
362
363 All agents will be viewable if the current user has the provided access right.
364 Defaults to 'View customers of all agents'.
365
366 =back
367
368 =cut
369
370 sub agentnums_sql {
371   my( $self ) = shift;
372   my %opt = ref($_[0]) ? %{$_[0]} : @_;
373
374   my $agentnum = $opt{'table'} ? $opt{'table'}.'.agentnum' : 'agentnum';
375
376   my @or = ();
377
378   my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
379   if ( $self->access_right($viewall_right) ) {
380     push @or, "$agentnum IS NOT NULL";
381   } else {
382     my @agentnums = $self->agentnums;
383     push @or, "$agentnum IN (". join(',', @agentnums). ')'
384       if @agentnums;
385   }
386
387   push @or, "$agentnum IS NULL"
388     if $opt{'null'}
389     || ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) );
390
391   return ' 1 = 0 ' unless scalar(@or);
392   '( '. join( ' OR ', @or ). ' )';
393
394 }
395
396 =item agentnum
397
398 Returns true if the user can view the specified agent.
399
400 Also accepts optional hashref cache, to avoid redundant database calls.
401
402 =cut
403
404 sub agentnum {
405   my( $self, $agentnum, $cache ) = @_;
406   $cache ||= {};
407   return $cache->{$self->usernum}->{$agentnum}
408     if $cache->{$self->usernum}->{$agentnum};
409   my $sth = dbh->prepare(
410     "SELECT COUNT(*) FROM access_usergroup
411                      JOIN access_groupagent USING ( groupnum )
412        WHERE usernum = ? AND agentnum = ?"
413   ) or die dbh->errstr;
414   $sth->execute($self->usernum, $agentnum) or die $sth->errstr;
415   $cache->{$self->usernum}->{$agentnum} = $sth->fetchrow_arrayref->[0];
416   $sth->finish;
417   return $cache->{$self->usernum}->{$agentnum};
418 }
419
420 =item agents [ HASHREF | OPTION => VALUE ... ]
421
422 Returns the list of agents this user can view (via group membership), as
423 FS::agent objects.  Accepts the same options as the agentnums_sql method.
424
425 =cut
426
427 sub agents {
428   my $self = shift;
429   qsearch({
430     'table'     => 'agent',
431     'hashref'   => { disabled=>'' },
432     'extra_sql' => ' AND '. $self->agentnums_sql(@_),
433     'order_by'  => 'ORDER BY agent',
434   });
435 }
436
437 =item access_users [ HASHREF | OPTION => VALUE ... ]
438
439 Returns an array of FS::access_user objects, one for each non-disabled 
440 access_user in the system that shares an agent (via group membership) with 
441 the invoking object.  Regardless of options and agents, will always at
442 least return the invoking user and any users who have viewall_right.
443
444 Accepts the following options:
445
446 =over 4
447
448 =item table
449
450 Only return users who appear in the usernum field of this table
451
452 =item disabled
453
454 Include disabled users if true (defaults to false)
455
456 =item viewall_right
457
458 All users will be returned if the current user has the provided 
459 access right, regardless of agents (other filters still apply.)  
460 Defaults to 'View customers of all agents'
461
462 =cut
463
464 #Leaving undocumented until such time as this functionality is actually used
465 #
466 #=item null
467 #
468 #Users with no agents will be returned.
469 #
470 #=item null_right
471 #
472 #Users with no agents will be returned if the current user has the provided
473 #access right.
474
475 sub access_users {
476   my $self = shift;
477   my %opt = ref($_[0]) ? %{$_[0]} : @_;
478   my $table = $opt{'table'};
479   my $search = { 'table' => 'access_user' };
480   $search->{'hashref'} = $opt{'disabled'} ? {} : { 'disabled' => '' };
481   $search->{'addl_from'} = "INNER JOIN $table ON (access_user.usernum = $table.usernum)"
482     if $table;
483   my @access_users = qsearch($search);
484   my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
485   return @access_users if $self->access_right($viewall_right);
486   #filter for users with agents $self can view
487   my @out;
488   my $agentnum_cache = {};
489 ACCESS_USER:
490   foreach my $access_user (@access_users) {
491     # you can always view yourself, regardless of agents,
492     # and you can always view someone who can view you, 
493     # since they might have affected your customers
494     if ( ($self->usernum eq $access_user->usernum) 
495          || $access_user->access_right($viewall_right)
496     ) {
497       push(@out,$access_user);
498       next;
499     }
500     # if user has no agents, you need null or null_right to view
501     my @agents = $access_user->agents('viewall_right'=>'NONE'); #handled viewall_right above
502     if (!@agents) {
503       if ( $opt{'null'} ||
504            ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) )
505       ) {
506         push(@out,$access_user);
507       }
508       next;
509     }
510     # otherwise, you need an agent in common
511     foreach my $agent (@agents) {
512       if ($self->agentnum($agent->agentnum,$agentnum_cache)) {
513         push(@out,$access_user);
514         next ACCESS_USER;
515       }
516     }
517   }
518   return @out;
519 }
520
521 =item access_users_hashref  [ HASHREF | OPTION => VALUE ... ]
522
523 Accepts same options as L</access_users>.  Returns a hashref of
524 users, with keys of usernum and values of username.
525
526 =cut
527
528 sub access_users_hashref {
529   my $self = shift;
530   my %access_users = map { $_->usernum => $_->username } 
531                        $self->access_users(@_);
532   return \%access_users;
533 }
534
535 =item access_right RIGHTNAME | LISTREF
536
537 Given a right name or a list reference of right names, returns true if this
538 user has this right, or, for a list, one of the rights (currently via group
539 membership, eventually also via user overrides).
540
541 =cut
542
543 sub access_right {
544   my( $self, $rightname ) = @_;
545
546   $rightname = [ $rightname ] unless ref($rightname);
547
548   warn "$me access_right called on ". join(', ', @$rightname). "\n"
549     if $DEBUG;
550
551   #some caching of ACL requests for low-hanging fruit perf improvement
552   #since we get a new $CurrentUser object each page view there shouldn't be any
553   #issues with stickiness
554   if ( $self->{_ACLcache} ) {
555
556     unless ( grep !exists($self->{_ACLcache}{$_}), @$rightname ) {
557       warn "$me ACL cache hit for ". join(', ', @$rightname). "\n"
558         if $DEBUG;
559       return scalar( grep $self->{_ACLcache}{$_}, @$rightname );
560     }
561
562     warn "$me ACL cache miss for ". join(', ', @$rightname). "\n"
563       if $DEBUG;
564
565   } else {
566
567     warn "initializing ACL cache\n"
568       if $DEBUG;
569     $self->{_ACLcache} = {};
570
571   }
572
573   my $has_right = ' rightname IN ('. join(',', map '?', @$rightname ). ') ';
574
575   my $sth = dbh->prepare("
576     SELECT groupnum FROM access_usergroup
577                     LEFT JOIN access_group USING ( groupnum )
578                     LEFT JOIN access_right
579                          ON ( access_group.groupnum = access_right.rightobjnum )
580       WHERE usernum = ?
581         AND righttype = 'FS::access_group'
582         AND $has_right
583       LIMIT 1
584   ") or die dbh->errstr;
585   $sth->execute($self->usernum, @$rightname) or die $sth->errstr;
586   my $row = $sth->fetchrow_arrayref;
587
588   my $return = $row ? $row->[0] : '';
589
590   #just caching the single-rightname hits should be enough of a win for now
591   if ( scalar(@$rightname) == 1 ) {
592     $self->{_ACLcache}{${$rightname}[0]} = $return;
593   }
594
595   $return;
596
597 }
598
599 =item refund_rights PAYBY
600
601 Accepts payment $payby (BILL,CASH,MCRD,MCHK,CARD,CHEK) and returns a
602 list of the refund rights associated with that $payby.
603
604 Returns empty list if $payby wasn't recognized.
605
606 =cut
607
608 sub refund_rights {
609   my $self = shift;
610   my $payby = shift;
611   my @rights = ();
612   push @rights, 'Post refund'                if $payby =~ /^(BILL|CASH|MCRD|MCHK)$/;
613   push @rights, 'Post check refund'          if $payby eq 'BILL';
614   push @rights, 'Post cash refund '          if $payby eq 'CASH';
615   push @rights, 'Refund payment'             if $payby =~ /^(CARD|CHEK)$/;
616   push @rights, 'Refund credit card payment' if $payby eq 'CARD';
617   push @rights, 'Refund Echeck payment'      if $payby eq 'CHEK';
618   return @rights;
619 }
620
621 =item refund_access_right PAYBY
622
623 Returns true if user has L</access_right> for any L</refund_rights>
624 for the specified payby.
625
626 =cut
627
628 sub refund_access_right {
629   my $self = shift;
630   my $payby = shift;
631   my @rights = $self->refund_rights($payby);
632   return '' unless @rights;
633   return $self->access_right(\@rights);
634 }
635
636 =item default_customer_view
637
638 Returns the default customer view for this user, from the 
639 "default_customer_view" user preference, the "cust_main-default_view" config,
640 or the hardcoded default, "basics" (formerly "jumbo" prior to 3.0).
641
642 =cut
643
644 sub default_customer_view {
645   my $self = shift;
646
647   $self->option('default_customer_view')
648     || FS::Conf->new->config('cust_main-default_view')
649     || 'basics'; #s/jumbo/basics/ starting with 3.0
650
651 }
652
653 =item spreadsheet_format [ OVERRIDE ]
654
655 Returns a hashref of this user's Excel spreadsheet download settings:
656 'extension' (xls or xlsx), 'class' (Spreadsheet::WriteExcel or
657 Excel::Writer::XLSX), and 'mime_type'.  If OVERRIDE is 'XLS' or 'XLSX',
658 use that instead of the user's setting.
659
660 =cut
661
662 # is there a better place to put this?
663 my %formats = (
664   XLS => {
665     extension => '.xls',
666     class => 'Spreadsheet::WriteExcel',
667     mime_type => 'application/vnd.ms-excel',
668   },
669   XLSX => {
670     extension => '.xlsx',
671     class => 'Excel::Writer::XLSX',
672     mime_type => # it's on wikipedia, it must be true
673       'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
674   }
675 );
676
677 sub spreadsheet_format {
678   my $self = shift;
679   my $override = shift;
680
681   my $f =  $override
682         || $self->option('spreadsheet_format') 
683         || FS::Conf->new->config('spreadsheet_format')
684         || 'XLS';
685
686   $formats{$f};
687 }
688
689 =item is_system_user
690
691 Returns true if this user has the name of a known system account.  These 
692 users cannot log into the web interface and can't have passwords set.
693
694 =cut
695
696 sub is_system_user {
697   my $self = shift;
698   return grep { $_ eq $self->username } ( qw(
699     fs_queue
700     fs_daily
701     fs_selfservice
702     fs_signup
703     fs_bootstrap
704     fs_selfserv
705     fs_api
706   ) );
707 }
708
709 =item change_password NEW_PASSWORD
710
711 Changes the user's password to NEW_PASSWORD. This does not check password
712 policy rules (see C<is_password_allowed>) and will return an error only if
713 editing the user's record fails for some reason.
714
715 If NEW_PASSWORD is the same as the existing password, this does nothing.
716
717 =cut
718
719 sub change_password {
720   #my( $self, $password ) = @_;
721   #FS::Auth->auth_class->change_password( $self, $password );
722   FS::Auth->auth_class->change_password( @_ );
723 }
724
725 =item change_password_fields NEW_PASSWORD
726
727 =cut
728
729 sub change_password_fields {
730   #my( $self, $password ) = @_;
731   #FS::Auth->auth_class->change_password_fields( $self, $password );
732   FS::Auth->auth_class->change_password_fields( @_ );
733 }
734
735 =item locale
736
737 =cut
738
739 sub locale {
740   my $self = shift;
741   return $self->{_locale} if exists($self->{_locale});
742   $self->{_locale} = $self->option('locale');
743 }
744
745 =item get_page_pref PATH, NAME, TABLENUM
746
747 Returns the user's page preference named NAME for the page at PATH. If the
748 page is a view or edit page or otherwise shows a single record at a time,
749 it should use TABLENUM to tell which record the preference is for.
750
751 =cut
752
753 sub get_page_pref {
754   my $self = shift;
755   my ($path, $prefname, $tablenum) = @_;
756   $tablenum ||= '';
757   
758   my $access_user_page_pref = qsearchs('access_user_page_pref', {
759       path      => $path,
760       usernum   => $self->usernum,
761       tablenum  => $tablenum,
762       prefname  => $prefname,
763   }); 
764   $access_user_page_pref ? $access_user_page_pref->prefvalue : '';
765
766
767 =item set_page_pref PATH, NAME, TABLENUM, VALUE
768
769 Sets the user's page preference named NAME for the page at PATH. Use TABLENUM
770 as for get_page_pref.
771
772 =cut
773
774 sub set_page_pref {
775   my $self = shift;
776   my ($path, $prefname, $tablenum, $prefvalue) = @_;
777   $tablenum ||= '';
778   
779   my $error;
780   my $access_user_page_pref = qsearchs('access_user_page_pref', {
781       path      => $path,
782       usernum   => $self->usernum,
783       tablenum  => $tablenum,
784       prefname  => $prefname,
785   });
786   if ( $access_user_page_pref ) { 
787     if ( $prefvalue eq $access_user_page_pref->get('prefvalue') ) {
788       return '';
789     }
790     if ( length($prefvalue) > 0 ) {
791       $access_user_page_pref->set('prefvalue', $prefvalue);
792       $error = $access_user_page_pref->replace;
793       $error .= " (updating $prefname)" if $error;
794     } else { 
795       $error = $access_user_page_pref->delete;
796       $error .= " (removing $prefname)" if $error;
797     }
798   } else {
799     if ( length($prefvalue) > 0 ) {
800       $access_user_page_pref = FS::access_user_page_pref->new({
801           path      => $path,
802           usernum   => $self->usernum,
803           tablenum  => $tablenum,
804           prefname  => $prefname,
805           prefvalue => $prefvalue,
806       });
807       $error = $access_user_page_pref->insert;
808       $error .= " (creating $prefname)" if $error;
809     } else { 
810       return '';
811     }
812   }
813
814   return $error;
815 }
816
817 =back
818
819 =head1 BUGS
820
821 =head1 SEE ALSO
822
823 L<FS::Record>, schema.html from the base documentation.
824
825 =cut
826
827 1;
828