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