64d38d3cfd7bfadf4f44c86e142e24a459cce469
[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
225             next unless $patron->in_storage;
226
227             $patron->move_to_deleted if $params->{move};
228             $patron->delete;
229
230             $patrons_deleted++;
231         }
232     }, $self, $params );
233     return $patrons_deleted;
234 }
235
236 =head3 search_unsubscribed
237
238     Koha::Patrons->search_unsubscribed;
239
240     Returns a set of Koha patron objects for patrons that recently
241     unsubscribed and are not locked (candidates for locking).
242     Depends on UnsubscribeReflectionDelay.
243
244 =cut
245
246 sub search_unsubscribed {
247     my ( $class ) = @_;
248
249     my $delay = C4::Context->preference('UnsubscribeReflectionDelay');
250     if( !defined($delay) || $delay eq q{} ) {
251         # return empty set
252         return $class->search({ borrowernumber => undef });
253     }
254     my $parser = Koha::Database->new->schema->storage->datetime_parser;
255     my $dt = dt_from_string()->subtract( days => $delay );
256     my $str = $parser->format_datetime($dt);
257     my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
258     my $cond = [ undef, 0, 1..$fails-1 ]; # NULL, 0, 1..fails-1 (if fails>0)
259     return $class->search(
260         {
261             'patron_consents.refused_on' => { '<=' => $str },
262             'login_attempts' => $cond,
263         },
264         { join => 'patron_consents' },
265     );
266 }
267
268 =head3 search_anonymize_candidates
269
270     Koha::Patrons->search_anonymize_candidates({ locked => 1 });
271
272     Returns a set of Koha patron objects for patrons whose account is expired
273     and locked (if parameter set). These are candidates for anonymizing.
274     Depends on PatronAnonymizeDelay.
275
276 =cut
277
278 sub search_anonymize_candidates {
279     my ( $class, $params ) = @_;
280
281     my $delay = C4::Context->preference('PatronAnonymizeDelay');
282     if( !defined($delay) || $delay eq q{} ) {
283         # return empty set
284         return $class->search({ borrowernumber => undef });
285     }
286     my $cond = {};
287     my $parser = Koha::Database->new->schema->storage->datetime_parser;
288     my $dt = dt_from_string()->subtract( days => $delay );
289     my $str = $parser->format_datetime($dt);
290     $cond->{dateexpiry} = { '<=' => $str };
291     $cond->{anonymized} = 0; # not yet done
292     if( $params->{locked} ) {
293         my $fails = C4::Context->preference('FailedLoginAttempts') || 0;
294         $cond->{login_attempts} = [ -and => { '!=' => undef }, { -not_in => [0, 1..$fails-1 ] } ]; # -not_in does not like undef
295     }
296     return $class->search( $cond );
297 }
298
299 =head3 search_anonymized
300
301     Koha::Patrons->search_anonymized;
302
303     Returns a set of Koha patron objects for patron accounts that have been
304     anonymized before and could be removed.
305     Depends on PatronRemovalDelay.
306
307 =cut
308
309 sub search_anonymized {
310     my ( $class ) = @_;
311
312     my $delay = C4::Context->preference('PatronRemovalDelay');
313     if( !defined($delay) || $delay eq q{} ) {
314         # return empty set
315         return $class->search({ borrowernumber => undef });
316     }
317     my $cond = {};
318     my $parser = Koha::Database->new->schema->storage->datetime_parser;
319     my $dt = dt_from_string()->subtract( days => $delay );
320     my $str = $parser->format_datetime($dt);
321     $cond->{dateexpiry} = { '<=' => $str };
322     $cond->{anonymized} = 1;
323     return $class->search( $cond );
324 }
325
326 =head3 lock
327
328     Koha::Patrons->search({ some filters })->lock({ expire => 1, remove => 1 })
329
330     Lock the passed set of patron objects. Optionally expire and remove holds.
331     Wrapper around Koha::Patron->lock.
332
333 =cut
334
335 sub lock {
336     my ( $self, $params ) = @_;
337     my $count = $self->count;
338     while( my $patron = $self->next ) {
339         $patron->lock($params);
340     }
341 }
342
343 =head3 anonymize
344
345     Koha::Patrons->search({ some filters })->anonymize();
346
347     Anonymize passed set of patron objects.
348     Wrapper around Koha::Patron->anonymize.
349
350 =cut
351
352 sub anonymize {
353     my ( $self ) = @_;
354     my $count = $self->count;
355     while( my $patron = $self->next ) {
356         $patron->anonymize;
357     }
358 }
359
360 =head3 search_patrons_to_update_category
361
362     my $patrons = Koha::Patrons->search_patrons_to_update_category( {
363                       from          => $from_category,
364                       fine_max      => $fine_max,
365                       fine_min      => $fin_min,
366                       too_young     => $too_young,
367                       too_old      => $too_old,
368                   });
369
370 This method returns all patron who should be updated from one category to another meeting criteria:
371
372 from          - borrower categorycode
373 fine_min      - with fines totaling at least this amount
374 fine_max      - with fines above this amount
375 too_young     - if passed, select patrons who are under the age limit for the current category
376 too_old       - if passed, select patrons who are over the age limit for the current category
377
378 =cut
379
380 sub search_patrons_to_update_category {
381     my ( $self, $params ) = @_;
382     my %query;
383     my $search_params;
384
385     my $cat_from = Koha::Patron::Categories->find($params->{from});
386     $search_params->{categorycode}=$params->{from};
387     if ($params->{too_young} || $params->{too_old}){
388         my $dtf = Koha::Database->new->schema->storage->datetime_parser;
389         if( $cat_from->dateofbirthrequired && $params->{too_young} ) {
390             my $date_after = dt_from_string()->subtract( years => $cat_from->dateofbirthrequired);
391             $search_params->{dateofbirth}{'>'} = $dtf->format_datetime( $date_after );
392         }
393         if( $cat_from->upperagelimit && $params->{too_old} ) {
394             my $date_before = dt_from_string()->subtract( years => $cat_from->upperagelimit);
395             $search_params->{dateofbirth}{'<'} = $dtf->format_datetime( $date_before );
396         }
397     }
398     if ($params->{fine_min} || $params->{fine_max}) {
399         $query{join} = ["accountlines"];
400         $query{select} = ["borrowernumber", "accountlines.amountoutstanding" ];
401         $query{group_by} = ["borrowernumber"];
402         $query{having} = \['sum(accountlines.amountoutstanding) <= ?',$params->{fine_max}] if defined $params->{fine_max};
403         $query{having} = \['sum(accountlines.amountoutstanding) >= ?',$params->{fine_min}] if defined $params->{fine_min};
404     }
405     return $self->search($search_params,\%query);
406 }
407
408 =head3 update_category_to
409
410     Koha::Patrons->search->update_category_to( {
411             category   => $to_category,
412         });
413
414 Update supplied patrons from current category to another and take care of guarantor info.
415 To make sure all the conditions are met, the caller has the responsibility to
416 call search_patrons_to_update to filter the Koha::Patrons set
417
418 =cut
419
420 sub update_category_to {
421     my ( $self, $params ) = @_;
422     my $counter = 0;
423     while( my $patron = $self->next ) {
424         $counter++;
425         $patron->categorycode($params->{category})->store();
426     }
427     return $counter;
428 }
429
430 =head3 _type
431
432 =cut
433
434 sub _type {
435     return 'Borrower';
436 }
437
438 =head3 object_class
439
440 =cut
441
442 sub object_class {
443     return 'Koha::Patron';
444 }
445
446 =head1 AUTHOR
447
448 Kyle M Hall <kyle@bywatersolutions.com>
449
450 =cut
451
452 1;