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