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