Bug 20287: Move fixup_cardnumber
[koha.git] / Koha / Patron.pm
1 package Koha::Patron;
2
3 # Copyright ByWater Solutions 2014
4 # Copyright PTFS Europe 2016
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 use List::MoreUtils qw( uniq );
25 use Text::Unaccent qw( unac_string );
26
27 use C4::Context;
28 use C4::Log;
29 use Koha::Checkouts;
30 use Koha::Database;
31 use Koha::DateUtils;
32 use Koha::Holds;
33 use Koha::Old::Checkouts;
34 use Koha::Patron::Categories;
35 use Koha::Patron::HouseboundProfile;
36 use Koha::Patron::HouseboundRole;
37 use Koha::Patron::Images;
38 use Koha::Patrons;
39 use Koha::Virtualshelves;
40 use Koha::Club::Enrollments;
41 use Koha::Account;
42 use Koha::Subscription::Routinglists;
43
44 use base qw(Koha::Object);
45
46 our $RESULTSET_PATRON_ID_MAPPING = {
47     Accountline          => 'borrowernumber',
48     Aqbasketuser         => 'borrowernumber',
49     Aqbudget             => 'budget_owner_id',
50     Aqbudgetborrower     => 'borrowernumber',
51     ArticleRequest       => 'borrowernumber',
52     BorrowerAttribute    => 'borrowernumber',
53     BorrowerDebarment    => 'borrowernumber',
54     BorrowerFile         => 'borrowernumber',
55     BorrowerModification => 'borrowernumber',
56     ClubEnrollment       => 'borrowernumber',
57     Issue                => 'borrowernumber',
58     ItemsLastBorrower    => 'borrowernumber',
59     Linktracker          => 'borrowernumber',
60     Message              => 'borrowernumber',
61     MessageQueue         => 'borrowernumber',
62     OldIssue             => 'borrowernumber',
63     OldReserve           => 'borrowernumber',
64     Rating               => 'borrowernumber',
65     Reserve              => 'borrowernumber',
66     Review               => 'borrowernumber',
67     SearchHistory        => 'userid',
68     Statistic            => 'borrowernumber',
69     Suggestion           => 'suggestedby',
70     TagAll               => 'borrowernumber',
71     Virtualshelfcontent  => 'borrowernumber',
72     Virtualshelfshare    => 'borrowernumber',
73     Virtualshelve        => 'owner',
74 };
75
76 =head1 NAME
77
78 Koha::Patron - Koha Patron Object class
79
80 =head1 API
81
82 =head2 Class Methods
83
84 =cut
85
86 =head3 new
87
88 =cut
89
90 sub new {
91     my ( $class, $params ) = @_;
92
93     return $class->SUPER::new($params);
94 }
95
96 sub fixup_cardnumber {
97     my ( $self ) = @_;
98     my $max = Koha::Patrons->search({
99         cardnumber => {-regexp => '^-?[0-9]+$'}
100     }, {
101         select => \'CAST(cardnumber AS SIGNED)',
102         as => ['cast_cardnumber']
103     })->_resultset->get_column('cast_cardnumber')->max;
104     $self->cardnumber($max+1);
105 }
106
107 sub store {
108     my( $self ) = @_;
109
110     $self->_result->result_source->schema->txn_do(
111         sub {
112             if (
113                 C4::Context->preference("autoMemberNum")
114                 and ( not defined $self->cardnumber
115                     or $self->cardnumber eq '' )
116               )
117             {
118                 # Warning: The caller is responsible for locking the members table in write
119                 # mode, to avoid database corruption.
120                 # We are in a transaction but the table is not locked
121                 $self->fixup_cardnumber;
122             }
123
124             $self->SUPER::store;
125         }
126     );
127 }
128
129 =head3 delete
130
131 $patron->delete
132
133 Delete patron's holds, lists and finally the patron.
134
135 Lists owned by the borrower are deleted, but entries from the borrower to
136 other lists are kept.
137
138 =cut
139
140 sub delete {
141     my ($self) = @_;
142
143     my $deleted;
144     $self->_result->result_source->schema->txn_do(
145         sub {
146             # Delete Patron's holds
147             $self->holds->delete;
148
149             # Delete all lists and all shares of this borrower
150             # Consistent with the approach Koha uses on deleting individual lists
151             # Note that entries in virtualshelfcontents added by this borrower to
152             # lists of others will be handled by a table constraint: the borrower
153             # is set to NULL in those entries.
154             # NOTE:
155             # We could handle the above deletes via a constraint too.
156             # But a new BZ report 11889 has been opened to discuss another approach.
157             # Instead of deleting we could also disown lists (based on a pref).
158             # In that way we could save shared and public lists.
159             # The current table constraints support that idea now.
160             # This pref should then govern the results of other routines/methods such as
161             # Koha::Virtualshelf->new->delete too.
162             # FIXME Could be $patron->get_lists
163             $_->delete for Koha::Virtualshelves->search( { owner => $self->borrowernumber } );
164
165             $deleted = $self->SUPER::delete;
166
167             logaction( "MEMBERS", "DELETE", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
168         }
169     );
170     return $deleted;
171 }
172
173
174 =head3 category
175
176 my $patron_category = $patron->category
177
178 Return the patron category for this patron
179
180 =cut
181
182 sub category {
183     my ( $self ) = @_;
184     return Koha::Patron::Category->_new_from_dbic( $self->_result->categorycode );
185 }
186
187 =head3 guarantor
188
189 Returns a Koha::Patron object for this patron's guarantor
190
191 =cut
192
193 sub guarantor {
194     my ( $self ) = @_;
195
196     return unless $self->guarantorid();
197
198     return Koha::Patrons->find( $self->guarantorid() );
199 }
200
201 sub image {
202     my ( $self ) = @_;
203
204     return scalar Koha::Patron::Images->find( $self->borrowernumber );
205 }
206
207 sub library {
208     my ( $self ) = @_;
209     return Koha::Library->_new_from_dbic($self->_result->branchcode);
210 }
211
212 =head3 guarantees
213
214 Returns the guarantees (list of Koha::Patron) of this patron
215
216 =cut
217
218 sub guarantees {
219     my ( $self ) = @_;
220
221     return Koha::Patrons->search( { guarantorid => $self->borrowernumber } );
222 }
223
224 =head3 housebound_profile
225
226 Returns the HouseboundProfile associated with this patron.
227
228 =cut
229
230 sub housebound_profile {
231     my ( $self ) = @_;
232     my $profile = $self->_result->housebound_profile;
233     return Koha::Patron::HouseboundProfile->_new_from_dbic($profile)
234         if ( $profile );
235     return;
236 }
237
238 =head3 housebound_role
239
240 Returns the HouseboundRole associated with this patron.
241
242 =cut
243
244 sub housebound_role {
245     my ( $self ) = @_;
246
247     my $role = $self->_result->housebound_role;
248     return Koha::Patron::HouseboundRole->_new_from_dbic($role) if ( $role );
249     return;
250 }
251
252 =head3 siblings
253
254 Returns the siblings of this patron.
255
256 =cut
257
258 sub siblings {
259     my ( $self ) = @_;
260
261     my $guarantor = $self->guarantor;
262
263     return unless $guarantor;
264
265     return Koha::Patrons->search(
266         {
267             guarantorid => {
268                 '!=' => undef,
269                 '=' => $guarantor->id,
270             },
271             borrowernumber => {
272                 '!=' => $self->borrowernumber,
273             }
274         }
275     );
276 }
277
278 =head3 merge_with
279
280     my $patron = Koha::Patrons->find($id);
281     $patron->merge_with( \@patron_ids );
282
283     This subroutine merges a list of patrons into the patron record. This is accomplished by finding
284     all related patron ids for the patrons to be merged in other tables and changing the ids to be that
285     of the keeper patron.
286
287 =cut
288
289 sub merge_with {
290     my ( $self, $patron_ids ) = @_;
291
292     my @patron_ids = @{ $patron_ids };
293
294     # Ensure the keeper isn't in the list of patrons to merge
295     @patron_ids = grep { $_ ne $self->id } @patron_ids;
296
297     my $schema = Koha::Database->new()->schema();
298
299     my $results;
300
301     $self->_result->result_source->schema->txn_do( sub {
302         foreach my $patron_id (@patron_ids) {
303             my $patron = Koha::Patrons->find( $patron_id );
304
305             next unless $patron;
306
307             # Unbless for safety, the patron will end up being deleted
308             $results->{merged}->{$patron_id}->{patron} = $patron->unblessed;
309
310             while (my ($r, $field) = each(%$RESULTSET_PATRON_ID_MAPPING)) {
311                 my $rs = $schema->resultset($r)->search({ $field => $patron_id });
312                 $results->{merged}->{ $patron_id }->{updated}->{$r} = $rs->count();
313                 $rs->update({ $field => $self->id });
314             }
315
316             $patron->move_to_deleted();
317             $patron->delete();
318         }
319     });
320
321     return $results;
322 }
323
324
325
326 =head3 wants_check_for_previous_checkout
327
328     $wants_check = $patron->wants_check_for_previous_checkout;
329
330 Return 1 if Koha needs to perform PrevIssue checking, else 0.
331
332 =cut
333
334 sub wants_check_for_previous_checkout {
335     my ( $self ) = @_;
336     my $syspref = C4::Context->preference("checkPrevCheckout");
337
338     # Simple cases
339     ## Hard syspref trumps all
340     return 1 if ($syspref eq 'hardyes');
341     return 0 if ($syspref eq 'hardno');
342     ## Now, patron pref trumps all
343     return 1 if ($self->checkprevcheckout eq 'yes');
344     return 0 if ($self->checkprevcheckout eq 'no');
345
346     # More complex: patron inherits -> determine category preference
347     my $checkPrevCheckoutByCat = $self->category->checkprevcheckout;
348     return 1 if ($checkPrevCheckoutByCat eq 'yes');
349     return 0 if ($checkPrevCheckoutByCat eq 'no');
350
351     # Finally: category preference is inherit, default to 0
352     if ($syspref eq 'softyes') {
353         return 1;
354     } else {
355         return 0;
356     }
357 }
358
359 =head3 do_check_for_previous_checkout
360
361     $do_check = $patron->do_check_for_previous_checkout($item);
362
363 Return 1 if the bib associated with $ITEM has previously been checked out to
364 $PATRON, 0 otherwise.
365
366 =cut
367
368 sub do_check_for_previous_checkout {
369     my ( $self, $item ) = @_;
370
371     # Find all items for bib and extract item numbers.
372     my @items = Koha::Items->search({biblionumber => $item->{biblionumber}});
373     my @item_nos;
374     foreach my $item (@items) {
375         push @item_nos, $item->itemnumber;
376     }
377
378     # Create (old)issues search criteria
379     my $criteria = {
380         borrowernumber => $self->borrowernumber,
381         itemnumber => \@item_nos,
382     };
383
384     # Check current issues table
385     my $issues = Koha::Checkouts->search($criteria);
386     return 1 if $issues->count; # 0 || N
387
388     # Check old issues table
389     my $old_issues = Koha::Old::Checkouts->search($criteria);
390     return $old_issues->count;  # 0 || N
391 }
392
393 =head3 is_debarred
394
395 my $debarment_expiration = $patron->is_debarred;
396
397 Returns the date a patron debarment will expire, or undef if the patron is not
398 debarred
399
400 =cut
401
402 sub is_debarred {
403     my ($self) = @_;
404
405     return unless $self->debarred;
406     return $self->debarred
407       if $self->debarred =~ '^9999'
408       or dt_from_string( $self->debarred ) > dt_from_string;
409     return;
410 }
411
412 =head3 is_expired
413
414 my $is_expired = $patron->is_expired;
415
416 Returns 1 if the patron is expired or 0;
417
418 =cut
419
420 sub is_expired {
421     my ($self) = @_;
422     return 0 unless $self->dateexpiry;
423     return 0 if $self->dateexpiry =~ '^9999';
424     return 1 if dt_from_string( $self->dateexpiry ) < dt_from_string->truncate( to => 'day' );
425     return 0;
426 }
427
428 =head3 is_going_to_expire
429
430 my $is_going_to_expire = $patron->is_going_to_expire;
431
432 Returns 1 if the patron is going to expired, depending on the NotifyBorrowerDeparture pref or 0
433
434 =cut
435
436 sub is_going_to_expire {
437     my ($self) = @_;
438
439     my $delay = C4::Context->preference('NotifyBorrowerDeparture') || 0;
440
441     return 0 unless $delay;
442     return 0 unless $self->dateexpiry;
443     return 0 if $self->dateexpiry =~ '^9999';
444     return 1 if dt_from_string( $self->dateexpiry )->subtract( days => $delay ) < dt_from_string->truncate( to => 'day' );
445     return 0;
446 }
447
448 =head3 update_password
449
450 my $updated = $patron->update_password( $userid, $password );
451
452 Update the userid and the password of a patron.
453 If the userid already exists, returns and let DBIx::Class warns
454 This will add an entry to action_logs if BorrowersLog is set.
455
456 =cut
457
458 sub update_password {
459     my ( $self, $userid, $password ) = @_;
460     eval { $self->userid($userid)->store; };
461     return if $@; # Make sure the userid is not already in used by another patron
462     $self->update(
463         {
464             password       => $password,
465             login_attempts => 0,
466         }
467     );
468     logaction( "MEMBERS", "CHANGE PASS", $self->borrowernumber, "" ) if C4::Context->preference("BorrowersLog");
469     return 1;
470 }
471
472 =head3 renew_account
473
474 my $new_expiry_date = $patron->renew_account
475
476 Extending the subscription to the expiry date.
477
478 =cut
479
480 sub renew_account {
481     my ($self) = @_;
482     my $date;
483     if ( C4::Context->preference('BorrowerRenewalPeriodBase') eq 'combination' ) {
484         $date = ( dt_from_string gt dt_from_string( $self->dateexpiry ) ) ? dt_from_string : dt_from_string( $self->dateexpiry );
485     } else {
486         $date =
487             C4::Context->preference('BorrowerRenewalPeriodBase') eq 'dateexpiry'
488             ? dt_from_string( $self->dateexpiry )
489             : dt_from_string;
490     }
491     my $expiry_date = $self->category->get_expiry_date($date);
492
493     $self->dateexpiry($expiry_date);
494     $self->date_renewed( dt_from_string() );
495     $self->store();
496
497     $self->add_enrolment_fee_if_needed;
498
499     logaction( "MEMBERS", "RENEW", $self->borrowernumber, "Membership renewed" ) if C4::Context->preference("BorrowersLog");
500     return dt_from_string( $expiry_date )->truncate( to => 'day' );
501 }
502
503 =head3 has_overdues
504
505 my $has_overdues = $patron->has_overdues;
506
507 Returns the number of patron's overdues
508
509 =cut
510
511 sub has_overdues {
512     my ($self) = @_;
513     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
514     return $self->_result->issues->search({ date_due => { '<' => $dtf->format_datetime( dt_from_string() ) } })->count;
515 }
516
517 =head3 track_login
518
519     $patron->track_login;
520     $patron->track_login({ force => 1 });
521
522     Tracks a (successful) login attempt.
523     The preference TrackLastPatronActivity must be enabled. Or you
524     should pass the force parameter.
525
526 =cut
527
528 sub track_login {
529     my ( $self, $params ) = @_;
530     return if
531         !$params->{force} &&
532         !C4::Context->preference('TrackLastPatronActivity');
533     $self->lastseen( dt_from_string() )->store;
534 }
535
536 =head3 move_to_deleted
537
538 my $is_moved = $patron->move_to_deleted;
539
540 Move a patron to the deletedborrowers table.
541 This can be done before deleting a patron, to make sure the data are not completely deleted.
542
543 =cut
544
545 sub move_to_deleted {
546     my ($self) = @_;
547     my $patron_infos = $self->unblessed;
548     delete $patron_infos->{updated_on}; #This ensures the updated_on date in deletedborrowers will be set to the current timestamp
549     return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos);
550 }
551
552 =head3 article_requests
553
554 my @requests = $borrower->article_requests();
555 my $requests = $borrower->article_requests();
556
557 Returns either a list of ArticleRequests objects,
558 or an ArtitleRequests object, depending on the
559 calling context.
560
561 =cut
562
563 sub article_requests {
564     my ( $self ) = @_;
565
566     $self->{_article_requests} ||= Koha::ArticleRequests->search({ borrowernumber => $self->borrowernumber() });
567
568     return $self->{_article_requests};
569 }
570
571 =head3 article_requests_current
572
573 my @requests = $patron->article_requests_current
574
575 Returns the article requests associated with this patron that are incomplete
576
577 =cut
578
579 sub article_requests_current {
580     my ( $self ) = @_;
581
582     $self->{_article_requests_current} ||= Koha::ArticleRequests->search(
583         {
584             borrowernumber => $self->id(),
585             -or          => [
586                 { status => Koha::ArticleRequest::Status::Pending },
587                 { status => Koha::ArticleRequest::Status::Processing }
588             ]
589         }
590     );
591
592     return $self->{_article_requests_current};
593 }
594
595 =head3 article_requests_finished
596
597 my @requests = $biblio->article_requests_finished
598
599 Returns the article requests associated with this patron that are completed
600
601 =cut
602
603 sub article_requests_finished {
604     my ( $self, $borrower ) = @_;
605
606     $self->{_article_requests_finished} ||= Koha::ArticleRequests->search(
607         {
608             borrowernumber => $self->id(),
609             -or          => [
610                 { status => Koha::ArticleRequest::Status::Completed },
611                 { status => Koha::ArticleRequest::Status::Canceled }
612             ]
613         }
614     );
615
616     return $self->{_article_requests_finished};
617 }
618
619 =head3 add_enrolment_fee_if_needed
620
621 my $enrolment_fee = $patron->add_enrolment_fee_if_needed;
622
623 Add enrolment fee for a patron if needed.
624
625 =cut
626
627 sub add_enrolment_fee_if_needed {
628     my ($self) = @_;
629     my $enrolment_fee = $self->category->enrolmentfee;
630     if ( $enrolment_fee && $enrolment_fee > 0 ) {
631         # insert fee in patron debts
632         C4::Accounts::manualinvoice( $self->borrowernumber, '', '', 'A', $enrolment_fee );
633     }
634     return $enrolment_fee || 0;
635 }
636
637 =head3 checkouts
638
639 my $checkouts = $patron->checkouts
640
641 =cut
642
643 sub checkouts {
644     my ($self) = @_;
645     my $checkouts = $self->_result->issues;
646     return Koha::Checkouts->_new_from_dbic( $checkouts );
647 }
648
649 =head3 pending_checkouts
650
651 my $pending_checkouts = $patron->pending_checkouts
652
653 This method will return the same as $self->checkouts, but with a prefetch on
654 items, biblio and biblioitems.
655
656 It has been introduced to replaced the C4::Members::GetPendingIssues subroutine
657
658 It should not be used directly, prefer to access fields you need instead of
659 retrieving all these fields in one go.
660
661
662 =cut
663
664 sub pending_checkouts {
665     my( $self ) = @_;
666     my $checkouts = $self->_result->issues->search(
667         {},
668         {
669             order_by => [
670                 { -desc => 'me.timestamp' },
671                 { -desc => 'issuedate' },
672                 { -desc => 'issue_id' }, # Sort by issue_id should be enough
673             ],
674             prefetch => { item => { biblio => 'biblioitems' } },
675         }
676     );
677     return Koha::Checkouts->_new_from_dbic( $checkouts );
678 }
679
680 =head3 old_checkouts
681
682 my $old_checkouts = $patron->old_checkouts
683
684 =cut
685
686 sub old_checkouts {
687     my ($self) = @_;
688     my $old_checkouts = $self->_result->old_issues;
689     return Koha::Old::Checkouts->_new_from_dbic( $old_checkouts );
690 }
691
692 =head3 get_overdues
693
694 my $overdue_items = $patron->get_overdues
695
696 Return the overdue items
697
698 =cut
699
700 sub get_overdues {
701     my ($self) = @_;
702     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
703     return $self->checkouts->search(
704         {
705             'me.date_due' => { '<' => $dtf->format_datetime(dt_from_string) },
706         },
707         {
708             prefetch => { item => { biblio => 'biblioitems' } },
709         }
710     );
711 }
712
713 =head3 get_routing_lists
714
715 my @routinglists = $patron->get_routing_lists
716
717 Returns the routing lists a patron is subscribed to.
718
719 =cut
720
721 sub get_routing_lists {
722     my ($self) = @_;
723     my $routing_list_rs = $self->_result->subscriptionroutinglists;
724     return Koha::Subscription::Routinglists->_new_from_dbic($routing_list_rs);
725 }
726
727 =head3 get_age
728
729 my $age = $patron->get_age
730
731 Return the age of the patron
732
733 =cut
734
735 sub get_age {
736     my ($self)    = @_;
737     my $today_str = dt_from_string->strftime("%Y-%m-%d");
738     return unless $self->dateofbirth;
739     my $dob_str   = dt_from_string( $self->dateofbirth )->strftime("%Y-%m-%d");
740
741     my ( $dob_y,   $dob_m,   $dob_d )   = split /-/, $dob_str;
742     my ( $today_y, $today_m, $today_d ) = split /-/, $today_str;
743
744     my $age = $today_y - $dob_y;
745     if ( $dob_m . $dob_d > $today_m . $today_d ) {
746         $age--;
747     }
748
749     return $age;
750 }
751
752 =head3 account
753
754 my $account = $patron->account
755
756 =cut
757
758 sub account {
759     my ($self) = @_;
760     return Koha::Account->new( { patron_id => $self->borrowernumber } );
761 }
762
763 =head3 holds
764
765 my $holds = $patron->holds
766
767 Return all the holds placed by this patron
768
769 =cut
770
771 sub holds {
772     my ($self) = @_;
773     my $holds_rs = $self->_result->reserves->search( {}, { order_by => 'reservedate' } );
774     return Koha::Holds->_new_from_dbic($holds_rs);
775 }
776
777 =head3 old_holds
778
779 my $old_holds = $patron->old_holds
780
781 Return all the historical holds for this patron
782
783 =cut
784
785 sub old_holds {
786     my ($self) = @_;
787     my $old_holds_rs = $self->_result->old_reserves->search( {}, { order_by => 'reservedate' } );
788     return Koha::Old::Holds->_new_from_dbic($old_holds_rs);
789 }
790
791 =head3 notice_email_address
792
793   my $email = $patron->notice_email_address;
794
795 Return the email address of patron used for notices.
796 Returns the empty string if no email address.
797
798 =cut
799
800 sub notice_email_address{
801     my ( $self ) = @_;
802
803     my $which_address = C4::Context->preference("AutoEmailPrimaryAddress");
804     # if syspref is set to 'first valid' (value == OFF), look up email address
805     if ( $which_address eq 'OFF' ) {
806         return $self->first_valid_email_address;
807     }
808
809     return $self->$which_address || '';
810 }
811
812 =head3 first_valid_email_address
813
814 my $first_valid_email_address = $patron->first_valid_email_address
815
816 Return the first valid email address for a patron.
817 For now, the order  is defined as email, emailpro, B_email.
818 Returns the empty string if the borrower has no email addresses.
819
820 =cut
821
822 sub first_valid_email_address {
823     my ($self) = @_;
824
825     return $self->email() || $self->emailpro() || $self->B_email() || q{};
826 }
827
828 =head3 get_club_enrollments
829
830 =cut
831
832 sub get_club_enrollments {
833     my ( $self, $return_scalar ) = @_;
834
835     my $e = Koha::Club::Enrollments->search( { borrowernumber => $self->borrowernumber(), date_canceled => undef } );
836
837     return $e if $return_scalar;
838
839     return wantarray ? $e->as_list : $e;
840 }
841
842 =head3 get_enrollable_clubs
843
844 =cut
845
846 sub get_enrollable_clubs {
847     my ( $self, $is_enrollable_from_opac, $return_scalar ) = @_;
848
849     my $params;
850     $params->{is_enrollable_from_opac} = $is_enrollable_from_opac
851       if $is_enrollable_from_opac;
852     $params->{is_email_required} = 0 unless $self->first_valid_email_address();
853
854     $params->{borrower} = $self;
855
856     my $e = Koha::Clubs->get_enrollable($params);
857
858     return $e if $return_scalar;
859
860     return wantarray ? $e->as_list : $e;
861 }
862
863 =head3 account_locked
864
865 my $is_locked = $patron->account_locked
866
867 Return true if the patron has reach the maximum number of login attempts (see pref FailedLoginAttempts).
868 Otherwise return false.
869 If the pref is not set (empty string, null or 0), the feature is considered as disabled.
870
871 =cut
872
873 sub account_locked {
874     my ($self) = @_;
875     my $FailedLoginAttempts = C4::Context->preference('FailedLoginAttempts');
876     return ( $FailedLoginAttempts
877           and $self->login_attempts
878           and $self->login_attempts >= $FailedLoginAttempts )? 1 : 0;
879 }
880
881 =head3 can_see_patron_infos
882
883 my $can_see = $patron->can_see_patron_infos( $patron );
884
885 Return true if the patron (usually the logged in user) can see the patron's infos for a given patron
886
887 =cut
888
889 sub can_see_patron_infos {
890     my ( $self, $patron ) = @_;
891     return $self->can_see_patrons_from( $patron->library->branchcode );
892 }
893
894 =head3 can_see_patrons_from
895
896 my $can_see = $patron->can_see_patrons_from( $branchcode );
897
898 Return true if the patron (usually the logged in user) can see the patron's infos from a given library
899
900 =cut
901
902 sub can_see_patrons_from {
903     my ( $self, $branchcode ) = @_;
904     my $can = 0;
905     if ( $self->branchcode eq $branchcode ) {
906         $can = 1;
907     } elsif ( $self->has_permission( { borrowers => 'view_borrower_infos_from_any_libraries' } ) ) {
908         $can = 1;
909     } elsif ( my $library_groups = $self->library->library_groups ) {
910         while ( my $library_group = $library_groups->next ) {
911             if ( $library_group->parent->has_child( $branchcode ) ) {
912                 $can = 1;
913                 last;
914             }
915         }
916     }
917     return $can;
918 }
919
920 =head3 libraries_where_can_see_patrons
921
922 my $libraries = $patron-libraries_where_can_see_patrons;
923
924 Return the list of branchcodes(!) of libraries the patron is allowed to see other patron's infos.
925 The branchcodes are arbitrarily returned sorted.
926 We are supposing here that the object is related to the logged in patron (use of C4::Context::only_my_library)
927
928 An empty array means no restriction, the patron can see patron's infos from any libraries.
929
930 =cut
931
932 sub libraries_where_can_see_patrons {
933     my ( $self ) = @_;
934     my $userenv = C4::Context->userenv;
935
936     return () unless $userenv; # For tests, but userenv should be defined in tests...
937
938     my @restricted_branchcodes;
939     if (C4::Context::only_my_library) {
940         push @restricted_branchcodes, $self->branchcode;
941     }
942     else {
943         unless (
944             $self->has_permission(
945                 { borrowers => 'view_borrower_infos_from_any_libraries' }
946             )
947           )
948         {
949             my $library_groups = $self->library->library_groups({ ft_hide_patron_info => 1 });
950             if ( $library_groups->count )
951             {
952                 while ( my $library_group = $library_groups->next ) {
953                     my $parent = $library_group->parent;
954                     if ( $parent->has_child( $self->branchcode ) ) {
955                         push @restricted_branchcodes, $parent->children->get_column('branchcode');
956                     }
957                 }
958             }
959
960             @restricted_branchcodes = ( $self->branchcode ) unless @restricted_branchcodes;
961         }
962     }
963
964     @restricted_branchcodes = grep { defined $_ } @restricted_branchcodes;
965     @restricted_branchcodes = uniq(@restricted_branchcodes);
966     @restricted_branchcodes = sort(@restricted_branchcodes);
967     return @restricted_branchcodes;
968 }
969
970 sub has_permission {
971     my ( $self, $flagsrequired ) = @_;
972     return unless $self->userid;
973     # TODO code from haspermission needs to be moved here!
974     return C4::Auth::haspermission( $self->userid, $flagsrequired );
975 }
976
977 =head3 is_adult
978
979 my $is_adult = $patron->is_adult
980
981 Return true if the patron has a category with a type Adult (A) or Organization (I)
982
983 =cut
984
985 sub is_adult {
986     my ( $self ) = @_;
987     return $self->category->category_type =~ /^(A|I)$/ ? 1 : 0;
988 }
989
990 =head3 is_child
991
992 my $is_child = $patron->is_child
993
994 Return true if the patron has a category with a type Child (C)
995
996 =cut
997 sub is_child {
998     my( $self ) = @_;
999     return $self->category->category_type eq 'C' ? 1 : 0;
1000 }
1001
1002 =head3 has_valid_userid
1003
1004 my $patron = Koha::Patrons->find(42);
1005 $patron->userid( $new_userid );
1006 my $has_a_valid_userid = $patron->has_valid_userid
1007
1008 my $patron = Koha::Patron->new( $params );
1009 my $has_a_valid_userid = $patron->has_valid_userid
1010
1011 Return true if the current userid of this patron is valid/unique, otherwise false.
1012
1013 Note that this should be done in $self->store instead and raise an exception if needed.
1014
1015 =cut
1016
1017 sub has_valid_userid {
1018     my ($self) = @_;
1019
1020     return 0 unless $self->userid;
1021
1022     return 0 if ( $self->userid eq C4::Context->config('user') );    # DB user
1023
1024     my $already_exists = Koha::Patrons->search(
1025         {
1026             userid => $self->userid,
1027             (
1028                 $self->in_storage
1029                 ? ( borrowernumber => { '!=' => $self->borrowernumber } )
1030                 : ()
1031             ),
1032         }
1033     )->count;
1034     return $already_exists ? 0 : 1;
1035 }
1036
1037 =head3 generate_userid
1038
1039 my $patron = Koha::Patron->new( $params );
1040 my $userid = $patron->generate_userid
1041
1042 Generate a userid using the $surname and the $firstname (if there is a value in $firstname).
1043
1044 Return the generate userid ($firstname.$surname if there is a $firstname, or $surname if there is no value in $firstname) plus offset (0 if the $userid is unique, or a higher numeric value if not unique).
1045
1046 # Note: Should we set $self->userid with the generated value?
1047 # Certainly yes, but we AddMember and ModMember will be rewritten
1048
1049 =cut
1050
1051 sub generate_userid {
1052     my ($self) = @_;
1053     my $userid;
1054     my $offset = 0;
1055     my $existing_userid = $self->userid;
1056     my $firstname = $self->firstname // q{};
1057     my $surname = $self->surname // q{};
1058     #The script will "do" the following code and increment the $offset until the generated userid is unique
1059     do {
1060       $firstname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1061       $surname =~ s/[[:digit:][:space:][:blank:][:punct:][:cntrl:]]//g;
1062       $userid = lc(($firstname)? "$firstname.$surname" : $surname);
1063       $userid = unac_string('utf-8',$userid);
1064       $userid .= $offset unless $offset == 0;
1065       $self->userid( $userid );
1066       $offset++;
1067      } while (! $self->has_valid_userid );
1068
1069      # Resetting to the previous value as the callers do not expect
1070      # this method to modify the userid attribute
1071      # This will be done later (move of AddMember and ModMember)
1072      $self->userid( $existing_userid );
1073
1074      return $userid;
1075
1076 }
1077
1078 =head2 Internal methods
1079
1080 =head3 _type
1081
1082 =cut
1083
1084 sub _type {
1085     return 'Borrower';
1086 }
1087
1088 =head1 AUTHOR
1089
1090 Kyle M Hall <kyle@bywatersolutions.com>
1091 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1092
1093 =cut
1094
1095 1;