Bug 24380: Unit Test
[koha.git] / Koha / Patrons.pm
blob6943018e8859b9e06b4c59d900ef5329d49d38d2
1 package Koha::Patrons;
3 # Copyright 2014 ByWater Solutions
4 # Copyright 2016 Koha Development Team
6 # This file is part of Koha.
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.
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.
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>.
21 use Modern::Perl;
23 use Carp;
25 use Koha::Database;
26 use Koha::DateUtils;
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 );
35 use base qw(Koha::Objects);
37 =head1 NAME
39 Koha::Patron - Koha Patron Object class
41 =head1 API
43 =head2 Class Methods
45 =cut
47 =head3 search_limited
49 my $patrons = Koha::Patrons->search_limit( $params, $attributes );
51 Returns all the patrons the logged in user is allowed to see
53 =cut
55 sub search_limited {
56 my ( $self, $params, $attributes ) = @_;
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;
64 $params->{'me.branchcode'} = { -in => \@restricted_branchcodes } if @restricted_branchcodes;
65 return $self->search( $params, $attributes );
68 =head3 search_housebound_choosers
70 Returns all Patrons which are Housebound choosers.
72 =cut
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);
83 =head3 search_housebound_deliverers
85 Returns all Patrons which are Housebound deliverers.
87 =cut
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);
98 =head3 search_upcoming_membership_expires
100 my $patrons = Koha::Patrons->search_upcoming_membership_expires();
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.
107 =cut
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};
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;
121 $params->{dateexpiry} = {
122 ">=" => $dtf->format_date( $date_before ),
123 "<=" => $dtf->format_date( $date_after ),
125 return $self->SUPER::search(
126 $params, { join => ['branchcode', 'categorycode'] }
130 =head3 search_patrons_to_anonymise
132 my $patrons = Koha::Patrons->search_patrons_to_anonymise( { before => $older_than_date, [ library => $library ] } );
134 This method returns all patrons who has an issue history older than a given date.
136 =cut
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;
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 } ) : () ),
157 { join => ["old_issues"],
158 distinct => 1,
161 return Koha::Patrons->_new_from_dbic($rs);
164 =head3 anonymise_issue_history
166 Koha::Patrons->search->anonymise_issue_history( { [ before => $older_than_date ] } );
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
172 =cut
174 sub anonymise_issue_history {
175 my ( $self, $params ) = @_;
177 my $older_than_date = $params->{before};
179 $older_than_date = dt_from_string $older_than_date if $older_than_date;
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(
190 $older_than_date
191 ? ( returndate =>
192 { '<' => $dtf->format_datetime($older_than_date) } )
193 : ()
197 my $anonymous_patron = C4::Context->preference('AnonymousPatron') || undef;
198 $nb_rows += $old_issues_to_anonymise->update( { 'old_issues.borrowernumber' => $anonymous_patron } );
200 return $nb_rows;
203 =head3 delete
205 Koha::Patrons->search({ some filters here })->delete({ move => 1 });
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.
212 Just like DBIx, the delete will only succeed when all entries could be
213 deleted. Returns true or throws an exception.
215 =cut
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 ) {
225 next unless $patron->in_storage;
227 $patron->move_to_deleted if $params->{move};
228 $patron->delete;
230 $patrons_deleted++;
232 }, $self, $params );
233 return $patrons_deleted;
236 =head3 search_unsubscribed
238 Koha::Patrons->search_unsubscribed;
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.
244 =cut
246 sub search_unsubscribed {
247 my ( $class ) = @_;
249 my $delay = C4::Context->preference('UnsubscribeReflectionDelay');
250 if( !defined($delay) || $delay eq q{} ) {
251 # return empty set
252 return $class->search({ borrowernumber => undef });
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(
261 'patron_consents.refused_on' => { '<=' => $str },
262 'login_attempts' => $cond,
264 { join => 'patron_consents' },
268 =head3 search_anonymize_candidates
270 Koha::Patrons->search_anonymize_candidates({ locked => 1 });
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.
276 =cut
278 sub search_anonymize_candidates {
279 my ( $class, $params ) = @_;
281 my $delay = C4::Context->preference('PatronAnonymizeDelay');
282 if( !defined($delay) || $delay eq q{} ) {
283 # return empty set
284 return $class->search({ borrowernumber => undef });
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
296 return $class->search( $cond );
299 =head3 search_anonymized
301 Koha::Patrons->search_anonymized;
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.
307 =cut
309 sub search_anonymized {
310 my ( $class ) = @_;
312 my $delay = C4::Context->preference('PatronRemovalDelay');
313 if( !defined($delay) || $delay eq q{} ) {
314 # return empty set
315 return $class->search({ borrowernumber => undef });
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 );
326 =head3 lock
328 Koha::Patrons->search({ some filters })->lock({ expire => 1, remove => 1 })
330 Lock the passed set of patron objects. Optionally expire and remove holds.
331 Wrapper around Koha::Patron->lock.
333 =cut
335 sub lock {
336 my ( $self, $params ) = @_;
337 my $count = $self->count;
338 while( my $patron = $self->next ) {
339 $patron->lock($params);
343 =head3 anonymize
345 Koha::Patrons->search({ some filters })->anonymize();
347 Anonymize passed set of patron objects.
348 Wrapper around Koha::Patron->anonymize.
350 =cut
352 sub anonymize {
353 my ( $self ) = @_;
354 my $count = $self->count;
355 while( my $patron = $self->next ) {
356 $patron->anonymize;
360 =head3 search_patrons_to_update_category
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,
370 This method returns all patron who should be updated from one category to another meeting criteria:
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
378 =cut
380 sub search_patrons_to_update_category {
381 my ( $self, $params ) = @_;
382 my %query;
383 my $search_params;
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 );
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 );
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};
405 return $self->search($search_params,\%query);
408 =head3 update_category_to
410 Koha::Patrons->search->update_category_to( {
411 category => $to_category,
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
418 =cut
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();
427 return $counter;
430 =head3 filter_by_attribute_type
432 my $patrons = Koha::Patrons->filter_by_attribute_type($attribute_type_code);
434 Return a Koha::Patrons set with patrons having the attribute defined.
436 =cut
438 sub filter_by_attribute_type {
439 my ( $self, $attribute_type ) = @_;
440 my $rs = Koha::Patron::Attributes->search( { code => $attribute_type } )
441 ->_resultset()->search_related('borrowernumber');
442 return Koha::Patrons->_new_from_dbic($rs);
445 =head3 filter_by_attribute_value
447 my $patrons = Koha::Patrons->filter_by_attribute_value($attribute_value);
449 Return a Koha::Patrons set with patrong having the attribute value passed in parameter.
451 =cut
453 sub filter_by_attribute_value {
454 my ( $self, $attribute_value ) = @_;
455 my $rs = Koha::Patron::Attributes->search(
457 'borrower_attribute_types.staff_searchable' => 1,
458 attribute => { like => "%$attribute_value%" }
460 { join => 'borrower_attribute_types' }
461 )->_resultset()->search_related('borrowernumber');
462 return Koha::Patrons->_new_from_dbic($rs);
466 =head3 _type
468 =cut
470 sub _type {
471 return 'Borrower';
474 =head3 object_class
476 =cut
478 sub object_class {
479 return 'Koha::Patron';
482 =head1 AUTHOR
484 Kyle M Hall <kyle@bywatersolutions.com>
486 =cut