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