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