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