Bug 33708: Make staff interface login not require public API (OAuth/OIDC)
[koha.git] / Koha / Patrons.pm
1 package Koha::Patrons;
2
3 # Copyright 2014 ByWater Solutions
4 # Copyright 2016 Koha Development Team
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use Modern::Perl;
22
23
24 use Koha::Database;
25 use Koha::DateUtils qw( dt_from_string );
26
27 use Koha::ArticleRequests;
28 use Koha::Patron;
29 use Koha::Exceptions::Patron;
30 use Koha::Patron::Categories;
31
32 use base qw(Koha::Objects);
33
34 =head1 NAME
35
36 Koha::Patron - Koha Patron Object class
37
38 =head1 API
39
40 =head2 Class Methods
41
42 =cut
43
44 =head3 search_limited
45
46 my $patrons = Koha::Patrons->search_limit( $params, $attributes );
47
48 Returns all the patrons the logged in user is allowed to see
49
50 =cut
51
52 sub search_limited {
53     my ( $self, $params, $attributes ) = @_;
54
55     my $userenv = C4::Context->userenv;
56     my @restricted_branchcodes;
57     if ( $userenv and $userenv->{number} ) {
58         my $logged_in_user = Koha::Patrons->find( $userenv->{number} );
59         @restricted_branchcodes = $logged_in_user->libraries_where_can_see_patrons;
60     }
61     $params->{'me.branchcode'} = { -in => \@restricted_branchcodes } if @restricted_branchcodes;
62     return $self->search( $params, $attributes );
63 }
64
65 =head3 search_housebound_choosers
66
67 Returns all Patrons which are Housebound choosers.
68
69 =cut
70
71 sub search_housebound_choosers {
72     my ( $self ) = @_;
73     my $cho = $self->_resultset
74         ->search_related('housebound_role', {
75             housebound_chooser => 1,
76         })->search_related('borrowernumber');
77     return Koha::Patrons->_new_from_dbic($cho);
78 }
79
80 =head3 search_housebound_deliverers
81
82 Returns all Patrons which are Housebound deliverers.
83
84 =cut
85
86 sub search_housebound_deliverers {
87     my ( $self ) = @_;
88     my $del = $self->_resultset
89         ->search_related('housebound_role', {
90             housebound_deliverer => 1,
91         })->search_related('borrowernumber');
92     return Koha::Patrons->_new_from_dbic($del);
93 }
94
95 =head3 search_upcoming_membership_expires
96
97 my $patrons = Koha::Patrons->search_upcoming_membership_expires();
98
99 The 'before' and 'after' represent the number of days before/after the date
100 that is set by the preference MembershipExpiryDaysNotice.
101 If the pref is 14, before 2 and after 3 then you will get all expires
102 from 12 to 17 days.
103
104 =cut
105
106 sub search_upcoming_membership_expires {
107     my ( $self, $params ) = @_;
108     my $before = $params->{before} || 0;
109     my $after  = $params->{after} || 0;
110     delete $params->{before};
111     delete $params->{after};
112
113     my $days = C4::Context->preference("MembershipExpiryDaysNotice") || 0;
114     my $date_before = dt_from_string->add( days => $days - $before );
115     my $date_after = dt_from_string->add( days => $days + $after );
116     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
117
118     $params->{dateexpiry} = {
119         ">=" => $dtf->format_date( $date_before ),
120         "<=" => $dtf->format_date( $date_after ),
121     };
122     return $self->SUPER::search(
123         $params, { join => ['branchcode', 'categorycode'] }
124     );
125 }
126
127 =head3 search_patrons_to_anonymise
128
129     my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library ] } );
130
131 This method returns all patrons who has an issue history older than a given date.
132
133 =cut
134
135 sub search_patrons_to_anonymise {
136     my ( $class, $params ) = @_;
137     my $older_than_date = $params->{before};
138     my $library         = $params->{library};
139     $older_than_date = $older_than_date ? dt_from_string($older_than_date) : dt_from_string;
140     $library ||=
141       ( C4::Context->preference('IndependentBranches') && C4::Context->userenv && !C4::Context->IsSuperLibrarian() && C4::Context->userenv->{branch} )
142       ? C4::Context->userenv->{branch}
143       : undef;
144     my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
145
146     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
147     my $rs = $class->_resultset->search(
148         {   returndate                  => { '<'   =>  $dtf->format_datetime($older_than_date), },
149             'old_issues.borrowernumber' => { 'not' => undef },
150             privacy                     => { '<>'  => 0 },                  # Keep forever
151             ( $library ? ( 'old_issues.branchcode' => $library ) : () ),
152             ( $anonymous_patron ? ( 'old_issues.borrowernumber' => { '!=' => $anonymous_patron } ) : () ),
153         },
154         {   join     => ["old_issues"],
155             distinct => 1,
156         }
157     );
158     return Koha::Patrons->_new_from_dbic($rs);
159 }
160
161 =head3 delete
162
163     Koha::Patrons->search({ some filters here })->delete({ move => 1 });
164
165     Delete passed set of patron objects.
166     Wrapper for Koha::Patron->delete. (We do not want to bypass Koha::Patron
167     and let DBIx do the job without further housekeeping.)
168     Includes a move to deletedborrowers if move flag set.
169
170     Just like DBIx, the delete will only succeed when all entries could be
171     deleted. Returns true or throws an exception.
172
173 =cut
174
175 sub delete {
176     my ( $self, $params ) = @_;
177     my $patrons_deleted;
178     $self->_resultset->result_source->schema->txn_do( sub {
179         my ( $set, $params ) = @_;
180         my $count = $set->count;
181         while ( my $patron = $set->next ) {
182
183             next unless $patron->in_storage;
184
185             $patron->move_to_deleted if $params->{move};
186             $patron->delete;
187
188             $patrons_deleted++;
189         }
190     }, $self, $params );
191     return $patrons_deleted;
192 }
193
194 =head3 filter_by_expiration_date
195
196     Koha::Patrons->filter_by_expiration_date{{ days => $x });
197
198     Returns set of Koha patron objects expired $x days.
199
200 =cut
201
202 sub filter_by_expiration_date {
203     my ( $class, $params ) = @_;
204
205     return $class->filter_by_last_update(
206         {
207             timestamp_column_name => 'dateexpiry',
208             days                  => $params->{days} || 0,
209             days_inclusive        => 1,
210         }
211     );
212 }
213
214 =head3 search_unsubscribed
215
216     Koha::Patrons->search_unsubscribed;
217
218     Returns a set of Koha patron objects for patrons that recently
219     unsubscribed and are not locked (candidates for locking).
220     Depends on UnsubscribeReflectionDelay.
221
222 =cut
223
224 sub search_unsubscribed {
225     my ( $class ) = @_;
226
227     my $delay = C4::Context->preference('UnsubscribeReflectionDelay');
228     if( !defined($delay) || $delay eq q{} ) {
229         # return empty set
230         return $class->search({ borrowernumber => undef });
231     }
232     my $parser = Koha::Database->new->schema->storage->datetime_parser;
233     my $dt = dt_from_string()->subtract( days => $delay );
234     my $str = $parser->format_datetime($dt);
235     my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
236     my $cond = [ undef, 0, 1..$fails-1 ]; # NULL, 0, 1..fails-1 (if fails>0)
237     return $class->search(
238         {
239             'patron_consents.refused_on' => { '<=' => $str },
240             'patron_consents.type' => 'GDPR_PROCESSING',
241             'login_attempts' => $cond,
242         },
243         { join => 'patron_consents' },
244     );
245 }
246
247 =head3 search_anonymize_candidates
248
249     Koha::Patrons->search_anonymize_candidates({ locked => 1 });
250
251     Returns a set of Koha patron objects for patrons whose account is expired
252     and locked (if parameter set). These are candidates for anonymizing.
253     Depends on PatronAnonymizeDelay.
254
255 =cut
256
257 sub search_anonymize_candidates {
258     my ( $class, $params ) = @_;
259
260     my $delay = C4::Context->preference('PatronAnonymizeDelay');
261     if( !defined($delay) || $delay eq q{} ) {
262         # return empty set
263         return $class->search({ borrowernumber => undef });
264     }
265     my $cond = {};
266     my $parser = Koha::Database->new->schema->storage->datetime_parser;
267     my $dt = dt_from_string()->subtract( days => $delay );
268     my $str = $parser->format_datetime($dt);
269     $cond->{dateexpiry} = { '<=' => $str };
270     $cond->{anonymized} = 0; # not yet done
271     if( $params->{locked} ) {
272         my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
273         $cond->{login_attempts} = [ -and => { '!=' => undef }, { -not_in => [0, 1..$fails-1 ] } ]; # -not_in does not like undef
274     }
275     return $class->search( $cond );
276 }
277
278 =head3 search_anonymized
279
280     Koha::Patrons->search_anonymized;
281
282     Returns a set of Koha patron objects for patron accounts that have been
283     anonymized before and could be removed.
284     Depends on PatronRemovalDelay.
285
286 =cut
287
288 sub search_anonymized {
289     my ( $class ) = @_;
290
291     my $delay = C4::Context->preference('PatronRemovalDelay');
292     if( !defined($delay) || $delay eq q{} ) {
293         # return empty set
294         return $class->search({ borrowernumber => undef });
295     }
296     my $cond = {};
297     my $parser = Koha::Database->new->schema->storage->datetime_parser;
298     my $dt = dt_from_string()->subtract( days => $delay );
299     my $str = $parser->format_datetime($dt);
300     $cond->{dateexpiry} = { '<=' => $str };
301     $cond->{anonymized} = 1;
302     return $class->search( $cond );
303 }
304
305 =head3 lock
306
307     Koha::Patrons->search({ some filters })->lock({ expire => 1, remove => 1 })
308
309     Lock the passed set of patron objects. Optionally expire and remove holds.
310     Wrapper around Koha::Patron->lock.
311
312 =cut
313
314 sub lock {
315     my ( $self, $params ) = @_;
316     my $count = $self->count;
317     while( my $patron = $self->next ) {
318         $patron->lock($params);
319     }
320 }
321
322 =head3 anonymize
323
324     Koha::Patrons->search({ some filters })->anonymize();
325
326     Anonymize passed set of patron objects.
327     Wrapper around Koha::Patron->anonymize.
328
329 =cut
330
331 sub anonymize {
332     my ( $self ) = @_;
333     my $count = $self->count;
334     while( my $patron = $self->next ) {
335         $patron->anonymize;
336     }
337 }
338
339 =head3 search_patrons_to_update_category
340
341     my $patrons = Koha::Patrons->search_patrons_to_update_category( {
342                       from          => $from_category,
343                       fine_max      => $fine_max,
344                       fine_min      => $fin_min,
345                       too_young     => $too_young,
346                       too_old      => $too_old,
347                   });
348
349 This method returns all patron who should be updated from one category to another meeting criteria:
350
351 from          - borrower categorycode
352 fine_min      - with fines totaling at least this amount
353 fine_max      - with fines above this amount
354 too_young     - if passed, select patrons who are under the age limit for the current category
355 too_old       - if passed, select patrons who are over the age limit for the current category
356
357 =cut
358
359 sub search_patrons_to_update_category {
360     my ( $self, $params ) = @_;
361     my %query;
362     my $search_params;
363
364     my $cat_from = Koha::Patron::Categories->find($params->{from});
365     $search_params->{categorycode}=$params->{from};
366     if ($params->{too_young} || $params->{too_old}){
367         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
368         if( $cat_from->dateofbirthrequired && $params->{too_young} ) {
369             my $date_after = dt_from_string()->subtract( years => $cat_from->dateofbirthrequired);
370             $search_params->{dateofbirth}{'>'} = $dtf->format_datetime( $date_after );
371         }
372         if( $cat_from->upperagelimit && $params->{too_old} ) {
373             my $date_before = dt_from_string()->subtract( years => $cat_from->upperagelimit);
374             $search_params->{dateofbirth}{'<'} = $dtf->format_datetime( $date_before );
375         }
376     }
377     if ($params->{fine_min} || $params->{fine_max}) {
378         $query{join} = ["accountlines"];
379         $query{columns} = ["borrowernumber"];
380         $query{group_by} = ["borrowernumber"];
381         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) <= ?',$params->{fine_max}] if defined $params->{fine_max};
382         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) >= ?',$params->{fine_min}] if defined $params->{fine_min};
383     }
384     return $self->search($search_params,\%query);
385 }
386
387 =head3 update_category_to
388
389     Koha::Patrons->search->update_category_to( {
390             category   => $to_category,
391         });
392
393 Update supplied patrons from current category to another and take care of guarantor info.
394 To make sure all the conditions are met, the caller has the responsibility to
395 call search_patrons_to_update to filter the Koha::Patrons set
396
397 =cut
398
399 sub update_category_to {
400     my ( $self, $params ) = @_;
401     my $counter = 0;
402     while( my $patron = $self->next ) {
403         $counter++;
404         $patron->categorycode($params->{category})->store();
405     }
406     return $counter;
407 }
408
409 =head3 filter_by_attribute_type
410
411 my $patrons = Koha::Patrons->filter_by_attribute_type($attribute_type_code);
412
413 Return a Koha::Patrons set with patrons having the attribute defined.
414
415 =cut
416
417 sub filter_by_attribute_type {
418     my ( $self, $attribute_type ) = @_;
419     my $rs = Koha::Patron::Attributes->search( { code => $attribute_type } )
420       ->_resultset()->search_related('borrowernumber');
421     return Koha::Patrons->_new_from_dbic($rs);
422 }
423
424 =head3 filter_by_attribute_value
425
426 my $patrons = Koha::Patrons->filter_by_attribute_value($attribute_value);
427
428 Return a Koha::Patrons set with patrong having the attribute value passed in parameter.
429
430 =cut
431
432 sub filter_by_attribute_value {
433     my ( $self, $attribute_value ) = @_;
434     my $rs = Koha::Patron::Attributes->search(
435         {
436             'borrower_attribute_types.staff_searchable' => 1,
437             attribute => { like => "%$attribute_value%" }
438         },
439         { join => 'borrower_attribute_types' }
440     )->_resultset()->search_related('borrowernumber');
441     return Koha::Patrons->_new_from_dbic($rs);
442 }
443
444 =head3 filter_by_amount_owed
445
446     Koha::Patrons->filter_by_amount_owed(
447         {
448             less_than  => '2.00',
449             more_than  => '0.50',
450             debit_type => $debit_type_code,
451             library    => $branchcode
452         }
453     );
454
455 Returns patrons filtered by how much money they owe, between passed limits.
456
457 Optionally limit to debts of a particular debit_type or/and owed to a particular library.
458
459 =head4 arguments hashref
460
461 =over 4
462
463 =item less_than (optional)  - filter out patrons who owe less than Amount
464
465 =item more_than (optional)  - filter out patrons who owe more than Amount
466
467 =item debit_type (optional) - filter the amount owed by debit type
468
469 =item library (optional)    - filter the amount owed to a particular branch
470
471 =back
472
473 =cut
474
475 sub filter_by_amount_owed {
476     my ( $self, $options ) = @_;
477
478     return $self
479       unless (
480         defined($options)
481         && (   defined( $options->{less_than} )
482             || defined( $options->{more_than} ) )
483       );
484
485     my $where = {};
486     my $group_by =
487       [ map { 'me.' . $_ } $self->_resultset->result_source->columns ];
488
489     my $attrs = {
490         join     => 'accountlines',
491         group_by => $group_by,
492         '+select' =>
493           { sum => 'accountlines.amountoutstanding', '-as' => 'outstanding' },
494         '+as' => 'outstanding'
495     };
496
497     $where->{'accountlines.debit_type_code'} = $options->{debit_type}
498       if defined( $options->{debit_type} );
499
500     $where->{'accountlines.branchcode'} = $options->{library}
501       if defined( $options->{library} );
502
503     $attrs->{'having'} = [
504         { 'outstanding' => { '<' => $options->{less_than} } },
505         { 'outstanding' => undef }
506       ]
507       if ( defined( $options->{less_than} )
508         && !defined( $options->{more_than} ) );
509
510     $attrs->{'having'} = { 'outstanding' => { '>' => $options->{more_than} } }
511       if (!defined( $options->{less_than} )
512         && defined( $options->{more_than} ) );
513
514     $attrs->{'having'}->{'-and'} = [
515         { 'outstanding' => { '>' => $options->{more_than} } },
516         { 'outstanding' => { '<' => $options->{less_than} } }
517       ]
518       if ( defined( $options->{less_than} )
519         && defined( $options->{more_than} ) );
520
521     return $self->search( $where, $attrs );
522 }
523
524 =head3 filter_by_have_permission
525
526     my $patrons = Koha::Patrons->search->filter_by_have_permission('suggestions.suggestions_manage');
527
528     my $patrons = Koha::Patrons->search->filter_by_have_permission('suggestions');
529
530 Filter patrons who have a given subpermission or the whole permission.
531
532 =cut
533
534 sub filter_by_have_permission {
535     my ($self, $subpermission) = @_;
536
537     my ($p, $sp) = split '\.', $subpermission;
538
539     my $perm = Koha::Database->new()->schema()->resultset('Userflag')->find({flag => $p});
540
541     Koha::Exceptions::ObjectNotFound->throw( sprintf( "Permission %s not found", $p ) )
542       unless $perm;
543
544     my $bit = $perm->bit;
545
546     return $self->search(
547         {
548             -and => [
549                 -or => [
550                     \"me.flags & (1 << $bit)",
551                     { 'me.flags' => 1 },
552                     (
553                         $sp
554                         ? {
555                             -and => [
556                                 { 'user_permissions.module_bit' => $bit },
557                                 { 'user_permissions.code'       => $sp }
558                             ]
559                           }
560                         : ()
561                     )
562                 ]
563             ]
564         },
565         { prefetch => 'user_permissions' }
566     );
567 }
568
569 =head3 _type
570
571 =cut
572
573 sub _type {
574     return 'Borrower';
575 }
576
577 =head3 object_class
578
579 =cut
580
581 sub object_class {
582     return 'Koha::Patron';
583 }
584
585 =head1 AUTHOR
586
587 Kyle M Hall <kyle@bywatersolutions.com>
588
589 =cut
590
591 1;