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