Bug 28653: Add new method RefreshIssuesTable
[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::ArticleRequest::Status;
29 use Koha::Patron;
30 use Koha::Exceptions::Patron;
31 use Koha::Patron::Categories;
32
33 use base qw(Koha::Objects);
34
35 =head1 NAME
36
37 Koha::Patron - Koha Patron Object class
38
39 =head1 API
40
41 =head2 Class Methods
42
43 =cut
44
45 =head3 search_limited
46
47 my $patrons = Koha::Patrons->search_limit( $params, $attributes );
48
49 Returns all the patrons the logged in user is allowed to see
50
51 =cut
52
53 sub search_limited {
54     my ( $self, $params, $attributes ) = @_;
55
56     my $userenv = C4::Context->userenv;
57     my @restricted_branchcodes;
58     if ( $userenv and $userenv->{number} ) {
59         my $logged_in_user = Koha::Patrons->find( $userenv->{number} );
60         @restricted_branchcodes = $logged_in_user->libraries_where_can_see_patrons;
61     }
62     $params->{'me.branchcode'} = { -in => \@restricted_branchcodes } if @restricted_branchcodes;
63     return $self->search( $params, $attributes );
64 }
65
66 =head3 search_housebound_choosers
67
68 Returns all Patrons which are Housebound choosers.
69
70 =cut
71
72 sub search_housebound_choosers {
73     my ( $self ) = @_;
74     my $cho = $self->_resultset
75         ->search_related('housebound_role', {
76             housebound_chooser => 1,
77         })->search_related('borrowernumber');
78     return Koha::Patrons->_new_from_dbic($cho);
79 }
80
81 =head3 search_housebound_deliverers
82
83 Returns all Patrons which are Housebound deliverers.
84
85 =cut
86
87 sub search_housebound_deliverers {
88     my ( $self ) = @_;
89     my $del = $self->_resultset
90         ->search_related('housebound_role', {
91             housebound_deliverer => 1,
92         })->search_related('borrowernumber');
93     return Koha::Patrons->_new_from_dbic($del);
94 }
95
96 =head3 search_upcoming_membership_expires
97
98 my $patrons = Koha::Patrons->search_upcoming_membership_expires();
99
100 The 'before' and 'after' represent the number of days before/after the date
101 that is set by the preference MembershipExpiryDaysNotice.
102 If the pref is 14, before 2 and after 3 then you will get all expires
103 from 12 to 17 days.
104
105 =cut
106
107 sub search_upcoming_membership_expires {
108     my ( $self, $params ) = @_;
109     my $before = $params->{before} || 0;
110     my $after  = $params->{after} || 0;
111     delete $params->{before};
112     delete $params->{after};
113
114     my $days = C4::Context->preference("MembershipExpiryDaysNotice") || 0;
115     my $date_before = dt_from_string->add( days => $days - $before );
116     my $date_after = dt_from_string->add( days => $days + $after );
117     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
118
119     $params->{dateexpiry} = {
120         ">=" => $dtf->format_date( $date_before ),
121         "<=" => $dtf->format_date( $date_after ),
122     };
123     return $self->SUPER::search(
124         $params, { join => ['branchcode', 'categorycode'] }
125     );
126 }
127
128 =head3 search_patrons_to_anonymise
129
130     my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library ] } );
131
132 This method returns all patrons who has an issue history older than a given date.
133
134 =cut
135
136 sub search_patrons_to_anonymise {
137     my ( $class, $params ) = @_;
138     my $older_than_date = $params->{before};
139     my $library         = $params->{library};
140     $older_than_date = $older_than_date ? dt_from_string($older_than_date) : dt_from_string;
141     $library ||=
142       ( C4::Context->preference('IndependentBranches') && C4::Context->userenv && !C4::Context->IsSuperLibrarian() && C4::Context->userenv->{branch} )
143       ? C4::Context->userenv->{branch}
144       : undef;
145     my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
146
147     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
148     my $rs = $class->_resultset->search(
149         {   returndate                  => { '<'   =>  $dtf->format_datetime($older_than_date), },
150             'old_issues.borrowernumber' => { 'not' => undef },
151             privacy                     => { '<>'  => 0 },                  # Keep forever
152             ( $library ? ( 'old_issues.branchcode' => $library ) : () ),
153             ( $anonymous_patron ? ( 'old_issues.borrowernumber' => { '!=' => $anonymous_patron } ) : () ),
154         },
155         {   join     => ["old_issues"],
156             distinct => 1,
157         }
158     );
159     return Koha::Patrons->_new_from_dbic($rs);
160 }
161
162 =head3 anonymise_issue_history
163
164     Koha::Patrons->search->anonymise_issue_history( { [ before => $older_than_date ] } );
165
166 Anonymise issue history (old_issues) for all patrons older than the given date (optional).
167 To make sure all the conditions are met, the caller has the responsibility to
168 call search_patrons_to_anonymise to filter the Koha::Patrons set
169
170 =cut
171
172 sub anonymise_issue_history {
173     my ( $self, $params ) = @_;
174
175     my $older_than_date = $params->{before};
176
177     $older_than_date = dt_from_string $older_than_date if $older_than_date;
178
179     # The default of 0 does not work due to foreign key constraints
180     # The anonymisation should not fail quietly if AnonymousPatron is not a valid entry
181     # Set it to undef (NULL)
182     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
183     my $nb_rows = 0;
184     while ( my $patron = $self->next ) {
185         my $old_issues_to_anonymise = $patron->old_checkouts->search(
186         {
187             (
188                 $older_than_date
189                 ? ( returndate =>
190                       { '<' => $dtf->format_datetime($older_than_date) } )
191                 : ()
192             )
193         }
194         );
195         my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
196         $nb_rows += $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
197     }
198     return $nb_rows;
199 }
200
201 =head3 delete
202
203     Koha::Patrons->search({ some filters here })->delete({ move => 1 });
204
205     Delete passed set of patron objects.
206     Wrapper for Koha::Patron->delete. (We do not want to bypass Koha::Patron
207     and let DBIx do the job without further housekeeping.)
208     Includes a move to deletedborrowers if move flag set.
209
210     Just like DBIx, the delete will only succeed when all entries could be
211     deleted. Returns true or throws an exception.
212
213 =cut
214
215 sub delete {
216     my ( $self, $params ) = @_;
217     my $patrons_deleted;
218     $self->_resultset->result_source->schema->txn_do( sub {
219         my ( $set, $params ) = @_;
220         my $count = $set->count;
221         while ( my $patron = $set->next ) {
222
223             next unless $patron->in_storage;
224
225             $patron->move_to_deleted if $params->{move};
226             $patron->delete;
227
228             $patrons_deleted++;
229         }
230     }, $self, $params );
231     return $patrons_deleted;
232 }
233
234 =head3 filter_by_expiration_date
235
236     Koha::Patrons->filter_by_expiration_date{{ days => $x });
237
238     Returns set of Koha patron objects expired $x days.
239
240 =cut
241
242 sub filter_by_expiration_date {
243     my ( $class, $params ) = @_;
244
245     return $class->filter_by_last_update(
246         {
247             timestamp_column_name => 'dateexpiry',
248             days                  => $params->{days} || 0,
249             days_inclusive        => 1,
250         }
251     );
252 }
253
254 =head3 search_unsubscribed
255
256     Koha::Patrons->search_unsubscribed;
257
258     Returns a set of Koha patron objects for patrons that recently
259     unsubscribed and are not locked (candidates for locking).
260     Depends on UnsubscribeReflectionDelay.
261
262 =cut
263
264 sub search_unsubscribed {
265     my ( $class ) = @_;
266
267     my $delay = C4::Context->preference('UnsubscribeReflectionDelay');
268     if( !defined($delay) || $delay eq q{} ) {
269         # return empty set
270         return $class->search({ borrowernumber => undef });
271     }
272     my $parser = Koha::Database->new->schema->storage->datetime_parser;
273     my $dt = dt_from_string()->subtract( days => $delay );
274     my $str = $parser->format_datetime($dt);
275     my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
276     my $cond = [ undef, 0, 1..$fails-1 ]; # NULL, 0, 1..fails-1 (if fails>0)
277     return $class->search(
278         {
279             'patron_consents.refused_on' => { '<=' => $str },
280             'login_attempts' => $cond,
281         },
282         { join => 'patron_consents' },
283     );
284 }
285
286 =head3 search_anonymize_candidates
287
288     Koha::Patrons->search_anonymize_candidates({ locked => 1 });
289
290     Returns a set of Koha patron objects for patrons whose account is expired
291     and locked (if parameter set). These are candidates for anonymizing.
292     Depends on PatronAnonymizeDelay.
293
294 =cut
295
296 sub search_anonymize_candidates {
297     my ( $class, $params ) = @_;
298
299     my $delay = C4::Context->preference('PatronAnonymizeDelay');
300     if( !defined($delay) || $delay eq q{} ) {
301         # return empty set
302         return $class->search({ borrowernumber => undef });
303     }
304     my $cond = {};
305     my $parser = Koha::Database->new->schema->storage->datetime_parser;
306     my $dt = dt_from_string()->subtract( days => $delay );
307     my $str = $parser->format_datetime($dt);
308     $cond->{dateexpiry} = { '<=' => $str };
309     $cond->{anonymized} = 0; # not yet done
310     if( $params->{locked} ) {
311         my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
312         $cond->{login_attempts} = [ -and => { '!=' => undef }, { -not_in => [0, 1..$fails-1 ] } ]; # -not_in does not like undef
313     }
314     return $class->search( $cond );
315 }
316
317 =head3 search_anonymized
318
319     Koha::Patrons->search_anonymized;
320
321     Returns a set of Koha patron objects for patron accounts that have been
322     anonymized before and could be removed.
323     Depends on PatronRemovalDelay.
324
325 =cut
326
327 sub search_anonymized {
328     my ( $class ) = @_;
329
330     my $delay = C4::Context->preference('PatronRemovalDelay');
331     if( !defined($delay) || $delay eq q{} ) {
332         # return empty set
333         return $class->search({ borrowernumber => undef });
334     }
335     my $cond = {};
336     my $parser = Koha::Database->new->schema->storage->datetime_parser;
337     my $dt = dt_from_string()->subtract( days => $delay );
338     my $str = $parser->format_datetime($dt);
339     $cond->{dateexpiry} = { '<=' => $str };
340     $cond->{anonymized} = 1;
341     return $class->search( $cond );
342 }
343
344 =head3 lock
345
346     Koha::Patrons->search({ some filters })->lock({ expire => 1, remove => 1 })
347
348     Lock the passed set of patron objects. Optionally expire and remove holds.
349     Wrapper around Koha::Patron->lock.
350
351 =cut
352
353 sub lock {
354     my ( $self, $params ) = @_;
355     my $count = $self->count;
356     while( my $patron = $self->next ) {
357         $patron->lock($params);
358     }
359 }
360
361 =head3 anonymize
362
363     Koha::Patrons->search({ some filters })->anonymize();
364
365     Anonymize passed set of patron objects.
366     Wrapper around Koha::Patron->anonymize.
367
368 =cut
369
370 sub anonymize {
371     my ( $self ) = @_;
372     my $count = $self->count;
373     while( my $patron = $self->next ) {
374         $patron->anonymize;
375     }
376 }
377
378 =head3 search_patrons_to_update_category
379
380     my $patrons = Koha::Patrons->search_patrons_to_update_category( {
381                       from          => $from_category,
382                       fine_max      => $fine_max,
383                       fine_min      => $fin_min,
384                       too_young     => $too_young,
385                       too_old      => $too_old,
386                   });
387
388 This method returns all patron who should be updated from one category to another meeting criteria:
389
390 from          - borrower categorycode
391 fine_min      - with fines totaling at least this amount
392 fine_max      - with fines above this amount
393 too_young     - if passed, select patrons who are under the age limit for the current category
394 too_old       - if passed, select patrons who are over the age limit for the current category
395
396 =cut
397
398 sub search_patrons_to_update_category {
399     my ( $self, $params ) = @_;
400     my %query;
401     my $search_params;
402
403     my $cat_from = Koha::Patron::Categories->find($params->{from});
404     $search_params->{categorycode}=$params->{from};
405     if ($params->{too_young} || $params->{too_old}){
406         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
407         if( $cat_from->dateofbirthrequired && $params->{too_young} ) {
408             my $date_after = dt_from_string()->subtract( years => $cat_from->dateofbirthrequired);
409             $search_params->{dateofbirth}{'>'} = $dtf->format_datetime( $date_after );
410         }
411         if( $cat_from->upperagelimit && $params->{too_old} ) {
412             my $date_before = dt_from_string()->subtract( years => $cat_from->upperagelimit);
413             $search_params->{dateofbirth}{'<'} = $dtf->format_datetime( $date_before );
414         }
415     }
416     if ($params->{fine_min} || $params->{fine_max}) {
417         $query{join} = ["accountlines"];
418         $query{columns} = ["borrowernumber"];
419         $query{group_by} = ["borrowernumber"];
420         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) <= ?',$params->{fine_max}] if defined $params->{fine_max};
421         $query{having} = \['COALESCE(sum(accountlines.amountoutstanding),0) >= ?',$params->{fine_min}] if defined $params->{fine_min};
422     }
423     return $self->search($search_params,\%query);
424 }
425
426 =head3 update_category_to
427
428     Koha::Patrons->search->update_category_to( {
429             category   => $to_category,
430         });
431
432 Update supplied patrons from current category to another and take care of guarantor info.
433 To make sure all the conditions are met, the caller has the responsibility to
434 call search_patrons_to_update to filter the Koha::Patrons set
435
436 =cut
437
438 sub update_category_to {
439     my ( $self, $params ) = @_;
440     my $counter = 0;
441     while( my $patron = $self->next ) {
442         $counter++;
443         $patron->categorycode($params->{category})->store();
444     }
445     return $counter;
446 }
447
448 =head3 filter_by_attribute_type
449
450 my $patrons = Koha::Patrons->filter_by_attribute_type($attribute_type_code);
451
452 Return a Koha::Patrons set with patrons having the attribute defined.
453
454 =cut
455
456 sub filter_by_attribute_type {
457     my ( $self, $attribute_type ) = @_;
458     my $rs = Koha::Patron::Attributes->search( { code => $attribute_type } )
459       ->_resultset()->search_related('borrowernumber');
460     return Koha::Patrons->_new_from_dbic($rs);
461 }
462
463 =head3 filter_by_attribute_value
464
465 my $patrons = Koha::Patrons->filter_by_attribute_value($attribute_value);
466
467 Return a Koha::Patrons set with patrong having the attribute value passed in parameter.
468
469 =cut
470
471 sub filter_by_attribute_value {
472     my ( $self, $attribute_value ) = @_;
473     my $rs = Koha::Patron::Attributes->search(
474         {
475             'borrower_attribute_types.staff_searchable' => 1,
476             attribute => { like => "%$attribute_value%" }
477         },
478         { join => 'borrower_attribute_types' }
479     )->_resultset()->search_related('borrowernumber');
480     return Koha::Patrons->_new_from_dbic($rs);
481 }
482
483
484 =head3 _type
485
486 =cut
487
488 sub _type {
489     return 'Borrower';
490 }
491
492 =head3 object_class
493
494 =cut
495
496 sub object_class {
497     return 'Koha::Patron';
498 }
499
500 =head1 AUTHOR
501
502 Kyle M Hall <kyle@bywatersolutions.com>
503
504 =cut
505
506 1;