Bug 28959: Add virtualshelves.public as a boolean
[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 anonymise_issue_history
162
163     Koha::Patrons->search->anonymise_issue_history( { [ before => $older_than_date ] } );
164
165 Anonymise issue history (old_issues) for all patrons older than the given date (optional).
166 To make sure all the conditions are met, the caller has the responsibility to
167 call search_patrons_to_anonymise to filter the Koha::Patrons set
168
169 =cut
170
171 sub anonymise_issue_history {
172     my ( $self, $params ) = @_;
173
174     my $older_than_date = $params->{before};
175
176     $older_than_date = dt_from_string $older_than_date if $older_than_date;
177
178     # The default of 0 does not work due to foreign key constraints
179     # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
180     # Set it to undef (NULL)
181     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
182     my $nb_rows = 0;
183     while ( my $patron = $self->next ) {
184         my $old_issues_to_anonymise = $patron->old_checkouts->search(
185         {
186             (
187                 $older_than_date
188                 ? ( returndate =>
189                       { '<' => $dtf->format_datetime($older_than_date) } )
190                 : ()
191             )
192         }
193         );
194         my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
195         $nb_rows += $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
196     }
197     return $nb_rows;
198 }
199
200 =head3 delete
201
202     Koha::Patrons->search({ some filters here })->delete({ move => 1 });
203
204     Delete passed set of patron objects.
205     Wrapper for Koha::Patron->delete. (We do not want to bypass Koha::Patron
206     and let DBIx do the job without further housekeeping.)
207     Includes a move to deletedborrowers if move flag set.
208
209     Just like DBIx, the delete will only succeed when all entries could be
210     deleted. Returns true or throws an exception.
211
212 =cut
213
214 sub delete {
215     my ( $self, $params ) = @_;
216     my $patrons_deleted;
217     $self->_resultset->result_source->schema->txn_do( sub {
218         my ( $set, $params ) = @_;
219         my $count = $set->count;
220         while ( my $patron = $set->next ) {
221
222             next unless $patron->in_storage;
223
224             $patron->move_to_deleted if $params->{move};
225             $patron->delete;
226
227             $patrons_deleted++;
228         }
229     }, $self, $params );
230     return $patrons_deleted;
231 }
232
233 =head3 filter_by_expiration_date
234
235     Koha::Patrons->filter_by_expiration_date{{ days => $x });
236
237     Returns set of Koha patron objects expired $x days.
238
239 =cut
240
241 sub filter_by_expiration_date {
242     my ( $class, $params ) = @_;
243
244     return $class->filter_by_last_update(
245         {
246             timestamp_column_name => 'dateexpiry',
247             days                  => $params->{days} || 0,
248             days_inclusive        => 1,
249         }
250     );
251 }
252
253 =head3 search_unsubscribed
254
255     Koha::Patrons->search_unsubscribed;
256
257     Returns a set of Koha patron objects for patrons that recently
258     unsubscribed and are not locked (candidates for locking).
259     Depends on UnsubscribeReflectionDelay.
260
261 =cut
262
263 sub search_unsubscribed {
264     my ( $class ) = @_;
265
266     my $delay = C4::Context->preference('UnsubscribeReflectionDelay');
267     if( !defined($delay) || $delay eq q{} ) {
268         # return empty set
269         return $class->search({ borrowernumber => undef });
270     }
271     my $parser = Koha::Database->new->schema->storage->datetime_parser;
272     my $dt = dt_from_string()->subtract( days => $delay );
273     my $str = $parser->format_datetime($dt);
274     my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
275     my $cond = [ undef, 0, 1..$fails-1 ]; # NULL, 0, 1..fails-1 (if fails>0)
276     return $class->search(
277         {
278             'patron_consents.refused_on' => { '<=' => $str },
279             'login_attempts' => $cond,
280         },
281         { join => 'patron_consents' },
282     );
283 }
284
285 =head3 search_anonymize_candidates
286
287     Koha::Patrons->search_anonymize_candidates({ locked => 1 });
288
289     Returns a set of Koha patron objects for patrons whose account is expired
290     and locked (if parameter set). These are candidates for anonymizing.
291     Depends on PatronAnonymizeDelay.
292
293 =cut
294
295 sub search_anonymize_candidates {
296     my ( $class, $params ) = @_;
297
298     my $delay = C4::Context->preference('PatronAnonymizeDelay');
299     if( !defined($delay) || $delay eq q{} ) {
300         # return empty set
301         return $class->search({ borrowernumber => undef });
302     }
303     my $cond = {};
304     my $parser = Koha::Database->new->schema->storage->datetime_parser;
305     my $dt = dt_from_string()->subtract( days => $delay );
306     my $str = $parser->format_datetime($dt);
307     $cond->{dateexpiry} = { '<=' => $str };
308     $cond->{anonymized} = 0; # not yet done
309     if( $params->{locked} ) {
310         my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
311         $cond->{login_attempts} = [ -and => { '!=' => undef }, { -not_in => [0, 1..$fails-1 ] } ]; # -not_in does not like undef
312     }
313     return $class->search( $cond );
314 }
315
316 =head3 search_anonymized
317
318     Koha::Patrons->search_anonymized;
319
320     Returns a set of Koha patron objects for patron accounts that have been
321     anonymized before and could be removed.
322     Depends on PatronRemovalDelay.
323
324 =cut
325
326 sub search_anonymized {
327     my ( $class ) = @_;
328
329     my $delay = C4::Context->preference('PatronRemovalDelay');
330     if( !defined($delay) || $delay eq q{} ) {
331         # return empty set
332         return $class->search({ borrowernumber => undef });
333     }
334     my $cond = {};
335     my $parser = Koha::Database->new->schema->storage->datetime_parser;
336     my $dt = dt_from_string()->subtract( days => $delay );
337     my $str = $parser->format_datetime($dt);
338     $cond->{dateexpiry} = { '<=' => $str };
339     $cond->{anonymized} = 1;
340     return $class->search( $cond );
341 }
342
343 =head3 lock
344
345     Koha::Patrons->search({ some filters })->lock({ expire => 1, remove => 1 })
346
347     Lock the passed set of patron objects. Optionally expire and remove holds.
348     Wrapper around Koha::Patron->lock.
349
350 =cut
351
352 sub lock {
353     my ( $self, $params ) = @_;
354     my $count = $self->count;
355     while( my $patron = $self->next ) {
356         $patron->lock($params);
357     }
358 }
359
360 =head3 anonymize
361
362     Koha::Patrons->search({ some filters })->anonymize();
363
364     Anonymize passed set of patron objects.
365     Wrapper around Koha::Patron->anonymize.
366
367 =cut
368
369 sub anonymize {
370     my ( $self ) = @_;
371     my $count = $self->count;
372     while( my $patron = $self->next ) {
373         $patron->anonymize;
374     }
375 }
376
377 =head3 search_patrons_to_update_category
378
379     my $patrons = Koha::Patrons->search_patrons_to_update_category( {
380                       from          => $from_category,
381                       fine_max      => $fine_max,
382                       fine_min      => $fin_min,
383                       too_young     => $too_young,
384                       too_old      => $too_old,
385                   });
386
387 This method returns all patron who should be updated from one category to another meeting criteria:
388
389 from          - borrower categorycode
390 fine_min      - with fines totaling at least this amount
391 fine_max      - with fines above this amount
392 too_young     - if passed, select patrons who are under the age limit for the current category
393 too_old       - if passed, select patrons who are over the age limit for the current category
394
395 =cut
396
397 sub search_patrons_to_update_category {
398     my ( $self, $params ) = @_;
399     my %query;
400     my $search_params;
401
402     my $cat_from = Koha::Patron::Categories->find($params->{from});
403     $search_params->{categorycode}=$params->{from};
404     if ($params->{too_young} || $params->{too_old}){
405         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
406         if( $cat_from->dateofbirthrequired && $params->{too_young} ) {
407             my $date_after = dt_from_string()->subtract( years => $cat_from->dateofbirthrequired);
408             $search_params->{dateofbirth}{'>'} = $dtf->format_datetime( $date_after );
409         }
410         if( $cat_from->upperagelimit && $params->{too_old} ) {
411             my $date_before = dt_from_string()->subtract( years => $cat_from->upperagelimit);
412             $search_params->{dateofbirth}{'<'} = $dtf->format_datetime( $date_before );
413         }
414     }
415     if ($params->{fine_min} || $params->{fine_max}) {
416         $query{join} = ["accountlines"];
417         $query{columns} = ["borrowernumber"];
418         $query{group_by} = ["borrowernumber"];
419         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) <= ?',$params->{fine_max}] if defined $params->{fine_max};
420         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) >= ?',$params->{fine_min}] if defined $params->{fine_min};
421     }
422     return $self->search($search_params,\%query);
423 }
424
425 =head3 update_category_to
426
427     Koha::Patrons->search->update_category_to( {
428             category   => $to_category,
429         });
430
431 Update supplied patrons from current category to another and take care of guarantor info.
432 To make sure all the conditions are met, the caller has the responsibility to
433 call search_patrons_to_update to filter the Koha::Patrons set
434
435 =cut
436
437 sub update_category_to {
438     my ( $self, $params ) = @_;
439     my $counter = 0;
440     while( my $patron = $self->next ) {
441         $counter++;
442         $patron->categorycode($params->{category})->store();
443     }
444     return $counter;
445 }
446
447 =head3 filter_by_attribute_type
448
449 my $patrons = Koha::Patrons->filter_by_attribute_type($attribute_type_code);
450
451 Return a Koha::Patrons set with patrons having the attribute defined.
452
453 =cut
454
455 sub filter_by_attribute_type {
456     my ( $self, $attribute_type ) = @_;
457     my $rs = Koha::Patron::Attributes->search( { code => $attribute_type } )
458       ->_resultset()->search_related('borrowernumber');
459     return Koha::Patrons->_new_from_dbic($rs);
460 }
461
462 =head3 filter_by_attribute_value
463
464 my $patrons = Koha::Patrons->filter_by_attribute_value($attribute_value);
465
466 Return a Koha::Patrons set with patrong having the attribute value passed in parameter.
467
468 =cut
469
470 sub filter_by_attribute_value {
471     my ( $self, $attribute_value ) = @_;
472     my $rs = Koha::Patron::Attributes->search(
473         {
474             'borrower_attribute_types.staff_searchable' => 1,
475             attribute => { like => "%$attribute_value%" }
476         },
477         { join => 'borrower_attribute_types' }
478     )->_resultset()->search_related('borrowernumber');
479     return Koha::Patrons->_new_from_dbic($rs);
480 }
481
482
483 =head3 _type
484
485 =cut
486
487 sub _type {
488     return 'Borrower';
489 }
490
491 =head3 object_class
492
493 =cut
494
495 sub object_class {
496     return 'Koha::Patron';
497 }
498
499 =head1 AUTHOR
500
501 Kyle M Hall <kyle@bywatersolutions.com>
502
503 =cut
504
505 1;