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