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